fix(db-postgres): joins with hasMany: true relationships nested to an array (#12980)

Fixes https://github.com/payloadcms/payload/issues/12679
This commit is contained in:
Sasha
2025-06-30 21:25:29 +03:00
committed by GitHub
parent 463c9754c7
commit 0e8ac0bad5
8 changed files with 135 additions and 2 deletions

View File

@@ -111,6 +111,12 @@ export const Categories: CollectionConfig = {
collection: 'posts',
on: 'array.category',
},
{
name: 'arrayHasManyPosts',
type: 'join',
collection: 'posts',
on: 'arrayHasMany.category',
},
{
name: 'localizedArrayPosts',
type: 'join',

View File

@@ -114,6 +114,18 @@ export const Posts: CollectionConfig = {
},
],
},
{
name: 'arrayHasMany',
type: 'array',
fields: [
{
name: 'category',
type: 'relationship',
hasMany: true,
relationTo: categoriesSlug,
},
],
},
{
name: 'localizedArray',
type: 'array',

View File

@@ -115,6 +115,7 @@ describe('Joins Field', () => {
camelCaseCategory: category.id,
},
array: [{ category: category.id }],
arrayHasMany: [{ category: [category.id] }],
localizedArray: [{ category: category.id }],
blocks: [{ blockType: 'block', category: category.id }],
})
@@ -247,6 +248,16 @@ describe('Joins Field', () => {
expect(categoryWithPosts.arrayPosts.docs).toHaveLength(10)
})
it('should populate joins with array hasMany relationships', async () => {
const categoryWithPosts = await payload.findByID({
id: category.id,
collection: categoriesSlug,
})
expect(categoryWithPosts.arrayHasManyPosts.docs).toBeDefined()
expect(categoryWithPosts.arrayHasManyPosts.docs).toHaveLength(10)
})
it('should populate joins with localized array relationships', async () => {
const categoryWithPosts = await payload.findByID({
id: category.id,

View File

@@ -109,6 +109,7 @@ export interface Config {
'group.relatedPosts': 'posts';
'group.camelCasePosts': 'posts';
arrayPosts: 'posts';
arrayHasManyPosts: 'posts';
localizedArrayPosts: 'posts';
blocksPosts: 'posts';
polymorphic: 'posts';
@@ -240,6 +241,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;
}
/**
@@ -312,6 +320,12 @@ export interface Post {
id?: string | null;
}[]
| null;
arrayHasMany?:
| {
category?: (string | Category)[] | null;
id?: string | null;
}[]
| null;
localizedArray?:
| {
category?: (string | null) | Category;
@@ -405,6 +419,11 @@ export interface Category {
hasNextPage?: boolean;
totalDocs?: number;
};
arrayHasManyPosts?: {
docs?: (string | Post)[];
hasNextPage?: boolean;
totalDocs?: number;
};
localizedArrayPosts?: {
docs?: (string | Post)[];
hasNextPage?: boolean;
@@ -971,6 +990,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
@@ -1002,6 +1028,12 @@ export interface PostsSelect<T extends boolean = true> {
category?: T;
id?: T;
};
arrayHasMany?:
| T
| {
category?: T;
id?: T;
};
localizedArray?:
| T
| {
@@ -1049,6 +1081,7 @@ export interface CategoriesSelect<T extends boolean = true> {
camelCasePosts?: T;
};
arrayPosts?: T;
arrayHasManyPosts?: T;
localizedArrayPosts?: T;
blocksPosts?: T;
polymorphic?: T;

View File

@@ -240,6 +240,18 @@ export default buildConfigWithDefaults({
type: 'relationship',
relationTo: 'directors',
},
{
type: 'array',
name: 'array',
fields: [
{
name: 'director',
type: 'relationship',
relationTo: 'directors',
hasMany: true,
},
],
},
],
},
{

View File

@@ -1151,6 +1151,29 @@ describe('Relationships', () => {
expect(result.docs[0]!.id).toBe(doc.id)
})
it('should allow querying hasMany in array', async () => {
const director = await payload.create({
collection: 'directors',
data: { name: 'Test Director1337' },
})
const movie = await payload.create({
collection: 'movies',
data: { array: [{ director: [director.id] }] },
})
const res = await payload.find({
collection: 'movies',
where: { 'array.director': { equals: director.id } },
})
expect(res.docs).toHaveLength(1)
expect(res.docs[0].id).toBe(movie.id)
const res2 = await payload.find({
collection: 'movies',
where: { 'array.director.name': { equals: 'Test Director1337' } },
})
expect(res2.docs).toHaveLength(1)
expect(res2.docs[0].id).toBe(movie.id)
})
describe('Nested Querying Separate Collections', () => {
let director: Director

View File

@@ -268,6 +268,12 @@ export interface Movie {
name?: string | null;
select?: ('a' | 'b' | 'c')[] | null;
director?: (string | null) | Director;
array?:
| {
director?: (string | Director)[] | null;
id?: string | null;
}[]
| null;
updatedAt: string;
createdAt: string;
_status?: ('draft' | 'published') | null;
@@ -313,6 +319,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;
}
/**
@@ -733,6 +746,12 @@ export interface MoviesSelect<T extends boolean = true> {
name?: T;
select?: T;
director?: T;
array?:
| T
| {
director?: T;
id?: T;
};
updatedAt?: T;
createdAt?: T;
_status?: T;
@@ -906,6 +925,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