Skip to content

Commit 560cabe

Browse files
authored
fix(graphql): force nullable for relationships to avoid errors when the related document is related (#15915)
Fixes #9788 In a situation when you have a relationship field and the related document is deleted, in Postgres the value of this field (if the related document can be deleted) will be set to `NULL`, even if the field is required. This causes issues in GraphQL because of its schema strictness it just throws an error when it sees `null`. The PR forces `nullable` for relationship/upload fields (excluding the ones with `hasMany: true` since they'd have an empty array instead)
1 parent 57bde77 commit 560cabe

4 files changed

Lines changed: 150 additions & 4 deletions

File tree

packages/graphql/src/schema/fieldToSchemaMap.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -654,7 +654,8 @@ export const fieldToSchemaMap: FieldToSchemaMap = {
654654
type: withNullableType({
655655
type: hasManyValues ? new GraphQLList(new GraphQLNonNull(type)) : type,
656656
field,
657-
forceNullable,
657+
// can be null if the related doc is deleted even if the field is required, unless hasMany
658+
forceNullable: !field.hasMany,
658659
parentIsLocalized,
659660
}) as GraphQLOutputType,
660661
args: relationshipArgs,
@@ -1072,7 +1073,8 @@ export const fieldToSchemaMap: FieldToSchemaMap = {
10721073
type: withNullableType({
10731074
type: hasManyValues ? new GraphQLList(new GraphQLNonNull(type)) : type,
10741075
field,
1075-
forceNullable,
1076+
// can be null if the related doc is deleted even if the field is required, unless hasMany
1077+
forceNullable: !field.hasMany,
10761078
parentIsLocalized,
10771079
}) as GraphQLOutputType,
10781080
args: relationshipArgs,

test/graphql/config.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,31 @@ export default buildConfigWithDefaults({
4040
],
4141
},
4242
],
43+
globals: [
44+
{
45+
slug: 'home',
46+
versions: { drafts: true },
47+
fields: [
48+
{
49+
name: 'topPosts',
50+
type: 'array',
51+
required: true,
52+
fields: [
53+
{
54+
name: 'post',
55+
type: 'relationship',
56+
relationTo: 'posts',
57+
required: true,
58+
},
59+
{
60+
name: 'caption',
61+
type: 'text',
62+
},
63+
],
64+
},
65+
],
66+
},
67+
],
4368
admin: {
4469
importMap: {
4570
baseDir: path.resolve(dirname),

test/graphql/int.spec.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,5 +217,56 @@ query {
217217
id: createdPost.id,
218218
})
219219
})
220+
221+
it('should not error when querying a global with a deleted relationship in an array', async () => {
222+
const post1 = await payload.create({
223+
collection: 'posts',
224+
data: {
225+
title: 'Post 1',
226+
},
227+
})
228+
229+
await payload.updateGlobal({
230+
slug: 'home',
231+
data: {
232+
topPosts: [
233+
{
234+
post: post1.id,
235+
caption: 'The best post out there',
236+
},
237+
],
238+
},
239+
})
240+
241+
const query = `query {
242+
Home {
243+
topPosts {
244+
post {
245+
title
246+
}
247+
}
248+
}
249+
}`
250+
251+
const beforeDelete = await restClient
252+
.GRAPHQL_POST({ body: JSON.stringify({ query }) })
253+
.then((res) => res.json())
254+
255+
expect(beforeDelete.errors).toBeUndefined()
256+
expect(beforeDelete.data.Home.topPosts).toEqual([
257+
expect.objectContaining({ post: { title: 'Post 1' } }),
258+
])
259+
260+
await payload.delete({
261+
collection: 'posts',
262+
id: post1.id,
263+
})
264+
265+
const afterDelete = await restClient
266+
.GRAPHQL_POST({ body: JSON.stringify({ query }) })
267+
.then((res) => res.json())
268+
269+
expect(afterDelete.errors).toBeUndefined()
270+
})
220271
})
221272
})

test/graphql/payload-types.ts

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,16 @@ export interface Config {
8787
defaultIDType: string;
8888
};
8989
fallbackLocale: null;
90-
globals: {};
91-
globalsSelect: {};
90+
globals: {
91+
home: Home;
92+
};
93+
globalsSelect: {
94+
home: HomeSelect<false> | HomeSelect<true>;
95+
};
9296
locale: null;
97+
widgets: {
98+
collections: CollectionsWidget;
99+
};
93100
user: User;
94101
jobs: {
95102
tasks: unknown;
@@ -123,6 +130,14 @@ export interface Post {
123130
title?: string | null;
124131
'hyphenated-name'?: string | null;
125132
relationToSelf?: (string | null) | Post;
133+
contentBlockField?:
134+
| {
135+
text?: string | null;
136+
id?: string | null;
137+
blockName?: string | null;
138+
blockType: 'content';
139+
}[]
140+
| null;
126141
updatedAt: string;
127142
createdAt: string;
128143
}
@@ -233,6 +248,17 @@ export interface PostsSelect<T extends boolean = true> {
233248
title?: T;
234249
'hyphenated-name'?: T;
235250
relationToSelf?: T;
251+
contentBlockField?:
252+
| T
253+
| {
254+
content?:
255+
| T
256+
| {
257+
text?: T;
258+
id?: T;
259+
blockName?: T;
260+
};
261+
};
236262
updatedAt?: T;
237263
createdAt?: T;
238264
}
@@ -298,6 +324,48 @@ export interface PayloadMigrationsSelect<T extends boolean = true> {
298324
updatedAt?: T;
299325
createdAt?: T;
300326
}
327+
/**
328+
* This interface was referenced by `Config`'s JSON-Schema
329+
* via the `definition` "home".
330+
*/
331+
export interface Home {
332+
id: string;
333+
topPosts: {
334+
post: string | Post;
335+
caption?: string | null;
336+
id?: string | null;
337+
}[];
338+
_status?: ('draft' | 'published') | null;
339+
updatedAt?: string | null;
340+
createdAt?: string | null;
341+
}
342+
/**
343+
* This interface was referenced by `Config`'s JSON-Schema
344+
* via the `definition` "home_select".
345+
*/
346+
export interface HomeSelect<T extends boolean = true> {
347+
topPosts?:
348+
| T
349+
| {
350+
post?: T;
351+
caption?: T;
352+
id?: T;
353+
};
354+
_status?: T;
355+
updatedAt?: T;
356+
createdAt?: T;
357+
globalType?: T;
358+
}
359+
/**
360+
* This interface was referenced by `Config`'s JSON-Schema
361+
* via the `definition` "collections_widget".
362+
*/
363+
export interface CollectionsWidget {
364+
data?: {
365+
[k: string]: unknown;
366+
};
367+
width: 'full';
368+
}
301369
/**
302370
* This interface was referenced by `Config`'s JSON-Schema
303371
* via the `definition` "auth".

0 commit comments

Comments
 (0)