diff --git a/.changeset/kind-snails-pull.md b/.changeset/kind-snails-pull.md new file mode 100644 index 000000000..e29620a57 --- /dev/null +++ b/.changeset/kind-snails-pull.md @@ -0,0 +1,6 @@ +--- +'@pothos/plugin-dataloader': minor +'@pothos/deno': minor +--- + +Add default isTypeOf for loadableNode diff --git a/packages/deno/packages/plugin-dataloader/README.md b/packages/deno/packages/plugin-dataloader/README.md index 64254bc85..c7836ee40 100644 --- a/packages/deno/packages/plugin-dataloader/README.md +++ b/packages/deno/packages/plugin-dataloader/README.md @@ -306,7 +306,7 @@ const UserNode = builder.loadableNode('UserNode', { id: { resolve: (user) => user.id, }, - // For loadable objects we always need to include an isTypeOf check + // For loadable nodes, we need to include an isTypeOf check if the first arg is a string isTypeOf: (obj) => obj instanceof User, load: (ids: string[], context: ContextType) => context.loadUsersById(ids), fields: (t) => ({}), diff --git a/packages/deno/packages/plugin-dataloader/schema-builder.ts b/packages/deno/packages/plugin-dataloader/schema-builder.ts index 77fb0b6b0..4c1a25aaa 100644 --- a/packages/deno/packages/plugin-dataloader/schema-builder.ts +++ b/packages/deno/packages/plugin-dataloader/schema-builder.ts @@ -1,5 +1,6 @@ // @ts-nocheck -import SchemaBuilder, { InterfaceParam, ObjectParam, PothosSchemaError, SchemaTypes, ShapeFromTypeParam, } from '../core/index.ts'; +import type { GraphQLResolveInfo } from 'https://cdn.skypack.dev/graphql?dts'; +import SchemaBuilder, { InterfaceParam, ObjectParam, OutputRef, PothosSchemaError, SchemaTypes, ShapeFromTypeParam, } from '../core/index.ts'; import { ImplementableLoadableNodeRef } from './refs/index.ts'; import { ImplementableLoadableInterfaceRef } from './refs/interface.ts'; import { ImplementableLoadableObjectRef } from './refs/object.ts'; @@ -74,7 +75,33 @@ schemaBuilderProto.loadableNode = function loadableNode(this, name, options); - ref.implement(options); + ref.implement({ + ...options, + isTypeOf: options.isTypeOf ?? + (typeof nameOrRef === "function" + ? (maybeNode: unknown, context: object, info: GraphQLResolveInfo) => { + if (!maybeNode) { + return false; + } + if (maybeNode instanceof (nameOrRef as Function)) { + return true; + } + const proto = Object.getPrototypeOf(maybeNode) as { + constructor: unknown; + }; + try { + if (proto?.constructor) { + const config = this.configStore.getTypeConfig(proto.constructor as OutputRef); + return config.name === name; + } + } + catch { + // ignore + } + return false; + } + : undefined), + }); if (typeof nameOrRef !== "string") { this.configStore.associateRefWithName(nameOrRef, name); } diff --git a/packages/plugin-dataloader/README.md b/packages/plugin-dataloader/README.md index 64254bc85..c7836ee40 100644 --- a/packages/plugin-dataloader/README.md +++ b/packages/plugin-dataloader/README.md @@ -306,7 +306,7 @@ const UserNode = builder.loadableNode('UserNode', { id: { resolve: (user) => user.id, }, - // For loadable objects we always need to include an isTypeOf check + // For loadable nodes, we need to include an isTypeOf check if the first arg is a string isTypeOf: (obj) => obj instanceof User, load: (ids: string[], context: ContextType) => context.loadUsersById(ids), fields: (t) => ({}), diff --git a/packages/plugin-dataloader/src/schema-builder.ts b/packages/plugin-dataloader/src/schema-builder.ts index e8c279cc7..26fabe7ec 100644 --- a/packages/plugin-dataloader/src/schema-builder.ts +++ b/packages/plugin-dataloader/src/schema-builder.ts @@ -1,6 +1,8 @@ +import type { GraphQLResolveInfo } from 'graphql'; import SchemaBuilder, { InterfaceParam, ObjectParam, + OutputRef, PothosSchemaError, SchemaTypes, ShapeFromTypeParam, @@ -159,7 +161,36 @@ schemaBuilderProto.loadableNode = function loadableNode< options, ); - ref.implement(options); + ref.implement({ + ...options, + isTypeOf: + options.isTypeOf ?? + (typeof nameOrRef === 'function' + ? (maybeNode: unknown, context: object, info: GraphQLResolveInfo) => { + if (!maybeNode) { + return false; + } + + if (maybeNode instanceof (nameOrRef as Function)) { + return true; + } + + const proto = Object.getPrototypeOf(maybeNode) as { constructor: unknown }; + + try { + if (proto?.constructor) { + const config = this.configStore.getTypeConfig(proto.constructor as OutputRef); + + return config.name === name; + } + } catch { + // ignore + } + + return false; + } + : undefined), + }); if (typeof nameOrRef !== 'string') { this.configStore.associateRefWithName(nameOrRef, name); diff --git a/packages/plugin-dataloader/tests/__snapshots__/index.test.ts.snap b/packages/plugin-dataloader/tests/__snapshots__/index.test.ts.snap index c2c45283f..613042543 100644 --- a/packages/plugin-dataloader/tests/__snapshots__/index.test.ts.snap +++ b/packages/plugin-dataloader/tests/__snapshots__/index.test.ts.snap @@ -319,8 +319,12 @@ exports[`dataloader > queries > valid queries 1`] = ` "classThing": { "id": "Q2xhc3NMb2FkYWJsZVRoaW5nOjEyMw==", }, + "classThingNode": { + "__typename": "ClassLoadableThing", + "id": "Q2xhc3NMb2FkYWJsZVRoaW5nOjE=", + }, "classThingRef": { - "id": "Q2xhc3NMb2FkYWJsZVRoaW5nOjEyMw==", + "id": "Q2xhc3NMb2FkYWJsZVRoaW5nOjE=", }, "counts": [ { diff --git a/packages/plugin-dataloader/tests/example/schema/nodes.ts b/packages/plugin-dataloader/tests/example/schema/nodes.ts index b842e230c..a2a2374d5 100644 --- a/packages/plugin-dataloader/tests/example/schema/nodes.ts +++ b/packages/plugin-dataloader/tests/example/schema/nodes.ts @@ -11,21 +11,25 @@ const UserNode = builder.loadableNodeRef('UserNode', { load: (keys: string[], context: ContextType) => { countCall(context, userNodeCounts, keys.length); return Promise.resolve( - keys.map((id) => (Number(id) > 0 ? { id: Number(id) } : new Error(`Invalid ID ${id}`))), + keys.map((id) => + Number(id) > 0 ? { objType: 'UserNode', id: Number(id) } : new Error(`Invalid ID ${id}`), + ), ); }, }); builder.objectType(UserNode, { interfaces: [TestInterface], - isTypeOf: (obj) => - typeof obj === 'object' && obj !== null && Object.prototype.hasOwnProperty.call(obj, 'id'), + isTypeOf: (obj) => (obj as any).objType === 'UserNode', fields: (t) => ({}), }); class ClassThing { - id: number = 123; + id: number; name: string = 'some name'; + constructor(id = 123) { + this.id = id; + } } const ClassThingRef = builder.loadableNode(ClassThing, { @@ -36,7 +40,8 @@ const ClassThingRef = builder.loadableNode(ClassThing, { }, loaderOptions: { maxBatchSize: 20 }, // eslint-disable-next-line @typescript-eslint/require-await - load: async (keys: string[], context: ContextType) => [new ClassThing()], + load: async (keys: string[], context: ContextType) => + keys.map((k) => new ClassThing(Number.parseInt(k, 10))), fields: (t) => ({}), }); diff --git a/packages/plugin-dataloader/tests/index.test.ts b/packages/plugin-dataloader/tests/index.test.ts index 68edc06cb..a0ccb6ea7 100644 --- a/packages/plugin-dataloader/tests/index.test.ts +++ b/packages/plugin-dataloader/tests/index.test.ts @@ -158,6 +158,10 @@ describe('dataloader', () => { threeToMany: oneToMany(id: 3) { id } + classThingNode: node(id: "Q2xhc3NMb2FkYWJsZVRoaW5nOjE=") { + __typename + id + } } `; diff --git a/website/pages/docs/plugins/dataloader.mdx b/website/pages/docs/plugins/dataloader.mdx index 3248fff9d..5a04bb01c 100644 --- a/website/pages/docs/plugins/dataloader.mdx +++ b/website/pages/docs/plugins/dataloader.mdx @@ -320,7 +320,7 @@ const UserNode = builder.loadableNode('UserNode', { id: { resolve: (user) => user.id, }, - // For loadable objects we always need to include an isTypeOf check + // For loadable nodes, we need to include an isTypeOf check if the first arg is a string isTypeOf: (obj) => obj instanceof User, load: (ids: string[], context: ContextType) => context.loadUsersById(ids), fields: (t) => ({}),