Important: This documentation covers Yarn 1 (Classic).
For Yarn 2+ docs and migration guide, see yarnpkg.com.

Package detail

@pothos/plugin-dataloader

hayes60.8kISC4.3.0TypeScript support: included

A Pothos plugin for attaching dataloader to object types

pothos, graphql, schema, typescript, dataloader

readme

Dataloader Plugin

This plugin makes it easy to add fields and types that are loaded through a dataloader.

Usage

Install

To use the dataloader plugin you will need to install both the dataloader package and the Pothos dataloader plugin:

yarn add dataloader @pothos/plugin-dataloader

Setup

import DataloaderPlugin from '@pothos/plugin-dataloader';

const builder = new SchemaBuilder({
  plugins: [DataloaderPlugin],
});

loadable objects

To create an object type that can be loaded with a dataloader use the new builder.loadableObject method:

const User = builder.loadableObject('User', {
  // load will be called with ids of users that need to be loaded
  // Note that the types for keys (and context if present) are required
  load: (ids: string[], context: ContextType) => context.loadUsersById(ids),
  fields: (t) => ({
    id: t.exposeID('id', {}),
    username: t.string({
      // the shape of parent will be inferred from `loadUsersById()` above
      resolve: (parent) => parent.username,
    }),
  }),
});

It is VERY IMPORTANT to return values from load in an order that exactly matches the order of the requested IDs. The order is used to map results to their IDs, and if the results are returned in a different order, your GraphQL requests will end up with the wrong data. Correctly sorting results returned from a database or other data source can be tricky, so there this plugin has a sort option (described below) to simplify the sorting process. For more details on how the load function works, see the dataloader docs.

When defining fields that return Users, you will now be able to return either a string (based in ids param of load), or a User object (type based on the return type of loadUsersById).

builder.queryType({
  fields: (t) => ({
    user: t.field({
      type: User,
      args: {
        id: t.arg.string({ required: true }),
      },
      // Here we can just return the ID directly rather than loading the user ourselves
      resolve: (root, args) => args.id,
    }),
    currentUser: t.field({
      type: User,
      // If we already have the user, we use it, and the dataloader will not be called
      resolve: (root, args, context) => context.currentUser,
    }),
    users: t.field({
      type: [User],
      args: {
        ids: t.arg.stringList({ required: true }),
      },
      // Mixing ids and user objects also works
      resolve: (_root, args, context) => [...args.ids, context.CurrentUser],
    }),
  }),
});

Pothos will detect when a resolver returns string, number, or bigint (typescript will constrain the allowed types to whatever is expected by the load function). If a resolver returns an object instead, Pothos knows it can skip the dataloader for that object.

loadable fields

In some cases you may need more granular dataloaders. To handle these cases there is a new t.loadable method for defining fields with their own dataloaders.

// Normal object that the fields below will load
interface PostShape {
  id: string;
  title: string;
  content: string;
}

const Post = builder.objectRef<PostShape>('Post').implement({
  fields: (t) => ({
    id: t.exposeID('id', {}),
    title: t.exposeString('title', {}),
    content: t.exposeString('title', {}),
  }),
});

// Loading a single Post
builder.objectField(User, 'latestPost', (t) =>
  t.loadable({
    type: Post,
    // will be called with ids of latest posts for all users in query
    load: (ids: number[], context) => context.loadPosts(ids),
    resolve: (user, args) => user.lastPostID,
  }),
);
// Loading multiple Posts
builder.objectField(User, 'posts', (t) =>
  t.loadable({
    type: [Post],
    // will be called with ids of posts loaded for all users in query
    load: (ids: number[], context) => context.loadPosts(ids),
    resolve: (user, args) => user.postIDs,
  }),
);

loadableList fields for one-to-many relations

loadable fields can return lists, but do not work for loading a list of records from a single id.

The loadableList method can be used to define loadable fields that represent this kind of relationship.

// Loading multiple Posts
builder.objectField(User, 'posts', (t) =>
  t.loadableList({
    // type is singular, but will create a list field
    type: Post,
    // will be called with ids of all the users, and should return `Post[][]`
    load: (ids: number[], context) => context.postsByUserIds(ids),
    resolve: (user, args) => user.id,
  }),
);

dataloader options

You can provide additional options for your dataloaders using loaderOptions.

const User = builder.loadableObject('User', {
  loaderOptions: { maxBatchSize: 20 },
  load: (ids: string[], context: ContextType) => context.loadUsersById(ids),
  fields: (t) => ({ id: t.exposeID('id', {}) }),
});

builder.objectField(User, 'posts', (t) =>
  t.loadable({
    type: [Post],
    loaderOptions: { maxBatchSize: 20 },
    load: (ids: number[], context) => context.loadPosts(ids),
    resolve: (user, args) => user.postIDs,
  }),
);

See dataloader docs for all available options.

Manually using dataloader

Dataloaders for "loadable" objects can be accessed via their ref by passing in the context object for the current request. dataloaders are not shared across requests, so we need the context to get the correct dataloader for the current request:

// create loadable object
const User = builder.loadableObject('User', {
  load: (ids: string[], context: ContextType) => context.loadUsersById(ids),
  fields: (t) => ({
    id: t.exposeID('id', {}),
  }),
});

builder.queryField('user', (t) =>
  t.field({
    type: User,
    resolve: (parent, args, context) => {
      // get data loader for User type
      const loader = User.getDataloader(context);

      // manually load a user
      return loader.load('123');
    },
  }),
);

Errors

Calling dataloader.loadMany will resolve to a value like (Type | Error)[]. Your load function may also return results in that format if your loader can have parital failures. GraphQL does not have special handling for Error objects. Instead Pothos will map these results to something like (Type | Promise<Type>)[] where Errors are replaced with promises that will be rejected. This allows the normal graphql resolver flow to correctly handle these errors.

If you are using the loadMany method from a dataloader manually, you can apply the same mapping using the rejectErrors helper:

import { rejectErrors } from '@pothos/plugin-dataloader';

builder.queryField('user', (t) =>
  t.field({
    type: [User],
    resolve: (parent, args, context) => {
      const loader = User.getDataloader(context);

      return rejectErrors(loader.loadMany(['123', '456']));
    },
  }),
);

(Optional) Adding loaders to context

If you want to make dataloaders accessible via the context object directly, there is some additional setup required. Below are a few options for different ways you can load data from the context object. You can determine which of these options works best for you or add you own helpers.

First you'll need to update the types for your context type:

import { LoadableRef } from '@pothos/plugin-dataloader';

export interface ContextType {
  userLoader: DataLoader<string, { id: number }>; // expose a specific loader
  getLoader: <K, V>(ref: LoadableRef<K, V, ContextType>) => DataLoader<K, V>; // helper to get a loader from a ref
  load: <K, V>(ref: LoadableRef<K, V, ContextType>, id: K) => Promise<V>; // helper for loading a single resource
  loadMany: <K, V>(ref: LoadableRef<K, V, ContextType>, ids: K[]) => Promise<(Error | V)[]>; // helper for loading many
  // other context fields
}

next you'll need to update your context factory function. The exact format of this depends on what graphql server implementation you are using.

import { initContextCache } from '@pothos/core';
import { LoadableRef, rejectErrors } from '@pothos/plugin-dataloader';

export const createContext = (req, res): ContextType => ({
  // Adding this will prevent any issues if you server implementation
  // copies or extends the context object before passing it to your resolvers
  ...initContextCache(),

  // using getters allows us to access the context object using `this`
  get userLoader() {
    return User.getDataloader(this);
  },
  get getLoader() {
    return <K, V>(ref: LoadableRef<K, V, ContextType>) => ref.getDataloader(this);
  },
  get load() {
    return <K, V>(ref: LoadableRef<K, V, ContextType>, id: K) => ref.getDataloader(this).load(id);
  },
  get loadMany() {
    return <K, V>(ref: LoadableRef<K, V, ContextType>, ids: K[]) =>
      rejectErrors(ref.getDataloader(this).loadMany(ids));
  },
});

Now you can use these helpers from your context object:

builder.queryFields((t) => ({
  fromContext1: t.field({
    type: User,
    resolve: (root, args, { userLoader }) => userLoader.load('123'),
  }),
  fromContext2: t.field({
    type: User,
    resolve: (root, args, { getLoader }) => getLoader(User).load('456'),
  }),
  fromContext3: t.field({
    type: User,
    resolve: (root, args, { load }) => load(User, '789'),
  }),
  fromContext4: t.field({
    type: [User],
    resolve: (root, args, { loadMany }) => loadMany(User, ['123', '456']),
  }),
}));

Using with the Relay plugin

If you are using the Relay plugin, there is an additional method loadableNode that gets added to the builder. You can use this method to create node objects that work like other loadeble objects.

const UserNode = builder.loadableNode('UserNode', {
  id: {
    resolve: (user) => user.id,
  },
  load: (ids: string[], context: ContextType) => context.loadUsersById(ids),
  fields: (t) => ({}),
});

Loadable Refs and Circular references

You may run into type errors if you define 2 loadable objects that circularly reference each other in their definitions.

There are a some general strategies to avoid this outlined in the circular-references guide.

This plug also has methods for creating refs (similar to builder.objectRef) that can be used to split the definition and implementation of your types to avoid any issues with circular references.

const User = builder.loadableObjectRef('User', {
  load: (ids: string[], context: ContextType) => context.loadUsersById(ids),
});

User.implement({
  fields: (t) => ({
    id: t.exposeID('id', {}),
  }),
});

// Or with relay
const UserNode = builder.loadableNodeRef('UserNode', {
  load: (ids: string[], context: ContextType) => context.loadUsersById(ids),
  id: {
    resolve: (user) => user.id,
  },
});

UserNode.implement({
  isTypeOf: (obj) => obj instanceof User,
  fields: (t) => ({}),
});

All the plugin specific options should be passed when defining the ref. This allows the ref to be used by any method that accepts a ref to implement an object:

const User = builder.loadableObjectRef('User', {
  load: (ids: string[], context: ContextType) => context.loadUsersById(ids),
});

builder.objectType(User, {
  fields: (t) => ({
    id: t.exposeID('id', {}),
  }),
});

The above example is not useful on its own, but this pattern will allow these refs to be used with other that also allow you to define object types with additional behaviors.

Caching resources loaded manually in a resolver

When manually loading a resource in a resolver it is not automatically added to the dataloader cache. If you want any resolved value to be stored in the cache in case it is used somewhere else in the query you can use the cacheResolved option.

The cacheResolved option takes a function that converts the loaded object into it's cache Key:

const User = builder.loadableObject('User', {
  load: (ids: string[], context: ContextType) => context.loadUsersById(ids),
  cacheResolved: user => user.id,
  fields: (t) => ({
    id: t.exposeID('id', {}),
    ...
  }),
});

Whenever a resolver returns a User or list or Users, those objects will automatically be added the dataloaders cache, so they can be re-used in other parts of the query.

Sorting results from your load function

As mentioned above, the load function must return results in the same order as the provided array of IDs. Doing this correctly can be a little complicated, so this plugin includes an alternative. For any type or field that creates a dataloader, you can also provide a sort option which will correctly map your results into the correct order based on their ids. To do this, you will need to provide a function that accepts a result object, and returns its id.

const User = builder.loadableObject('User', {
  load: (ids: string[], context: ContextType) => context.loadUsersById(ids),
  sort: user => user.id,
  fields: (t) => ({
    id: t.exposeID('id', {}),
    ...
  }),
});

This will also work with loadable nodes, interfaces, unions, or fields.

When sorting, if the list of results contains an Error the error is thrown because it can not be mapped to the correct location. This sort option should NOT be used for cases where the result list is expected to contain errors.

Shared toKey method.

Defining multiple functions to extract the key from a loaded object can become redundant. In cases when you are using both cacheResolved and sort you can use a toKey function instead:

const User = builder.loadableObject('User', {
  load: (ids: string[], context: ContextType) => context.loadUsersById(ids),
  toKey: user => user.id,
  cacheResolved: true,
  sort: true,
  fields: (t) => ({
    id: t.exposeID('id', {}),
    ...
  }),
});

changelog

Change Log

4.3.0

Minor Changes

  • 10e364c: expose info when using byPath

4.2.0

Minor Changes

  • 6a80a7c: Use builder.nodeRef in ImplementableLoadableNodeRef to avoid re-implementing node logic

4.1.1

Patch Changes

  • 75f7830: Fix t.loadable with nullable: { items: true }

4.1.0

Minor Changes

  • 27af377: replace eslint and prettier with biome

4.0.2

Patch Changes

4.0.1

Patch Changes

  • 9bd203e: Fix graphql peer dependency version to match documented minumum version
  • Updated dependencies [9bd203e]

4.0.0

Major Changes

Patch Changes

  • c1e6dcb: update readmes
  • Updated dependencies [c1e6dcb]
  • Updated dependencies [29841a8]

4.0.0-next.1

Patch Changes

4.0.0-next.0

Major Changes

Patch Changes

3.19.0

Minor Changes

  • c84bfc4: Improve typing when returning errors from dataloader methods

3.18.2

Patch Changes

  • 1ecea46: revert accidental pinning of graphql peer dependency

3.18.1

Patch Changes

  • 144041f: Fix cacheKey and small type issue for byPath option

3.18.0

Minor Changes

  • 3e20fd4: Add byPath option to loadable field methods that groups by the path in the query rather than the field. This allows the load method to access the fields args
  • 3e20fd4: Add a new loadableGroup method for easier batch loading of where-in style queries for loadable lists

3.17.2

Patch Changes

  • 9db5200: Improve handling of mismatched result sizes in dataloaders

3.17.1

Patch Changes

  • 4c6bc638: Add provinance to npm releases

3.17.0

Minor Changes

  • 1878d5d9: Allow readonly arrays in more places

3.16.0

Minor Changes

  • e8d75349: - allow connection fields (edges / pageInfo) to be promises
    • add completeValue helper to core for unwrapping MaybePromise values
    • set nodes as null if edges is null and the field permits a null return

3.15.0

Minor Changes

  • 22041db0: Add default isTypeOf for loadableNode
  • 68c94e4f: Support parsing globalIDs for loadableNode

3.14.0

Minor Changes

  • bf0385ae: Add new PothosError classes

3.13.0

Minor Changes

  • cd1c0502: Add support for nested lists

3.12.7

Patch Changes

  • d4d41796: Update dev dependencies

3.12.6

Patch Changes

  • 6f00194c: Fix an issue with esm import transform

3.12.5

Patch Changes

  • b12f9122: Fix issue with esm build script

3.12.4

Patch Changes

  • 9fa27cf7: Transform dynamic type imports in d.ts files

3.12.3

Patch Changes

  • 3a82d645: Apply esm transform to esm d.ts definitions

3.12.2

Patch Changes

  • 218fc68b: Fix script for copying ems d.ts definitions

3.12.1

Patch Changes

  • 67531f1e: Create separate typescript definitions for esm files

3.12.0

Minor Changes

  • 11929311: Update type definitions to work with module: "nodeNext"

3.11.1

Patch Changes

  • aa18acb7: update dev dependencies
  • aa18acb7: Fix nullable loadable fields

3.11.0

Minor Changes

  • d67764b5: Make options objecst on toSchema, queryType, and mutationType optional

3.10.0

Minor Changes

  • 390e74a7: Add idFieldOptions to relay plugin options

3.9.0

Minor Changes

  • f7f74585: Add option for configuring name of id field for relay nodes

3.8.0

Minor Changes

  • 3a7ff291: Refactor internal imports to remove import cycles

Patch Changes

  • 3a7ff291: Update dev dependencies

3.7.1

Patch Changes

  • 7311904e: Update dev deps

3.7.0

Minor Changes

  • ecb2714c: Add types entry to export map in package.json and update dev dependencies

    This should fix compatibility with typescripts new "moduleResolution": "node12"

3.6.2

Patch Changes

  • 6e4ccc7b: Fix loadable refs when used with builder.objectType

3.6.1

Patch Changes

  • 971f1aad: Update dev dependencies

3.6.0

Minor Changes

  • 241a385f: Add peer dependency on @pothos/core

3.5.0

Minor Changes

  • 6279235f: Update build process to use swc and move type definitions to dts directory

Patch Changes

  • 21a2454e: update dev dependencies

3.4.0

Minor Changes

  • c0bdbc1b: Add loadableObjectRef loadableInterfaceRef and loadableNodeRef

3.3.1

Patch Changes

  • 03aecf76: update .npmignore

3.3.0

Minor Changes

  • 4ad5f4ff: Normalize resolveType and isTypeOf behavior to match graphql spec behavior and allow both to be optional

Patch Changes

  • 43ca3031: Update dev dependencies

3.2.0

Minor Changes

  • eb9c33b8: Add loadManyWithoutCache option to dataloader to avoid double caching in loadableNode

3.1.1

Patch Changes

  • 2d9b21cd: Use workspace:* for dev dependencies on pothos packages

3.1.0

Minor Changes

  • 7593d24f: Add loadableList method to dataloader plugin for handling one-to-many relations

3.0.0

Major Changes

  • 4caad5e4: Rename GiraphQL to Pothos

2.20.0

Minor Changes

  • 9307635a: Migrate build process to use turborepo

2.19.3

Patch Changes

  • 2b08f852: Fix syntax highlighting in docs and update npm README.md files"

2.19.2

Patch Changes

  • c6aa732: graphql@15 type compatibility fix

2.19.1

Patch Changes

  • c85dc33: Add types entry in package.json

2.19.0

Minor Changes

  • aeef5e5: Update dependencies

2.18.0

Minor Changes

  • 9107f29: Update dependencies (includes graphql 16)

2.17.0

Minor Changes

  • 17db3bd: Make type refs extendable by plugins

2.16.1

Patch Changes

  • c976bfe: Update dependencies

2.16.0

Minor Changes

  • 3f104b3: Add new sort and toKey options to allow automatic sorting of loadable objects and fields so load functions can return values in arbirary order

2.15.0

Minor Changes

  • 5562695: Add loadableInterface and loadableUnion methods

2.14.1

Patch Changes

  • 4150f92: Fixed esm transformer for path-imports from dependencies

2.14.0

Minor Changes

  • dc87e68: update esm build process so extensions are added during build rather than in source

2.13.0

Minor Changes

  • 8c83898: Adds option to prime dataloaders with objects returned from the resolver.

Patch Changes

  • b4b8381: Updrade deps (typescript 4.4)

2.12.0

Minor Changes

  • 4f9b886: Add integration between error and dataloader plugins to that errors from dataloaders can be handled via errors plugin

2.11.0

Minor Changes

  • f70501b: Add support for classes and object refs with dataloader objects

2.10.0

Minor Changes

  • a4c87cf: Use ".js" extensions everywhere and add module and exports to package.json to better support ems in node

2.9.2

Patch Changes

  • f13208c: bump to fix latest tag

2.9.1

Patch Changes

  • 9ab8fbc: re-release previous version due to build-process issue

2.9.0

Minor Changes

  • 3dd3ff14: Updated dev dependencies, switched to pnpm, and added changesets for releases

All notable changes to this project will be documented in this file. See Conventional Commits for commit guidelines.

2.8.1 - 2021-08-05

Note: Version bump only for package @giraphql/plugin-dataloader

2.8.0 - 2021-08-03

🚀 Updates

Note: Version bump only for package @giraphql/plugin-dataloader

2.7.1-alpha.0 - 2021-08-02

Note: Version bump only for package @giraphql/plugin-dataloader

2.7.0 - 2021-07-30

🚀 Updates

Note: Version bump only for package @giraphql/plugin-dataloader

2.6.0 - 2021-07-29

Note: Version bump only for package @giraphql/plugin-dataloader

2.6.0-alpha.0 - 2021-07-28

🚀 Updates

  • expose input and object ref from relayMutationField (af5a061)

🐞 Fixes

  • improve handling of null edges in resolveConnection helpers (6577a00)

Note: Version bump only for package @giraphql/plugin-dataloader

2.5.6 - 2021-07-23

Note: Version bump only for package @giraphql/plugin-dataloader

2.5.6-alpha.0 - 2021-07-17

Note: Version bump only for package @giraphql/plugin-dataloader

2.5.5 - 2021-07-10

Note: Version bump only for package @giraphql/plugin-dataloader

2.5.4 - 2021-07-04

Note: Version bump only for package @giraphql/plugin-dataloader

2.5.4-alpha.1 - 2021-07-04

📦 Dependencies

Note: Version bump only for package @giraphql/plugin-dataloader

2.5.4-alpha.0 - 2021-07-03

Note: Version bump only for package @giraphql/plugin-dataloader

2.5.3 - 2021-07-02

Note: Version bump only for package @giraphql/plugin-dataloader

2.5.2 - 2021-07-02

Note: Version bump only for package @giraphql/plugin-dataloader

2.5.1 - 2021-06-29

🐞 Fixes

  • loadableNode should correctly include additional interfaces (f11f7d7)

Note: Version bump only for package @giraphql/plugin-dataloader

2.5.0 - 2021-06-28

Note: Version bump only for package @giraphql/plugin-dataloader

2.5.0-alpha.1 - 2021-06-28

Note: Version bump only for package @giraphql/plugin-dataloader

2.5.0-alpha.0 - 2021-06-28

🚀 Updates

Note: Version bump only for package @giraphql/plugin-dataloader

2.4.0 - 2021-06-11

🚀 Updates

  • make field options args optional when empty (ae71648)

📦 Dependencies

Note: Version bump only for package @giraphql/plugin-dataloader

2.3.0 - 2021-06-10

Note: Version bump only for package @giraphql/plugin-dataloader

2.3.0-alpha.0 - 2021-06-09

🚀 Updates

  • plum parentShape through all ussage of output refs (2dac2ca)

Note: Version bump only for package @giraphql/plugin-dataloader

2.2.4 - 2021-05-31

🐞 Fixes

  • support interfaces on loadableObject and loadableNode (1dd672c)

Note: Version bump only for package @giraphql/plugin-dataloader

2.2.4-alpha.2 - 2021-05-29

Note: Version bump only for package @giraphql/plugin-dataloader

2.2.4-alpha.1 - 2021-05-29

Note: Version bump only for package @giraphql/plugin-dataloader

2.2.4-alpha.0 - 2021-05-29

Note: Version bump only for package @giraphql/plugin-dataloader

2.2.3 - 2021-05-28

Note: Version bump only for package @giraphql/plugin-dataloader

2.2.2 - 2021-05-26

Note: Version bump only for package @giraphql/plugin-dataloader

2.2.1 - 2021-05-18

Note: Version bump only for package @giraphql/plugin-dataloader

2.2.0 - 2021-05-13

🚀 Updates

  • add loadableNodes method to use relay and dataloader plugin together (966c06f)

📘 Docs

  • add docs for loadableNode (1ae01e8)

🛠 Internals

  • add tests for loadableNode (c1b49a0)

Note: Version bump only for package @giraphql/plugin-dataloader

2.1.3 - 2021-05-12

🛠 Internals

  • add docs and tests for removing fields (a3aa90e)
  • udate dev deps (3251227)

Note: Version bump only for package @giraphql/plugin-dataloader

2.1.2 - 2021-05-10

🐞 Fixes

  • update ci build command (7e1d1d2)

Note: Version bump only for package @giraphql/plugin-dataloader

2.1.1 - 2021-05-10

🐞 Fixes

  • force new version to fix esm build issue (25f1fd2)

Note: Version bump only for package @giraphql/plugin-dataloader

2.1.0 - 2021-05-10

🚀 Updates

  • add esm build for all packages (d8bbdc9)

📘 Docs

  • add docs on adding dataloader options (cdf096a)
  • fix a couple issues in dataloader docs (10f0a6c)

Note: Version bump only for package @giraphql/plugin-dataloader

2.0.0 - 2021-05-09

📘 Docs

Note: Version bump only for package @giraphql/plugin-dataloader

2.0.0-alpha.2 - 2021-05-08

🚀 Updates

  • add deno support for dataloader plugin (720ba01)

Note: Version bump only for package @giraphql/plugin-dataloader

2.0.0-alpha.1 - 2021-05-08

🚀 Updates

  • add dataloader plugin (2e2403a)
  • support more dataloader flows and add tests (adf9408)

🐞 Fixes

  • rename duplicate field in example (8c55d1f)
  • update snapshots with new test fields (a7cc628)

Note: Version bump only for package @giraphql/plugin-dataloader