diff --git a/.changeset/slimy-brooms-hunt.md b/.changeset/slimy-brooms-hunt.md new file mode 100644 index 000000000..1b26f2926 --- /dev/null +++ b/.changeset/slimy-brooms-hunt.md @@ -0,0 +1,7 @@ +--- +'@pothos/plugin-dataloader': minor +'@pothos/plugin-relay': minor +'@pothos/deno': minor +--- + +Support parsing globalIDs for loadableNode diff --git a/packages/deno/packages/plugin-dataloader/global-types.ts b/packages/deno/packages/plugin-dataloader/global-types.ts index 79ad00153..75b73e211 100644 --- a/packages/deno/packages/plugin-dataloader/global-types.ts +++ b/packages/deno/packages/plugin-dataloader/global-types.ts @@ -16,9 +16,9 @@ declare global { loadableInterface: ? ShapeFromTypeParam : object, Key extends bigint | number | string, Interfaces extends InterfaceParam[], NameOrRef extends InterfaceParam | string, CacheKey = Key>(nameOrRef: NameOrRef, options: LoadableInterfaceOptions) => LoadableInterfaceRef; loadableObjectRef: (name: string, options: DataLoaderOptions) => ImplementableLoadableObjectRef; loadableInterfaceRef: (name: string, options: DataLoaderOptions) => ImplementableLoadableInterfaceRef; - loadableNodeRef: (name: string, options: DataLoaderOptions & LoadableNodeId) => ImplementableLoadableNodeRef; + loadableNodeRef: (name: string, options: DataLoaderOptions & LoadableNodeId) => ImplementableLoadableNodeRef; loadableUnion: , CacheKey = Key, Shape = ShapeFromTypeParam>(name: string, options: LoadableUnionOptions) => LoadableUnionRef; - loadableNode: "relay" extends PluginName ? ? ShapeFromTypeParam : object, Key extends bigint | number | string, Interfaces extends InterfaceParam[], NameOrRef extends ObjectParam | string, CacheKey = Key>(nameOrRef: NameOrRef, options: LoadableNodeOptions) => Omit, "implement"> : "@pothos/plugin-relay is required to use this method"; + loadableNode: "relay" extends PluginName ? ? ShapeFromTypeParam : object, Interfaces extends InterfaceParam[], NameOrRef extends ObjectParam | string, IDShape extends bigint | number | string = string, Key extends bigint | number | string = IDShape, CacheKey = Key>(nameOrRef: NameOrRef, options: LoadableNodeOptions) => Omit, "implement"> : "@pothos/plugin-relay is required to use this method"; } export interface RootFieldBuilder { loadable: , Key, CacheKey, ResolveReturnShape, Nullable extends FieldNullability = Types["DefaultFieldNullability"]>(options: LoadableFieldOptions) => FieldRef; diff --git a/packages/deno/packages/plugin-dataloader/refs/node.ts b/packages/deno/packages/plugin-dataloader/refs/node.ts index dfbfbe841..b1678342b 100644 --- a/packages/deno/packages/plugin-dataloader/refs/node.ts +++ b/packages/deno/packages/plugin-dataloader/refs/node.ts @@ -3,11 +3,13 @@ import { GraphQLResolveInfo } from 'https://cdn.skypack.dev/graphql?dts'; import { FieldRef, InterfaceRef, PothosObjectTypeConfig, SchemaTypes } from '../../core/index.ts'; import { DataLoaderOptions, LoadableNodeId } from '../types.ts'; import { ImplementableLoadableObjectRef } from './object.ts'; -export class ImplementableLoadableNodeRef extends ImplementableLoadableObjectRef { +export class ImplementableLoadableNodeRef extends ImplementableLoadableObjectRef { + parseId: ((id: string, ctx: object) => IDShape) | undefined; private idOptions; - constructor(builder: PothosSchemaTypes.SchemaBuilder, name: string, { id, ...options }: DataLoaderOptions & LoadableNodeId) { + constructor(builder: PothosSchemaTypes.SchemaBuilder, name: string, { id, ...options }: DataLoaderOptions & LoadableNodeId) { super(builder, name, options); this.idOptions = id; + this.parseId = id.parse; this.builder.configStore.onTypeConfig(this, (config) => { const nodeInterface = (this.builder as PothosSchemaTypes.SchemaBuilder & { nodeInterfaceRef: () => InterfaceRef; diff --git a/packages/deno/packages/plugin-dataloader/schema-builder.ts b/packages/deno/packages/plugin-dataloader/schema-builder.ts index 4c1a25aaa..fe34a1233 100644 --- a/packages/deno/packages/plugin-dataloader/schema-builder.ts +++ b/packages/deno/packages/plugin-dataloader/schema-builder.ts @@ -62,7 +62,7 @@ schemaBuilderProto.loadableUnion = function loadableUnion ? ShapeFromTypeParam : object, Key extends DataloaderKey, Interfaces extends InterfaceParam[], NameOrRef extends ObjectParam | string, CacheKey = Key>(this: PothosSchemaTypes.SchemaBuilder, nameOrRef: NameOrRef, options: LoadableNodeOptions) { +schemaBuilderProto.loadableNode = function loadableNode ? ShapeFromTypeParam : object, Interfaces extends InterfaceParam[], NameOrRef extends ObjectParam | string, IDShape extends bigint | number | string = string, Key extends bigint | number | string = IDShape, CacheKey = Key>(this: PothosSchemaTypes.SchemaBuilder, nameOrRef: NameOrRef, options: LoadableNodeOptions) { if (typeof (this as PothosSchemaTypes.SchemaBuilder & Record) .nodeInterfaceRef !== "function") { throw new PothosSchemaError("builder.loadableNode requires @pothos/plugin-relay to be installed"); @@ -74,9 +74,13 @@ schemaBuilderProto.loadableNode = function loadableNode(this, name, options); + const ref = new ImplementableLoadableNodeRef(this, name, options); ref.implement({ ...options, + extensions: { + ...options.extensions, + pothosParseGlobalID: options.id.parse, + }, isTypeOf: options.isTypeOf ?? (typeof nameOrRef === "function" ? (maybeNode: unknown, context: object, info: GraphQLResolveInfo) => { diff --git a/packages/deno/packages/plugin-dataloader/types.ts b/packages/deno/packages/plugin-dataloader/types.ts index f99dcc4c1..40d585717 100644 --- a/packages/deno/packages/plugin-dataloader/types.ts +++ b/packages/deno/packages/plugin-dataloader/types.ts @@ -50,7 +50,9 @@ export type LoaderShapeFromType { getDataloader: (context: C) => DataLoader; } -export interface LoadableNodeId { - id: Omit>>, "args" | "nullable" | "type">; +export interface LoadableNodeId { + id: Omit>>, "args" | "nullable" | "type"> & { + parse?: (id: string, ctx: Types["Context"]) => IDShape; + }; } -export type LoadableNodeOptions[], NameOrRef extends ObjectParam | string, CacheKey> = DataloaderObjectTypeOptions & LoadableNodeId; +export type LoadableNodeOptions[], NameOrRef extends ObjectParam | string, IDShape extends bigint | number | string = string, Key extends bigint | number | string = IDShape, CacheKey = Key> = DataloaderObjectTypeOptions & LoadableNodeId; diff --git a/packages/deno/packages/plugin-relay/global-types.ts b/packages/deno/packages/plugin-relay/global-types.ts index 55adf620a..2c00a4b7d 100644 --- a/packages/deno/packages/plugin-relay/global-types.ts +++ b/packages/deno/packages/plugin-relay/global-types.ts @@ -68,7 +68,9 @@ declare global { }; globalID: >(...args: NormalizeArgs<[ options: GlobalIDInputFieldOptions - ]>) => InputFieldRef ? T : string>, Req>, Kind>; + ]>) => InputFieldRef infer T; + } ? T : string>, Req>, Kind>; globalIDList: , For extends ObjectParam>(...args: NormalizeArgs<[ @@ -77,7 +79,9 @@ declare global { { [inputShapeKey]: { typename: string; - id: For extends NodeRef ? T : string; + id: For extends { + parseId?: (...args: any[]) => infer T; + } ? T : string; }; } ], Req>, Kind>; diff --git a/packages/deno/packages/plugin-relay/input-field-builder.ts b/packages/deno/packages/plugin-relay/input-field-builder.ts index 3132ff5a9..411f66616 100644 --- a/packages/deno/packages/plugin-relay/input-field-builder.ts +++ b/packages/deno/packages/plugin-relay/input-field-builder.ts @@ -1,6 +1,5 @@ // @ts-nocheck import { FieldRequiredness, InputFieldBuilder, InputFieldRef, InputShapeFromTypeParam, ObjectRef, SchemaTypes, } from '../core/index.ts'; -import { NodeRef } from './node-ref.ts'; import { GlobalIDInputFieldOptions, GlobalIDInputShape, GlobalIDListInputFieldOptions, } from './types.ts'; type DefaultSchemaTypes = PothosSchemaTypes.ExtendDefaultTypes<{}>; const inputFieldBuilder = InputFieldBuilder.prototype as PothosSchemaTypes.InputFieldBuilder; @@ -15,7 +14,7 @@ inputFieldBuilder.globalIDList = function globalIDList[])?.map((type: ObjectRef) => ({ typename: this.builder.configStore.getTypeConfig(type).name, - parseId: type instanceof NodeRef ? type.parseId : undefined, + parseId: "parseId" in type ? type.parseId : undefined, })) ?? null, }, }) as never; @@ -29,7 +28,7 @@ inputFieldBuilder.globalID = function globalID({ for: forTy relayGlobalIDFor: ((forTypes && (Array.isArray(forTypes) ? forTypes : [forTypes])) as ObjectRef[])?.map((type: ObjectRef) => ({ typename: this.builder.configStore.getTypeConfig(type).name, - parseId: type instanceof NodeRef ? type.parseId : undefined, + parseId: "parseId" in type ? type.parseId : undefined, })) ?? null, }, }) as unknown as InputFieldRef> as never; diff --git a/packages/plugin-dataloader/src/global-types.ts b/packages/plugin-dataloader/src/global-types.ts index 3b912e897..118b29b98 100644 --- a/packages/plugin-dataloader/src/global-types.ts +++ b/packages/plugin-dataloader/src/global-types.ts @@ -81,10 +81,16 @@ declare global { options: DataLoaderOptions, ) => ImplementableLoadableInterfaceRef; - loadableNodeRef: ( + loadableNodeRef: < + Shape extends object, + IDShape extends bigint | number | string = string, + Key extends bigint | number | string = IDShape, + CacheKey = Key, + >( name: string, - options: DataLoaderOptions & LoadableNodeId, - ) => ImplementableLoadableNodeRef; + options: DataLoaderOptions & + LoadableNodeId, + ) => ImplementableLoadableNodeRef; loadableUnion: < Key extends bigint | number | string, @@ -101,14 +107,26 @@ declare global { Shape extends NameOrRef extends ObjectParam ? ShapeFromTypeParam : object, - Key extends bigint | number | string, Interfaces extends InterfaceParam[], NameOrRef extends ObjectParam | string, + IDShape extends bigint | number | string = string, + Key extends bigint | number | string = IDShape, CacheKey = Key, >( nameOrRef: NameOrRef, - options: LoadableNodeOptions, - ) => Omit, 'implement'> + options: LoadableNodeOptions< + Types, + Shape, + Interfaces, + NameOrRef, + IDShape, + Key, + CacheKey + >, + ) => Omit< + ImplementableLoadableNodeRef, + 'implement' + > : '@pothos/plugin-relay is required to use this method'; } diff --git a/packages/plugin-dataloader/src/refs/node.ts b/packages/plugin-dataloader/src/refs/node.ts index 9656f6245..67a675f37 100644 --- a/packages/plugin-dataloader/src/refs/node.ts +++ b/packages/plugin-dataloader/src/refs/node.ts @@ -7,9 +7,11 @@ export class ImplementableLoadableNodeRef< Types extends SchemaTypes, RefShape, Shape extends object, - Key extends bigint | number | string, - CacheKey, + IDShape extends bigint | number | string = string, + Key extends bigint | number | string = IDShape, + CacheKey = Key, > extends ImplementableLoadableObjectRef { + parseId: ((id: string, ctx: object) => IDShape) | undefined; private idOptions; constructor( @@ -18,10 +20,11 @@ export class ImplementableLoadableNodeRef< { id, ...options - }: DataLoaderOptions & LoadableNodeId, + }: DataLoaderOptions & LoadableNodeId, ) { super(builder, name, options); this.idOptions = id; + this.parseId = id.parse; this.builder.configStore.onTypeConfig(this, (config) => { const nodeInterface = ( diff --git a/packages/plugin-dataloader/src/schema-builder.ts b/packages/plugin-dataloader/src/schema-builder.ts index 26fabe7ec..23c711313 100644 --- a/packages/plugin-dataloader/src/schema-builder.ts +++ b/packages/plugin-dataloader/src/schema-builder.ts @@ -132,14 +132,15 @@ schemaBuilderProto.loadableNode = function loadableNode< Shape extends NameOrRef extends ObjectParam ? ShapeFromTypeParam : object, - Key extends DataloaderKey, Interfaces extends InterfaceParam[], NameOrRef extends ObjectParam | string, + IDShape extends bigint | number | string = string, + Key extends bigint | number | string = IDShape, CacheKey = Key, >( this: PothosSchemaTypes.SchemaBuilder, nameOrRef: NameOrRef, - options: LoadableNodeOptions, + options: LoadableNodeOptions, ) { if ( typeof (this as PothosSchemaTypes.SchemaBuilder & Record) @@ -155,7 +156,7 @@ schemaBuilderProto.loadableNode = function loadableNode< ? nameOrRef : (options as { name?: string }).name ?? (nameOrRef as { name: string }).name; - const ref = new ImplementableLoadableNodeRef( + const ref = new ImplementableLoadableNodeRef( this, name, options, @@ -163,6 +164,10 @@ schemaBuilderProto.loadableNode = function loadableNode< ref.implement({ ...options, + extensions: { + ...options.extensions, + pothosParseGlobalID: options.id.parse, + }, isTypeOf: options.isTypeOf ?? (typeof nameOrRef === 'function' diff --git a/packages/plugin-dataloader/src/types.ts b/packages/plugin-dataloader/src/types.ts index 12a9f15a6..744be6042 100644 --- a/packages/plugin-dataloader/src/types.ts +++ b/packages/plugin-dataloader/src/types.ts @@ -162,7 +162,7 @@ export interface LoadableRef { getDataloader: (context: C) => DataLoader; } -export interface LoadableNodeId { +export interface LoadableNodeId { id: Omit< FieldOptionsFromKind< Types, @@ -175,15 +175,18 @@ export interface LoadableNodeId MaybePromise> >, 'args' | 'nullable' | 'type' - >; + > & { + parse?: (id: string, ctx: Types['Context']) => IDShape; + }; } export type LoadableNodeOptions< Types extends SchemaTypes, Shape extends object, - Key extends bigint | number | string, Interfaces extends InterfaceParam[], NameOrRef extends ObjectParam | string, - CacheKey, + IDShape extends bigint | number | string = string, + Key extends bigint | number | string = IDShape, + CacheKey = Key, > = DataloaderObjectTypeOptions & - LoadableNodeId; + LoadableNodeId; diff --git a/packages/plugin-dataloader/tests/__snapshots__/index.test.ts.snap b/packages/plugin-dataloader/tests/__snapshots__/index.test.ts.snap index 613042543..95502cb9b 100644 --- a/packages/plugin-dataloader/tests/__snapshots__/index.test.ts.snap +++ b/packages/plugin-dataloader/tests/__snapshots__/index.test.ts.snap @@ -34,6 +34,11 @@ interface Error { message: String! } +type LoadableParseTest implements Node { + id: ID! + idNumber: Int! +} + interface Node { id: ID! } @@ -72,6 +77,8 @@ type Query { fromContext3: User! fromContext4: [User!]! fromContext5: [User!]! + loadableParse: LoadableParseTest! + loadableParseNodes(ids: [ID!]): [LoadableParseTest!]! node(id: ID!): Node nodes(ids: [ID!]!): [Node]! nullableUser(id: String): NullableUser @@ -255,6 +262,31 @@ exports[`dataloader > queries > query with errors 1`] = ` } `; +exports[`dataloader > queries > query with global ids 1`] = ` +{ + "data": { + "loadableParse": { + "id": "TG9hZGFibGVQYXJzZVRlc3Q6MQ==", + "idNumber": 1, + }, + "loadableParseNodes": [ + { + "id": "TG9hZGFibGVQYXJzZVRlc3Q6MQ==", + "idNumber": 1, + }, + { + "id": "TG9hZGFibGVQYXJzZVRlc3Q6Mg==", + "idNumber": 2, + }, + { + "id": "TG9hZGFibGVQYXJzZVRlc3Q6MTA=", + "idNumber": 10, + }, + ], + }, +} +`; + exports[`dataloader > queries > sorts loaded results 1`] = ` { "counts": [ diff --git a/packages/plugin-dataloader/tests/example/schema/nodes.ts b/packages/plugin-dataloader/tests/example/schema/nodes.ts index a2a2374d5..aafbc4c92 100644 --- a/packages/plugin-dataloader/tests/example/schema/nodes.ts +++ b/packages/plugin-dataloader/tests/example/schema/nodes.ts @@ -37,14 +37,39 @@ const ClassThingRef = builder.loadableNode(ClassThing, { interfaces: [TestInterface], id: { resolve: (user) => user.id, + parse: (id) => id, }, loaderOptions: { maxBatchSize: 20 }, // eslint-disable-next-line @typescript-eslint/require-await - load: async (keys: string[], context: ContextType) => + load: async (keys, context: ContextType) => keys.map((k) => new ClassThing(Number.parseInt(k, 10))), fields: (t) => ({}), }); +class LoadableParseTest { + readonly id: number; + constructor(id: number) { + if (typeof id !== 'number') { + throw new TypeError(`Expected id to be a number, saw ${id}`); + } + this.id = id; + } +} + +const LoadableParseRef = builder.loadableNode(LoadableParseTest, { + name: 'LoadableParseTest', + id: { + resolve: (user) => user.id, + parse: (id) => Number.parseInt(id, 10), + }, + loaderOptions: { maxBatchSize: 20 }, + // eslint-disable-next-line @typescript-eslint/require-await + load: async (keys, context: ContextType) => keys.map((k) => new LoadableParseTest(k)), + fields: (t) => ({ + idNumber: t.exposeInt('id'), + }), +}); + builder.queryFields((t) => ({ userNode: t.field({ type: UserNode, @@ -73,4 +98,15 @@ builder.queryFields((t) => ({ type: ClassThingRef, resolve: () => '1', }), + loadableParse: t.field({ + type: LoadableParseRef, + resolve: () => 1, + }), + loadableParseNodes: t.field({ + type: [LoadableParseRef], + args: { + ids: t.arg.globalIDList({ for: [LoadableParseRef] }), + }, + resolve: (source, args) => args.ids?.map((id) => id.id) ?? [], + }), })); diff --git a/packages/plugin-dataloader/tests/index.test.ts b/packages/plugin-dataloader/tests/index.test.ts index a0ccb6ea7..ee06d3f0f 100644 --- a/packages/plugin-dataloader/tests/index.test.ts +++ b/packages/plugin-dataloader/tests/index.test.ts @@ -174,6 +174,35 @@ describe('dataloader', () => { expect(result).toMatchSnapshot(); }); + it('query with global ids', async () => { + const query = gql` + query { + loadableParse { + id + idNumber + } + loadableParseNodes( + ids: [ + "TG9hZGFibGVQYXJzZVRlc3Q6MQ==" + "TG9hZGFibGVQYXJzZVRlc3Q6Mg==" + "TG9hZGFibGVQYXJzZVRlc3Q6MTA=" + ] + ) { + id + idNumber + } + } + `; + + const result = await execute({ + schema, + document: query, + contextValue: createContext(), + }); + + expect(result).toMatchSnapshot(); + }); + it('query with errors', async () => { const query = gql` query { diff --git a/packages/plugin-relay/src/global-types.ts b/packages/plugin-relay/src/global-types.ts index 17386f2cd..f9a41f168 100644 --- a/packages/plugin-relay/src/global-types.ts +++ b/packages/plugin-relay/src/global-types.ts @@ -198,7 +198,7 @@ declare global { ) => InputFieldRef< InputShapeFromTypeParam< Types, - GlobalIDInputShape ? T : string>, + GlobalIDInputShape infer T } ? T : string>, Req >, Kind @@ -213,7 +213,7 @@ declare global { { [inputShapeKey]: { typename: string; - id: For extends NodeRef ? T : string; + id: For extends { parseId?: (...args: any[]) => infer T } ? T : string; }; }, ], diff --git a/packages/plugin-relay/src/input-field-builder.ts b/packages/plugin-relay/src/input-field-builder.ts index ec1d47200..6c768168f 100644 --- a/packages/plugin-relay/src/input-field-builder.ts +++ b/packages/plugin-relay/src/input-field-builder.ts @@ -6,7 +6,6 @@ import { ObjectRef, SchemaTypes, } from '@pothos/core'; -import { NodeRef } from './node-ref'; import { GlobalIDInputFieldOptions, GlobalIDInputShape, @@ -37,7 +36,7 @@ inputFieldBuilder.globalIDList = function globalIDList[] )?.map((type: ObjectRef) => ({ typename: this.builder.configStore.getTypeConfig(type).name, - parseId: type instanceof NodeRef ? type.parseId : undefined, + parseId: 'parseId' in type ? type.parseId : undefined, })) ?? null, }, }) as never; @@ -60,7 +59,7 @@ inputFieldBuilder.globalID = function globalID( (Array.isArray(forTypes) ? forTypes : [forTypes])) as ObjectRef[] )?.map((type: ObjectRef) => ({ typename: this.builder.configStore.getTypeConfig(type).name, - parseId: type instanceof NodeRef ? type.parseId : undefined, + parseId: 'parseId' in type ? type.parseId : undefined, })) ?? null, }, }) as unknown as InputFieldRef<