Skip to content

Commit 48e9576

Browse files
authored
fix(graphql): error querying hasMany relationships when some document was deleted (#14002)
When you have a `hasMany: true` relationship field with at least 1 ID that references nothing (because the actual document was deleted and since MongoDB doesn't have foreign constraints - the relationship field still includes that "dead" ID) graphql querying of that field fails. This PR fixes it. The same applies if you don't have access to some document for all DBs
1 parent accd95e commit 48e9576

File tree

5 files changed

+148
-22
lines changed

5 files changed

+148
-22
lines changed

packages/graphql/src/schema/fieldToSchemaMap.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -678,15 +678,15 @@ export const fieldToSchemaMap: FieldToSchemaMap = {
678678

679679
if (result) {
680680
if (isRelatedToManyCollections) {
681-
results[i] = {
681+
results.push({
682682
relationTo: collectionSlug,
683683
value: {
684684
...result,
685685
collection: collectionSlug,
686686
},
687-
}
687+
})
688688
} else {
689-
results[i] = result
689+
results.push(result)
690690
}
691691
}
692692
}
@@ -1081,15 +1081,15 @@ export const fieldToSchemaMap: FieldToSchemaMap = {
10811081

10821082
if (result) {
10831083
if (isRelatedToManyCollections) {
1084-
results[i] = {
1084+
results.push({
10851085
relationTo: collectionSlug,
10861086
value: {
10871087
...result,
10881088
collection: collectionSlug,
10891089
},
1090-
}
1090+
})
10911091
} else {
1092-
results[i] = result
1092+
results.push(result)
10931093
}
10941094
}
10951095
}

test/_community/payload-types.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export interface Config {
8484
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
8585
};
8686
db: {
87-
defaultIDType: number;
87+
defaultIDType: string;
8888
};
8989
globals: {
9090
menu: Menu;
@@ -124,7 +124,7 @@ export interface UserAuthOperations {
124124
* via the `definition` "posts".
125125
*/
126126
export interface Post {
127-
id: number;
127+
id: string;
128128
title?: string | null;
129129
content?: {
130130
root: {
@@ -149,7 +149,7 @@ export interface Post {
149149
* via the `definition` "media".
150150
*/
151151
export interface Media {
152-
id: number;
152+
id: string;
153153
updatedAt: string;
154154
createdAt: string;
155155
url?: string | null;
@@ -193,7 +193,7 @@ export interface Media {
193193
* via the `definition` "users".
194194
*/
195195
export interface User {
196-
id: number;
196+
id: string;
197197
updatedAt: string;
198198
createdAt: string;
199199
email: string;
@@ -217,24 +217,24 @@ export interface User {
217217
* via the `definition` "payload-locked-documents".
218218
*/
219219
export interface PayloadLockedDocument {
220-
id: number;
220+
id: string;
221221
document?:
222222
| ({
223223
relationTo: 'posts';
224-
value: number | Post;
224+
value: string | Post;
225225
} | null)
226226
| ({
227227
relationTo: 'media';
228-
value: number | Media;
228+
value: string | Media;
229229
} | null)
230230
| ({
231231
relationTo: 'users';
232-
value: number | User;
232+
value: string | User;
233233
} | null);
234234
globalSlug?: string | null;
235235
user: {
236236
relationTo: 'users';
237-
value: number | User;
237+
value: string | User;
238238
};
239239
updatedAt: string;
240240
createdAt: string;
@@ -244,10 +244,10 @@ export interface PayloadLockedDocument {
244244
* via the `definition` "payload-preferences".
245245
*/
246246
export interface PayloadPreference {
247-
id: number;
247+
id: string;
248248
user: {
249249
relationTo: 'users';
250-
value: number | User;
250+
value: string | User;
251251
};
252252
key?: string | null;
253253
value?:
@@ -267,7 +267,7 @@ export interface PayloadPreference {
267267
* via the `definition` "payload-migrations".
268268
*/
269269
export interface PayloadMigration {
270-
id: number;
270+
id: string;
271271
name?: string | null;
272272
batch?: number | null;
273273
updatedAt: string;
@@ -393,7 +393,7 @@ export interface PayloadMigrationsSelect<T extends boolean = true> {
393393
* via the `definition` "menu".
394394
*/
395395
export interface Menu {
396-
id: number;
396+
id: string;
397397
globalText?: string | null;
398398
updatedAt?: string | null;
399399
createdAt?: string | null;

test/collections-graphql/config.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ const openAccess = {
1919
update: () => true,
2020
}
2121

22-
const collectionWithName = (collectionSlug: string): CollectionConfig => {
22+
const collectionWithName = (
23+
collectionSlug: string,
24+
extra: Partial<CollectionConfig> = {},
25+
): CollectionConfig => {
2326
return {
2427
slug: collectionSlug,
2528
access: openAccess,
@@ -29,6 +32,7 @@ const collectionWithName = (collectionSlug: string): CollectionConfig => {
2932
type: 'text',
3033
},
3134
],
35+
...extra,
3236
}
3337
}
3438

@@ -244,6 +248,7 @@ export default buildConfigWithDefaults({
244248
],
245249
},
246250
],
251+
versions: { drafts: true },
247252
},
248253
{
249254
slug: 'custom-ids',
@@ -261,7 +266,15 @@ export default buildConfigWithDefaults({
261266
},
262267
],
263268
},
264-
collectionWithName(relationSlug),
269+
collectionWithName(relationSlug, {
270+
access: {
271+
...openAccess,
272+
read: () => {
273+
return { name: { not_equals: 'restricted' } }
274+
},
275+
},
276+
versions: { drafts: true },
277+
}),
265278
collectionWithName('dummy'),
266279
{
267280
...collectionWithName(errorOnHookSlug),

test/collections-graphql/int.spec.ts

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import { idToString } from '../helpers/idToString.js'
1212
import { initPayloadInt } from '../helpers/initPayloadInt.js'
1313
import { errorOnHookSlug, pointSlug, relationSlug, slug } from './config.js'
1414

15+
const formatID = (id: number | string) => (typeof id === 'number' ? id : `"${id}"`)
16+
1517
const title = 'title'
1618

1719
let restClient: NextRESTClient
@@ -1010,6 +1012,99 @@ describe('collections-graphql', () => {
10101012
const queriedDoc = res.data.CyclicalRelationships.docs[0]
10111013
expect(queriedDoc.title).toEqual(queriedDoc.relationToSelf.title)
10121014
})
1015+
1016+
it('should still query hasMany relationships when some document was deleted', async () => {
1017+
const relation_1_draft = await payload.create({
1018+
collection: 'relation',
1019+
data: { _status: 'draft', name: 'relation_1_draft' },
1020+
draft: true,
1021+
})
1022+
1023+
const relation_2 = await payload.create({
1024+
collection: 'relation',
1025+
data: { name: 'relation_2', _status: 'published' },
1026+
})
1027+
1028+
await payload.create({
1029+
collection: 'posts',
1030+
draft: true,
1031+
data: {
1032+
_status: 'draft',
1033+
title: 'post with relations in draft',
1034+
relationHasManyField: [relation_1_draft.id, relation_2.id],
1035+
},
1036+
})
1037+
1038+
await payload.delete({ collection: 'relation', id: relation_1_draft.id })
1039+
1040+
const query = `query {
1041+
Posts(draft:true,where: { title: { equals: "post with relations in draft" }}) {
1042+
docs {
1043+
id
1044+
title
1045+
relationHasManyField {
1046+
id,
1047+
name
1048+
}
1049+
}
1050+
totalDocs
1051+
}
1052+
}`
1053+
1054+
const res = await restClient
1055+
.GRAPHQL_POST({ body: JSON.stringify({ query }) })
1056+
.then((res) => res.json())
1057+
1058+
const queriedDoc = res.data.Posts.docs[0]
1059+
expect(queriedDoc.title).toBe('post with relations in draft')
1060+
1061+
expect(queriedDoc.relationHasManyField[0].id).toBe(relation_2.id)
1062+
})
1063+
1064+
it('should still query hasMany relationships when user doesnt have access to some document', async () => {
1065+
const relation_1_draft = await payload.create({
1066+
collection: 'relation',
1067+
data: { name: 'restricted' },
1068+
})
1069+
1070+
const relation_2 = await payload.create({
1071+
collection: 'relation',
1072+
data: { name: 'relation_2' },
1073+
})
1074+
1075+
await payload.create({
1076+
collection: 'posts',
1077+
draft: true,
1078+
data: {
1079+
_status: 'draft',
1080+
title: 'post with relation restricted',
1081+
relationHasManyField: [relation_1_draft.id, relation_2.id],
1082+
},
1083+
})
1084+
1085+
const query = `query {
1086+
Posts(draft:true,where: { title: { equals: "post with relation restricted" }}) {
1087+
docs {
1088+
id
1089+
title
1090+
relationHasManyField {
1091+
id,
1092+
name
1093+
}
1094+
}
1095+
totalDocs
1096+
}
1097+
}`
1098+
1099+
const res = await restClient
1100+
.GRAPHQL_POST({ body: JSON.stringify({ query }) })
1101+
.then((res) => res.json())
1102+
1103+
const queriedDoc = res.data.Posts.docs[0]
1104+
expect(queriedDoc.title).toBe('post with relation restricted')
1105+
1106+
expect(queriedDoc.relationHasManyField[0].id).toBe(relation_2.id)
1107+
})
10131108
})
10141109
})
10151110

@@ -1106,7 +1201,7 @@ describe('collections-graphql', () => {
11061201
})
11071202

11081203
const query = `{
1109-
CyclicalRelationship(id: ${typeof newDoc.id === 'number' ? newDoc.id : `"${newDoc.id}"`}) {
1204+
CyclicalRelationship(id: ${formatID(newDoc.id)}) {
11101205
media {
11111206
id
11121207
title

test/collections-graphql/payload-types.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,13 @@ export interface User {
148148
hash?: string | null;
149149
loginAttempts?: number | null;
150150
lockUntil?: string | null;
151+
sessions?:
152+
| {
153+
id: string;
154+
createdAt?: string | null;
155+
expiresAt: string;
156+
}[]
157+
| null;
151158
password?: string | null;
152159
}
153160
/**
@@ -219,6 +226,7 @@ export interface Post {
219226
};
220227
updatedAt: string;
221228
createdAt: string;
229+
_status?: ('draft' | 'published') | null;
222230
}
223231
/**
224232
* This interface was referenced by `Config`'s JSON-Schema
@@ -229,6 +237,7 @@ export interface Relation {
229237
name?: string | null;
230238
updatedAt: string;
231239
createdAt: string;
240+
_status?: ('draft' | 'published') | null;
232241
}
233242
/**
234243
* This interface was referenced by `Config`'s JSON-Schema
@@ -435,6 +444,13 @@ export interface UsersSelect<T extends boolean = true> {
435444
hash?: T;
436445
loginAttempts?: T;
437446
lockUntil?: T;
447+
sessions?:
448+
| T
449+
| {
450+
id?: T;
451+
createdAt?: T;
452+
expiresAt?: T;
453+
};
438454
}
439455
/**
440456
* This interface was referenced by `Config`'s JSON-Schema
@@ -494,6 +510,7 @@ export interface PostsSelect<T extends boolean = true> {
494510
};
495511
updatedAt?: T;
496512
createdAt?: T;
513+
_status?: T;
497514
}
498515
/**
499516
* This interface was referenced by `Config`'s JSON-Schema
@@ -513,6 +530,7 @@ export interface RelationSelect<T extends boolean = true> {
513530
name?: T;
514531
updatedAt?: T;
515532
createdAt?: T;
533+
_status?: T;
516534
}
517535
/**
518536
* This interface was referenced by `Config`'s JSON-Schema

0 commit comments

Comments
 (0)