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
This commit is contained in:
@@ -84,7 +84,7 @@ export interface Config {
|
||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||
};
|
||||
db: {
|
||||
defaultIDType: number;
|
||||
defaultIDType: string;
|
||||
};
|
||||
globals: {
|
||||
menu: Menu;
|
||||
@@ -124,7 +124,7 @@ export interface UserAuthOperations {
|
||||
* via the `definition` "posts".
|
||||
*/
|
||||
export interface Post {
|
||||
id: number;
|
||||
id: string;
|
||||
title?: string | null;
|
||||
content?: {
|
||||
root: {
|
||||
@@ -149,7 +149,7 @@ export interface Post {
|
||||
* via the `definition` "media".
|
||||
*/
|
||||
export interface Media {
|
||||
id: number;
|
||||
id: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
url?: string | null;
|
||||
@@ -193,7 +193,7 @@ export interface Media {
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: number;
|
||||
id: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email: string;
|
||||
@@ -217,24 +217,24 @@ export interface User {
|
||||
* via the `definition` "payload-locked-documents".
|
||||
*/
|
||||
export interface PayloadLockedDocument {
|
||||
id: number;
|
||||
id: string;
|
||||
document?:
|
||||
| ({
|
||||
relationTo: 'posts';
|
||||
value: number | Post;
|
||||
value: string | Post;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'media';
|
||||
value: number | Media;
|
||||
value: string | Media;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'users';
|
||||
value: number | User;
|
||||
value: string | User;
|
||||
} | null);
|
||||
globalSlug?: string | null;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: number | User;
|
||||
value: string | User;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
@@ -244,10 +244,10 @@ export interface PayloadLockedDocument {
|
||||
* via the `definition` "payload-preferences".
|
||||
*/
|
||||
export interface PayloadPreference {
|
||||
id: number;
|
||||
id: string;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: number | User;
|
||||
value: string | User;
|
||||
};
|
||||
key?: string | null;
|
||||
value?:
|
||||
@@ -267,7 +267,7 @@ export interface PayloadPreference {
|
||||
* via the `definition` "payload-migrations".
|
||||
*/
|
||||
export interface PayloadMigration {
|
||||
id: number;
|
||||
id: string;
|
||||
name?: string | null;
|
||||
batch?: number | null;
|
||||
updatedAt: string;
|
||||
@@ -393,7 +393,7 @@ export interface PayloadMigrationsSelect<T extends boolean = true> {
|
||||
* via the `definition` "menu".
|
||||
*/
|
||||
export interface Menu {
|
||||
id: number;
|
||||
id: string;
|
||||
globalText?: string | null;
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
|
||||
@@ -19,7 +19,10 @@ const openAccess = {
|
||||
update: () => true,
|
||||
}
|
||||
|
||||
const collectionWithName = (collectionSlug: string): CollectionConfig => {
|
||||
const collectionWithName = (
|
||||
collectionSlug: string,
|
||||
extra: Partial<CollectionConfig> = {},
|
||||
): CollectionConfig => {
|
||||
return {
|
||||
slug: collectionSlug,
|
||||
access: openAccess,
|
||||
@@ -29,6 +32,7 @@ const collectionWithName = (collectionSlug: string): CollectionConfig => {
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
...extra,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,6 +248,7 @@ export default buildConfigWithDefaults({
|
||||
],
|
||||
},
|
||||
],
|
||||
versions: { drafts: true },
|
||||
},
|
||||
{
|
||||
slug: 'custom-ids',
|
||||
@@ -261,7 +266,15 @@ export default buildConfigWithDefaults({
|
||||
},
|
||||
],
|
||||
},
|
||||
collectionWithName(relationSlug),
|
||||
collectionWithName(relationSlug, {
|
||||
access: {
|
||||
...openAccess,
|
||||
read: () => {
|
||||
return { name: { not_equals: 'restricted' } }
|
||||
},
|
||||
},
|
||||
versions: { drafts: true },
|
||||
}),
|
||||
collectionWithName('dummy'),
|
||||
{
|
||||
...collectionWithName(errorOnHookSlug),
|
||||
|
||||
@@ -12,6 +12,8 @@ import { idToString } from '../helpers/idToString.js'
|
||||
import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
||||
import { errorOnHookSlug, pointSlug, relationSlug, slug } from './config.js'
|
||||
|
||||
const formatID = (id: number | string) => (typeof id === 'number' ? id : `"${id}"`)
|
||||
|
||||
const title = 'title'
|
||||
|
||||
let restClient: NextRESTClient
|
||||
@@ -1010,6 +1012,99 @@ describe('collections-graphql', () => {
|
||||
const queriedDoc = res.data.CyclicalRelationships.docs[0]
|
||||
expect(queriedDoc.title).toEqual(queriedDoc.relationToSelf.title)
|
||||
})
|
||||
|
||||
it('should still query hasMany relationships when some document was deleted', async () => {
|
||||
const relation_1_draft = await payload.create({
|
||||
collection: 'relation',
|
||||
data: { _status: 'draft', name: 'relation_1_draft' },
|
||||
draft: true,
|
||||
})
|
||||
|
||||
const relation_2 = await payload.create({
|
||||
collection: 'relation',
|
||||
data: { name: 'relation_2', _status: 'published' },
|
||||
})
|
||||
|
||||
await payload.create({
|
||||
collection: 'posts',
|
||||
draft: true,
|
||||
data: {
|
||||
_status: 'draft',
|
||||
title: 'post with relations in draft',
|
||||
relationHasManyField: [relation_1_draft.id, relation_2.id],
|
||||
},
|
||||
})
|
||||
|
||||
await payload.delete({ collection: 'relation', id: relation_1_draft.id })
|
||||
|
||||
const query = `query {
|
||||
Posts(draft:true,where: { title: { equals: "post with relations in draft" }}) {
|
||||
docs {
|
||||
id
|
||||
title
|
||||
relationHasManyField {
|
||||
id,
|
||||
name
|
||||
}
|
||||
}
|
||||
totalDocs
|
||||
}
|
||||
}`
|
||||
|
||||
const res = await restClient
|
||||
.GRAPHQL_POST({ body: JSON.stringify({ query }) })
|
||||
.then((res) => res.json())
|
||||
|
||||
const queriedDoc = res.data.Posts.docs[0]
|
||||
expect(queriedDoc.title).toBe('post with relations in draft')
|
||||
|
||||
expect(queriedDoc.relationHasManyField[0].id).toBe(relation_2.id)
|
||||
})
|
||||
|
||||
it('should still query hasMany relationships when user doesnt have access to some document', async () => {
|
||||
const relation_1_draft = await payload.create({
|
||||
collection: 'relation',
|
||||
data: { name: 'restricted' },
|
||||
})
|
||||
|
||||
const relation_2 = await payload.create({
|
||||
collection: 'relation',
|
||||
data: { name: 'relation_2' },
|
||||
})
|
||||
|
||||
await payload.create({
|
||||
collection: 'posts',
|
||||
draft: true,
|
||||
data: {
|
||||
_status: 'draft',
|
||||
title: 'post with relation restricted',
|
||||
relationHasManyField: [relation_1_draft.id, relation_2.id],
|
||||
},
|
||||
})
|
||||
|
||||
const query = `query {
|
||||
Posts(draft:true,where: { title: { equals: "post with relation restricted" }}) {
|
||||
docs {
|
||||
id
|
||||
title
|
||||
relationHasManyField {
|
||||
id,
|
||||
name
|
||||
}
|
||||
}
|
||||
totalDocs
|
||||
}
|
||||
}`
|
||||
|
||||
const res = await restClient
|
||||
.GRAPHQL_POST({ body: JSON.stringify({ query }) })
|
||||
.then((res) => res.json())
|
||||
|
||||
const queriedDoc = res.data.Posts.docs[0]
|
||||
expect(queriedDoc.title).toBe('post with relation restricted')
|
||||
|
||||
expect(queriedDoc.relationHasManyField[0].id).toBe(relation_2.id)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1106,7 +1201,7 @@ describe('collections-graphql', () => {
|
||||
})
|
||||
|
||||
const query = `{
|
||||
CyclicalRelationship(id: ${typeof newDoc.id === 'number' ? newDoc.id : `"${newDoc.id}"`}) {
|
||||
CyclicalRelationship(id: ${formatID(newDoc.id)}) {
|
||||
media {
|
||||
id
|
||||
title
|
||||
|
||||
@@ -148,6 +148,13 @@ export interface User {
|
||||
hash?: string | null;
|
||||
loginAttempts?: number | null;
|
||||
lockUntil?: string | null;
|
||||
sessions?:
|
||||
| {
|
||||
id: string;
|
||||
createdAt?: string | null;
|
||||
expiresAt: string;
|
||||
}[]
|
||||
| null;
|
||||
password?: string | null;
|
||||
}
|
||||
/**
|
||||
@@ -219,6 +226,7 @@ export interface Post {
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
_status?: ('draft' | 'published') | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
@@ -229,6 +237,7 @@ export interface Relation {
|
||||
name?: string | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
_status?: ('draft' | 'published') | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
@@ -435,6 +444,13 @@ export interface UsersSelect<T extends boolean = true> {
|
||||
hash?: T;
|
||||
loginAttempts?: T;
|
||||
lockUntil?: T;
|
||||
sessions?:
|
||||
| T
|
||||
| {
|
||||
id?: T;
|
||||
createdAt?: T;
|
||||
expiresAt?: T;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
@@ -494,6 +510,7 @@ export interface PostsSelect<T extends boolean = true> {
|
||||
};
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
_status?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
@@ -513,6 +530,7 @@ export interface RelationSelect<T extends boolean = true> {
|
||||
name?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
_status?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
|
||||
Reference in New Issue
Block a user