diff --git a/.changeset/eight-ligers-develop.md b/.changeset/eight-ligers-develop.md new file mode 100644 index 000000000..38bea0586 --- /dev/null +++ b/.changeset/eight-ligers-develop.md @@ -0,0 +1,6 @@ +--- +'@pothos/plugin-relay': patch +'@pothos/deno': patch +--- + +Fix a few issues with globalID parsing diff --git a/packages/deno/packages/plugin-relay/field-builder.ts b/packages/deno/packages/plugin-relay/field-builder.ts index 686e8bf04..d8ec8c8f3 100644 --- a/packages/deno/packages/plugin-relay/field-builder.ts +++ b/packages/deno/packages/plugin-relay/field-builder.ts @@ -55,7 +55,7 @@ fieldBuilderProto.node = function node({ id, ...options }) { const globalID = typeof rawID === "string" ? internalDecodeGlobalID(this.builder, rawID, context, info, true) : rawID && { - id: String(rawID.id), + id: rawID.id, typename: this.builder.configStore.getTypeConfig(rawID.type).name, }; return (await resolveNodes(this.builder, context, info, [globalID]))[0]; @@ -80,7 +80,7 @@ fieldBuilderProto.nodeList = function nodeList({ ids, ...options }) { const globalIds = rawIds.map((id) => typeof id === "string" ? internalDecodeGlobalID(this.builder, id, context, info, true) : id && { - id: String(id.id), + id: id.id, typename: this.builder.configStore.getTypeConfig(id.type).name, }); return resolveNodes(this.builder, context, info, globalIds); diff --git a/packages/deno/packages/plugin-relay/index.ts b/packages/deno/packages/plugin-relay/index.ts index 577b60483..19b501c50 100644 --- a/packages/deno/packages/plugin-relay/index.ts +++ b/packages/deno/packages/plugin-relay/index.ts @@ -15,7 +15,9 @@ export class PothosRelayPlugin extends BasePlugin, fieldConfig: PothosOutputFieldConfig): GraphQLFieldResolver { const argMappings = mapInputFields(fieldConfig.args, this.buildCache, (inputField) => { if (inputField.extensions?.isRelayGlobalID) { - return (inputField.extensions?.relayGlobalIDFor ?? true) as true | { + return (inputField.extensions?.relayGlobalIDFor ?? + inputField.extensions?.relayGlobalIDAlwaysParse ?? + false) as boolean | { typename: string; parseId: (id: string, ctx: object) => unknown; }[]; @@ -25,7 +27,7 @@ export class PothosRelayPlugin extends BasePlugin internalDecodeGlobalID(this.builder, String(globalID), ctx, info, Array.isArray(mappings.value) ? mappings.value : false)); + const argMapper = createInputValueMapper(argMappings, (globalID, mappings, ctx: Types["Context"], info: GraphQLResolveInfo) => internalDecodeGlobalID(this.builder, String(globalID), ctx, info, mappings.value ?? false)); return (parent, args, context, info) => resolver(parent, argMapper(args, undefined, context, info), context, info); } override wrapSubscribe(subscribe: GraphQLFieldResolver | undefined, fieldConfig: PothosOutputFieldConfig): GraphQLFieldResolver | undefined { diff --git a/packages/deno/packages/plugin-relay/input-field-builder.ts b/packages/deno/packages/plugin-relay/input-field-builder.ts index 57e36103e..3132ff5a9 100644 --- a/packages/deno/packages/plugin-relay/input-field-builder.ts +++ b/packages/deno/packages/plugin-relay/input-field-builder.ts @@ -15,7 +15,7 @@ inputFieldBuilder.globalIDList = function globalIDList[])?.map((type: ObjectRef) => ({ typename: this.builder.configStore.getTypeConfig(type).name, - parse: type instanceof NodeRef ? type.parseId : undefined, + parseId: type instanceof NodeRef ? type.parseId : undefined, })) ?? null, }, }) as never; @@ -29,7 +29,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, - parse: type instanceof NodeRef ? type.parseId : undefined, + parseId: type instanceof NodeRef ? type.parseId : undefined, })) ?? null, }, }) as unknown as InputFieldRef> as never; diff --git a/packages/deno/packages/plugin-relay/schema-builder.ts b/packages/deno/packages/plugin-relay/schema-builder.ts index 57230920b..4d0aa554b 100644 --- a/packages/deno/packages/plugin-relay/schema-builder.ts +++ b/packages/deno/packages/plugin-relay/schema-builder.ts @@ -101,7 +101,12 @@ schemaBuilderProto.nodeInterfaceRef = function nodeInterfaceRef() { ...this.options.relayOptions.nodeQueryOptions, type: ref as InterfaceRef, args: { - id: t.arg.globalID({ required: true }), + id: t.arg.globalID({ + required: true, + extensions: { + relayGlobalIDAlwaysParse: true, + }, + }), }, resolve: resolveNodeFn ? (root, args, context, info) => resolveNodeFn(root, args as { @@ -134,7 +139,12 @@ schemaBuilderProto.nodeInterfaceRef = function nodeInterfaceRef() { ...this.options.relayOptions.nodesQueryOptions, type: [ref], args: { - ids: t.arg.globalIDList({ required: true }), + ids: t.arg.globalIDList({ + required: true, + extensions: { + relayGlobalIDAlwaysParse: true, + }, + }), }, resolve: resolveNodesFn ? (root, args, context, info) => resolveNodesFn(root, args as { diff --git a/packages/plugin-relay/src/field-builder.ts b/packages/plugin-relay/src/field-builder.ts index 0480eac8f..23f8b314d 100644 --- a/packages/plugin-relay/src/field-builder.ts +++ b/packages/plugin-relay/src/field-builder.ts @@ -128,7 +128,7 @@ fieldBuilderProto.node = function node({ id, ...options }) { typeof rawID === 'string' ? internalDecodeGlobalID(this.builder, rawID, context, info, true) : rawID && { - id: String(rawID.id), + id: rawID.id, typename: this.builder.configStore.getTypeConfig(rawID.type).name, }; @@ -165,7 +165,7 @@ fieldBuilderProto.nodeList = function nodeList({ ids, ...options }) { typeof id === 'string' ? internalDecodeGlobalID(this.builder, id, context, info, true) : id && { - id: String(id.id), + id: id.id, typename: this.builder.configStore.getTypeConfig(id.type).name, }, ); diff --git a/packages/plugin-relay/src/index.ts b/packages/plugin-relay/src/index.ts index fbfc23c21..245863b7c 100644 --- a/packages/plugin-relay/src/index.ts +++ b/packages/plugin-relay/src/index.ts @@ -27,9 +27,9 @@ export class PothosRelayPlugin extends BasePlugin { const argMappings = mapInputFields(fieldConfig.args, this.buildCache, (inputField) => { if (inputField.extensions?.isRelayGlobalID) { - return (inputField.extensions?.relayGlobalIDFor ?? true) as - | true - | { typename: string; parseId: (id: string, ctx: object) => unknown }[]; + return (inputField.extensions?.relayGlobalIDFor ?? + inputField.extensions?.relayGlobalIDAlwaysParse ?? + false) as boolean | { typename: string; parseId: (id: string, ctx: object) => unknown }[]; } return null; @@ -42,13 +42,7 @@ export class PothosRelayPlugin extends BasePlugin - internalDecodeGlobalID( - this.builder, - String(globalID), - ctx, - info, - Array.isArray(mappings.value) ? mappings.value : false, - ), + internalDecodeGlobalID(this.builder, String(globalID), ctx, info, mappings.value ?? false), ); return (parent, args, context, info) => diff --git a/packages/plugin-relay/src/input-field-builder.ts b/packages/plugin-relay/src/input-field-builder.ts index b3906bbe2..ec1d47200 100644 --- a/packages/plugin-relay/src/input-field-builder.ts +++ b/packages/plugin-relay/src/input-field-builder.ts @@ -37,7 +37,7 @@ inputFieldBuilder.globalIDList = function globalIDList[] )?.map((type: ObjectRef) => ({ typename: this.builder.configStore.getTypeConfig(type).name, - parse: type instanceof NodeRef ? type.parseId : undefined, + parseId: type instanceof NodeRef ? type.parseId : undefined, })) ?? null, }, }) as never; @@ -60,7 +60,7 @@ inputFieldBuilder.globalID = function globalID( (Array.isArray(forTypes) ? forTypes : [forTypes])) as ObjectRef[] )?.map((type: ObjectRef) => ({ typename: this.builder.configStore.getTypeConfig(type).name, - parse: type instanceof NodeRef ? type.parseId : undefined, + parseId: type instanceof NodeRef ? type.parseId : undefined, })) ?? null, }, }) as unknown as InputFieldRef< diff --git a/packages/plugin-relay/src/schema-builder.ts b/packages/plugin-relay/src/schema-builder.ts index 0d2f656f4..b7f06e831 100644 --- a/packages/plugin-relay/src/schema-builder.ts +++ b/packages/plugin-relay/src/schema-builder.ts @@ -158,7 +158,12 @@ schemaBuilderProto.nodeInterfaceRef = function nodeInterfaceRef() { ...this.options.relayOptions.nodeQueryOptions, type: ref as InterfaceRef, args: { - id: t.arg.globalID({ required: true }), + id: t.arg.globalID({ + required: true, + extensions: { + relayGlobalIDAlwaysParse: true, + }, + }), }, resolve: resolveNodeFn ? (root, args, context, info) => @@ -198,7 +203,12 @@ schemaBuilderProto.nodeInterfaceRef = function nodeInterfaceRef() { ...this.options.relayOptions.nodesQueryOptions, type: [ref], args: { - ids: t.arg.globalIDList({ required: true }), + ids: t.arg.globalIDList({ + required: true, + extensions: { + relayGlobalIDAlwaysParse: true, + }, + }), }, resolve: resolveNodesFn ? (root, args, context, info) => diff --git a/packages/plugin-relay/tests/__snapshots__/index.test.ts.snap b/packages/plugin-relay/tests/__snapshots__/index.test.ts.snap index b49891206..b49a781a9 100644 --- a/packages/plugin-relay/tests/__snapshots__/index.test.ts.snap +++ b/packages/plugin-relay/tests/__snapshots__/index.test.ts.snap @@ -46,6 +46,13 @@ input GlobalIDInput { otherList: [OtherInput!] = [{someField: \\"abc\\"}] } +type IDResult { + arg: String! + id: String! + idType: String! + typename: String! +} + type IDWithColon implements Node { id: ID! idString: String! @@ -133,6 +140,7 @@ type PollAnswersUsingOffsetConnectionEdge { type Query { batchNumbers(after: ID, before: ID, first: Int, last: Int): QueryBatchNumbersConnection! cursorConnection(after: ID, before: ID, first: Int, last: Int): QueryCursorConnection! + echoIDs(genericNumberThingID: ID!, globalID: ID!, numberThingID: ID!): [IDResult!]! extraNode: Node idWithColon(id: ID!): IDWithColon! idsWithColon(ids: [ID!]!): [IDWithColon!]! diff --git a/packages/plugin-relay/tests/examples/global-connection-fields/schema/numbers.ts b/packages/plugin-relay/tests/examples/global-connection-fields/schema/numbers.ts index bc838d94e..12618e4f3 100644 --- a/packages/plugin-relay/tests/examples/global-connection-fields/schema/numbers.ts +++ b/packages/plugin-relay/tests/examples/global-connection-fields/schema/numbers.ts @@ -5,6 +5,9 @@ class NumberThing { id: number; constructor(n: number) { + if (typeof n !== 'number') { + throw new TypeError(`Expected NumberThing to receive number, saw ${typeof n} ${n}`); + } this.id = n; } } @@ -13,6 +16,11 @@ class BatchLoadableNumberThing { id: number; constructor(n: number) { + if (typeof n !== 'number') { + throw new TypeError( + `Expected BatchLoadableNumberThing to receive number, saw ${typeof n} ${n}`, + ); + } this.id = n; } } diff --git a/packages/plugin-relay/tests/examples/relay/schema/numbers.ts b/packages/plugin-relay/tests/examples/relay/schema/numbers.ts index d7ec4894e..348758ea5 100644 --- a/packages/plugin-relay/tests/examples/relay/schema/numbers.ts +++ b/packages/plugin-relay/tests/examples/relay/schema/numbers.ts @@ -16,6 +16,9 @@ class NumberThing { id: number; constructor(n: number) { + if (typeof n !== 'number') { + throw new TypeError(`Expected NumberThing to receive number, saw ${typeof n} ${n}`); + } this.id = n; } } @@ -24,6 +27,11 @@ class BatchLoadableNumberThing { id: number; constructor(n: number) { + if (typeof n !== 'number') { + throw new TypeError( + `Expected BatchLoadableNumberThing to receive number, saw ${typeof n} ${n}`, + ); + } this.id = n; } } @@ -269,3 +277,38 @@ builder.queryField('numberThingsByIDs', (t) => resolve: (root, args) => args.ids.map(({ id }) => new NumberThing(id)), }), ); + +const IDResult = builder + .objectRef<{ + id: unknown; + typename: string; + arg: string; + }>('IDResult') + .implement({ + fields: (t) => ({ + id: t.string({ + resolve: (n) => String(n.id), + }), + typename: t.exposeString('typename', {}), + arg: t.exposeString('arg', {}), + idType: t.string({ + resolve: (n) => typeof n.id, + }), + }), + }); + +builder.queryField('echoIDs', (t) => + t.field({ + type: [IDResult], + args: { + globalID: t.arg.globalID({ required: true }), + numberThingID: t.arg.globalID({ required: true, for: [NumberThingRef] }), + genericNumberThingID: t.arg.globalID({ required: true, for: [NumberThing] }), + }, + resolve: (_, args) => [ + { ...args.globalID, arg: 'globalID' }, + { ...args.numberThingID, arg: 'numberThingID' }, + { ...args.genericNumberThingID, arg: 'genericNumberThingID' }, + ], + }), +); diff --git a/packages/plugin-relay/tests/index.test.ts b/packages/plugin-relay/tests/index.test.ts index 0bc43dbbf..84015ed6c 100644 --- a/packages/plugin-relay/tests/index.test.ts +++ b/packages/plugin-relay/tests/index.test.ts @@ -537,6 +537,16 @@ describe('relay example schema', () => { id number } + echoIDs( + globalID: "TnVtYmVyOjE=" + numberThingID: "TnVtYmVyOjE=" + genericNumberThingID: "TnVtYmVyOjE=" + ) { + id + typename + arg + idType + } } `; @@ -549,6 +559,26 @@ describe('relay example schema', () => { expect(result).toMatchInlineSnapshot(` { "data": { + "echoIDs": [ + { + "arg": "globalID", + "id": "1", + "idType": "string", + "typename": "Number", + }, + { + "arg": "numberThingID", + "id": "1", + "idType": "number", + "typename": "Number", + }, + { + "arg": "genericNumberThingID", + "id": "1", + "idType": "string", + "typename": "Number", + }, + ], "idWithColon": { "id": "SURXaXRoQ29sb246MTp0ZXN0", "idString": "1:test",