diff --git a/.changeset/early-chefs-end.md b/.changeset/early-chefs-end.md new file mode 100644 index 000000000..0813bd450 --- /dev/null +++ b/.changeset/early-chefs-end.md @@ -0,0 +1,5 @@ +--- +'@pothos/plugin-relay': minor +--- + +handle string contining ':' in global ID diff --git a/packages/deno/packages/plugin-relay/utils/global-ids.ts b/packages/deno/packages/plugin-relay/utils/global-ids.ts index 43643d0fc..58c62755b 100644 --- a/packages/deno/packages/plugin-relay/utils/global-ids.ts +++ b/packages/deno/packages/plugin-relay/utils/global-ids.ts @@ -4,9 +4,10 @@ export function encodeGlobalID(typename: string, id: bigint | number | string) { return encodeBase64(`${typename}:${id}`); } export function decodeGlobalID(globalID: string) { - const [typename, id] = decodeBase64(globalID).split(":"); + const decoded = decodeBase64(globalID).split(":"); + const [typename, id] = decoded; if (!typename || !id) { throw new PothosValidationError(`Invalid global ID: ${globalID}`); } - return { typename, id }; + return { typename, id: decoded.length > 2 ? decoded.slice(1).join(":") : id }; } diff --git a/packages/plugin-relay/src/utils/global-ids.ts b/packages/plugin-relay/src/utils/global-ids.ts index 666e3a209..2ec02d9c1 100644 --- a/packages/plugin-relay/src/utils/global-ids.ts +++ b/packages/plugin-relay/src/utils/global-ids.ts @@ -5,11 +5,12 @@ export function encodeGlobalID(typename: string, id: bigint | number | string) { } export function decodeGlobalID(globalID: string) { - const [typename, id] = decodeBase64(globalID).split(':'); + const decoded = decodeBase64(globalID).split(':'); + const [typename, id] = decoded; if (!typename || !id) { throw new PothosValidationError(`Invalid global ID: ${globalID}`); } - return { typename, id }; + return { typename, id: decoded.length > 2 ? decoded.slice(1).join(':') : id }; } diff --git a/packages/plugin-relay/tests/__snapshots__/index.test.ts.snap b/packages/plugin-relay/tests/__snapshots__/index.test.ts.snap index 45ac44025..b49891206 100644 --- a/packages/plugin-relay/tests/__snapshots__/index.test.ts.snap +++ b/packages/plugin-relay/tests/__snapshots__/index.test.ts.snap @@ -46,6 +46,11 @@ input GlobalIDInput { otherList: [OtherInput!] = [{someField: \\"abc\\"}] } +type IDWithColon implements Node { + id: ID! + idString: String! +} + type Mutation { answerPoll(answer: Int!, id: ID!): Poll! createPoll(answers: [String!]!, question: String!): Poll! @@ -129,6 +134,8 @@ type Query { batchNumbers(after: ID, before: ID, first: Int, last: Int): QueryBatchNumbersConnection! cursorConnection(after: ID, before: ID, first: Int, last: Int): QueryCursorConnection! extraNode: Node + idWithColon(id: ID!): IDWithColon! + idsWithColon(ids: [ID!]!): [IDWithColon!]! inputGlobalID(id: ID!, inputObj: GlobalIDInput!, normalId: ID!): String! moreNodes: [Node]! node(id: ID!): Node diff --git a/packages/plugin-relay/tests/examples/relay/schema/numbers.ts b/packages/plugin-relay/tests/examples/relay/schema/numbers.ts index d80b301e0..d7ec4894e 100644 --- a/packages/plugin-relay/tests/examples/relay/schema/numbers.ts +++ b/packages/plugin-relay/tests/examples/relay/schema/numbers.ts @@ -1,6 +1,17 @@ import { resolveArrayConnection, resolveOffsetConnection } from '../../../../src'; import builder from '../builder'; +class IDWithColon { + id: string; + + constructor(id: string) { + if (!id.includes(':')) { + throw new TypeError(`Expected id to have a colon, saw ${id}`); + } + this.id = id; + } +} + class NumberThing { id: number; @@ -17,6 +28,16 @@ class BatchLoadableNumberThing { } } +const IDWithColonRef = builder.node(IDWithColon, { + name: 'IDWithColon', + id: { + resolve: (n) => n.id, + }, + fields: (t) => ({ + idString: t.exposeString('id'), + }), +}); + const NumberThingRef = builder.node(NumberThing, { id: { resolve: (n) => n.id, @@ -207,6 +228,26 @@ builder.queryField('sharedEdgeConnection', (t) => ), ); +builder.queryField('idWithColon', (t) => + t.field({ + type: IDWithColonRef, + args: { + id: t.arg.globalID({ required: true, for: [IDWithColonRef] }), + }, + resolve: (root, args) => new IDWithColon(args.id.id), + }), +); + +builder.queryField('idsWithColon', (t) => + t.field({ + type: [IDWithColonRef], + args: { + ids: t.arg.globalIDList({ required: true, for: [IDWithColonRef] }), + }, + resolve: (root, args) => args.ids.map((id) => new IDWithColon(id.id)), + }), +); + builder.queryField('numberThingByID', (t) => t.field({ type: NumberThing, diff --git a/packages/plugin-relay/tests/index.test.ts b/packages/plugin-relay/tests/index.test.ts index eb9f6322d..0bc43dbbf 100644 --- a/packages/plugin-relay/tests/index.test.ts +++ b/packages/plugin-relay/tests/index.test.ts @@ -513,6 +513,14 @@ describe('relay example schema', () => { it('parses ids', async () => { const query = gql` query { + idWithColon(id: "SURXaXRoQ29sb246MTp0ZXN0") { + id + idString + } + idsWithColon(ids: ["SURXaXRoQ29sb246MTp0ZXN0", "SURXaXRoQ29sb246Mjp0ZXN0OmV4YW1wbGU="]) { + id + idString + } numberThingByID(id: "TnVtYmVyOjE=") { id number @@ -541,6 +549,20 @@ describe('relay example schema', () => { expect(result).toMatchInlineSnapshot(` { "data": { + "idWithColon": { + "id": "SURXaXRoQ29sb246MTp0ZXN0", + "idString": "1:test", + }, + "idsWithColon": [ + { + "id": "SURXaXRoQ29sb246MTp0ZXN0", + "idString": "1:test", + }, + { + "id": "SURXaXRoQ29sb246Mjp0ZXN0OmV4YW1wbGU=", + "idString": "2:test:example", + }, + ], "invalid": null, "invalidList": null, "numberThingByID": {