From 5ae586ee2cc34d5044492db19f9795b27279628e Mon Sep 17 00:00:00 2001 From: Dmitriy Lazarev Date: Sat, 16 Dec 2023 01:58:39 +0400 Subject: [PATCH 1/2] Resolve node to null if entity doesn't exist Signed-off-by: Dmitriy Lazarev --- .../src/relationDirectiveMapper.test.ts | 64 +++++++++++++++++++ .../src/relationDirectiveMapper.ts | 22 +++++-- 2 files changed, 80 insertions(+), 6 deletions(-) diff --git a/plugins/graphql-backend-module-catalog/src/relationDirectiveMapper.test.ts b/plugins/graphql-backend-module-catalog/src/relationDirectiveMapper.test.ts index 507e3f9b9d..8c38bcfc38 100644 --- a/plugins/graphql-backend-module-catalog/src/relationDirectiveMapper.test.ts +++ b/plugins/graphql-backend-module-catalog/src/relationDirectiveMapper.test.ts @@ -563,4 +563,68 @@ describe('mapRelationDirective', () => { }, }); }); + + it('should return null for not existing entities', async () => { + const TestModule = createModule({ + id: 'test', + typeDefs: gql` + extend interface Node @discriminates(with: "kind") + + type Entity @implements(interface: "Node") { + ownedBy: User @relation + owner: User @relation(name: "ownedBy") + group: Group @relation(name: "ownedBy", kind: "Group") + } + type User @implements(interface: "Node") { + name: String! @field(at: "name") + } + type Group @implements(interface: "Node") { + name: String! @field(at: "name") + } + `, + }); + const entity = { + kind: 'Entity', + relations: [ + { type: 'ownedBy', targetRef: 'user:default/john' }, + { type: 'ownedBy', targetRef: 'group:default/team-a' }, + ], + }; + const user = { + kind: 'User', + name: 'John', + }; + const loader = () => + new DataLoader(async ids => + ids.map(id => { + const { query: { ref } = {} } = decodeId(id as string); + if (ref === 'user:default/john') return user; + if (ref === 'group:default/team-a') return null; + return entity; + }), + ); + const query = await createGraphQLAPI(TestModule, loader); + const result = await query(/* GraphQL */ ` + node(id: ${JSON.stringify( + encodeId({ + source: 'Mock', + typename: 'Entity', + query: { ref: 'entity' }, + }), + )}) { + ...on Entity { + ownedBy { name } + owner { name } + group { name } + } + } + `); + expect(result).toEqual({ + node: { + ownedBy: { name: 'John' }, + owner: { name: 'John' }, + group: null, + }, + }); + }); }); diff --git a/plugins/graphql-backend-module-catalog/src/relationDirectiveMapper.ts b/plugins/graphql-backend-module-catalog/src/relationDirectiveMapper.ts index 5114215484..9e3167c10c 100644 --- a/plugins/graphql-backend-module-catalog/src/relationDirectiveMapper.ts +++ b/plugins/graphql-backend-module-catalog/src/relationDirectiveMapper.ts @@ -15,7 +15,7 @@ import { encodeId, isConnectionType, createConnectionType, - getNodeTypeForConnection + getNodeTypeForConnection, } from '@frontside/hydraphql'; import { CATALOG_SOURCE } from './constants'; @@ -62,7 +62,7 @@ export function relationDirectiveMapper( if (directive.nodeType) { const nodeType = getNodeTypeForConnection( directive.nodeType, - (name) => api.typeMap[name], + name => api.typeMap[name], (name, type) => (api.typeMap[name] = type), ); @@ -102,7 +102,7 @@ export function relationDirectiveMapper( field.resolve = async ({ id }, args, { loader }) => { const ids = filterEntityRefs( - await loader.load(id) as Entity, + (await loader.load(id)) as Entity, directive.name, directive.kind, ).map(ref => ({ @@ -112,15 +112,21 @@ export function relationDirectiveMapper( query: { ref }, }), })); + const connection = connectionFromArray<{ id: string } | null>(ids, args); + ( + await loader.loadMany(connection.edges.map(({ node }) => node!.id)) + ).forEach( + (entity, index) => !entity && (connection.edges[index].node = null), + ); return { - ...connectionFromArray(ids, args), + ...connection, count: ids.length, }; }; } else { field.resolve = async ({ id }, _, { loader }) => { - const ids = filterEntityRefs( - await loader.load(id) as Entity, + const ids: ({ id: string } | null)[] = filterEntityRefs( + (await loader.load(id)) as Entity, directive.name, directive.kind, ).map(ref => ({ @@ -130,6 +136,10 @@ export function relationDirectiveMapper( query: { ref }, }), })); + if (!isList) ids.splice(1); + (await loader.loadMany(ids.map(node => node!.id))).forEach( + (entity, index) => !entity && (ids[index] = null), + ); return isList ? ids : ids[0] ?? null; }; } From d329856fc1f9313571f6019c6d060c56544eeafe Mon Sep 17 00:00:00 2001 From: Dmitriy Lazarev Date: Mon, 18 Dec 2023 20:47:09 +0400 Subject: [PATCH 2/2] Add changeset Signed-off-by: Dmitriy Lazarev --- .changeset/dry-news-share.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/dry-news-share.md diff --git a/.changeset/dry-news-share.md b/.changeset/dry-news-share.md new file mode 100644 index 0000000000..1a530f582f --- /dev/null +++ b/.changeset/dry-news-share.md @@ -0,0 +1,5 @@ +--- +'@frontside/backstage-plugin-graphql-backend-module-catalog': patch +--- + +Resolve node to null if entity doesn't exist