Skip to content

Commit f6acfdb

Browse files
authored
fix(drizzle): hasMany joins - localized, limit and schema paths (#8633)
Fixes #8630 - Fixes `hasMany: true` and `localized: true` on the foreign field - Adds `limit` to the subquery instead of hardcoded `11`. - Adds the schema path `field.on` to the subquery, without this having 2 or more relationship fields to the same collection breaks joins - Properly checks if the field is `hasMany`
1 parent 1dcae37 commit f6acfdb

File tree

8 files changed

+132
-13
lines changed

8 files changed

+132
-13
lines changed

packages/drizzle/src/find/traverseFields.ts

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import type { DBQueryConfig } from 'drizzle-orm'
12
import type { LibSQLDatabase } from 'drizzle-orm/libsql'
23
import type { Field, JoinQuery } from 'payload'
34

4-
import { and, type DBQueryConfig, eq, sql } from 'drizzle-orm'
5+
import { and, eq, sql } from 'drizzle-orm'
56
import { fieldAffectsData, fieldIsVirtual, tabHasName } from 'payload/shared'
67
import toSnakeCase from 'to-snake-case'
78

@@ -245,12 +246,15 @@ export const traverseFields = ({
245246

246247
const fields = adapter.payload.collections[field.collection].config.fields
247248
const joinCollectionTableName = adapter.tableNameMap.get(toSnakeCase(field.collection))
248-
const joinTableName = `${adapter.tableNameMap.get(toSnakeCase(field.collection))}${
249+
let joinTableName = `${adapter.tableNameMap.get(toSnakeCase(field.collection))}${
249250
field.localized && adapter.payload.config.localization ? adapter.localesSuffix : ''
250251
}`
251252

252-
if (!adapter.tables[joinTableName][field.on]) {
253+
if (field.hasMany) {
253254
const db = adapter.drizzle as LibSQLDatabase
255+
if (field.localized) {
256+
joinTableName = adapter.tableNameMap.get(toSnakeCase(field.collection))
257+
}
254258
const joinTable = `${joinTableName}${adapter.relationshipsSuffix}`
255259

256260
const joins: BuildQueryJoinAliases = [
@@ -262,6 +266,7 @@ export const traverseFields = ({
262266
sql.raw(`"${joinTable}"."${topLevelTableName}_id"`),
263267
adapter.tables[currentTableName].id,
264268
),
269+
eq(adapter.tables[joinTable].path, field.on),
265270
),
266271
table: adapter.tables[joinTable],
267272
},
@@ -291,30 +296,35 @@ export const traverseFields = ({
291296
query: db
292297
.select({
293298
id: adapter.tables[joinTableName].id,
299+
...(field.localized && {
300+
locale: adapter.tables[joinTable].locale,
301+
}),
294302
})
295303
.from(adapter.tables[joinTableName])
296304
.where(subQueryWhere)
297305
.orderBy(orderBy.order(orderBy.column))
298-
.limit(11),
306+
.limit(limit),
299307
})
300308

301309
const columnName = `${path.replaceAll('.', '_')}${field.name}`
302310

303-
const extras = field.localized ? _locales.extras : currentArgs.extras
311+
const jsonObjectSelect = field.localized
312+
? sql.raw(`'_parentID', "id", '_locale', "locale"`)
313+
: sql.raw(`'id', "id"`)
304314

305315
if (adapter.name === 'sqlite') {
306-
extras[columnName] = sql`
316+
currentArgs.extras[columnName] = sql`
307317
COALESCE((
308-
SELECT json_group_array("id")
318+
SELECT json_group_array(json_object(${jsonObjectSelect}))
309319
FROM (
310320
${subQuery}
311321
) AS ${sql.raw(`${columnName}_sub`)}
312322
), '[]')
313323
`.as(columnName)
314324
} else {
315-
extras[columnName] = sql`
325+
currentArgs.extras[columnName] = sql`
316326
COALESCE((
317-
SELECT json_agg("id")
327+
SELECT json_agg(json_build_object(${jsonObjectSelect}))
318328
FROM (
319329
${subQuery}
320330
) AS ${sql.raw(`${columnName}_sub`)}

packages/drizzle/src/transform/read/traverseFields.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -452,8 +452,8 @@ export const traverseFields = <T extends Record<string, unknown>>({
452452
} else {
453453
const hasNextPage = limit !== 0 && fieldData.length > limit
454454
fieldResult = {
455-
docs: (hasNextPage ? fieldData.slice(0, limit) : fieldData).map((objOrID) => ({
456-
id: typeof objOrID === 'object' ? objOrID.id : objOrID,
455+
docs: (hasNextPage ? fieldData.slice(0, limit) : fieldData).map(({ id }) => ({
456+
id,
457457
})),
458458
hasNextPage,
459459
}

packages/payload/src/fields/config/sanitizeJoinField.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ export const sanitizeJoinField = ({
8282

8383
// override the join field localized property to use whatever the relationship field has
8484
field.localized = joinRelationship.localized
85+
// override the join field hasMany property to use whatever the relationship field has
86+
field.hasMany = joinRelationship.hasMany
8587

8688
if (!joins[field.collection]) {
8789
joins[field.collection] = [join]

packages/payload/src/fields/config/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1452,6 +1452,10 @@ export type JoinField = {
14521452
*/
14531453
collection: CollectionSlug
14541454
defaultValue?: never
1455+
/**
1456+
* This does not need to be set and will be overridden by the relationship field's hasMany property.
1457+
*/
1458+
hasMany?: boolean
14551459
hidden?: false
14561460
index?: never
14571461
/**

test/joins/collections/Categories.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ export const Categories: CollectionConfig = {
5555
collection: postsSlug,
5656
on: 'categories',
5757
},
58+
{
59+
name: 'hasManyPostsLocalized',
60+
type: 'join',
61+
collection: postsSlug,
62+
on: 'categoriesLocalized',
63+
},
5864
{
5965
name: 'group',
6066
type: 'group',

test/joins/collections/Posts.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ export const Posts: CollectionConfig = {
2929
relationTo: categoriesSlug,
3030
hasMany: true,
3131
},
32+
{
33+
name: 'categoriesLocalized',
34+
type: 'relationship',
35+
relationTo: categoriesSlug,
36+
hasMany: true,
37+
localized: true,
38+
},
3239
{
3340
name: 'group',
3441
type: 'group',

test/joins/int.spec.ts

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { getFileByPath } from 'payload'
55
import { fileURLToPath } from 'url'
66

77
import type { NextRESTClient } from '../helpers/NextRESTClient.js'
8-
import type { Category, Post } from './payload-types.js'
8+
import type { Category, Config, Post } from './payload-types.js'
99

1010
import { devUser } from '../credentials.js'
1111
import { idToString } from '../helpers/idToString.js'
@@ -80,6 +80,7 @@ describe('Joins Field', () => {
8080
category: category.id,
8181
upload: uploadedImage,
8282
categories,
83+
categoriesLocalized: categories,
8384
group: {
8485
category: category.id,
8586
camelCaseCategory: category.id,
@@ -212,6 +213,89 @@ describe('Joins Field', () => {
212213
expect(otherCategoryWithPosts.hasManyPosts.docs[0].title).toBe('test 14')
213214
})
214215

216+
it('should populate joins using find with hasMany localized relationships', async () => {
217+
const post_1 = await createPost(
218+
{
219+
title: `test es localized 1`,
220+
categoriesLocalized: [category.id],
221+
group: {
222+
category: category.id,
223+
camelCaseCategory: category.id,
224+
},
225+
},
226+
'es',
227+
)
228+
229+
const post_2 = await createPost(
230+
{
231+
title: `test es localized 2`,
232+
categoriesLocalized: [otherCategory.id],
233+
group: {
234+
category: category.id,
235+
camelCaseCategory: category.id,
236+
},
237+
},
238+
'es',
239+
)
240+
241+
const resultEn = await payload.find({
242+
collection: 'categories',
243+
where: {
244+
id: { equals: category.id },
245+
},
246+
})
247+
const otherResultEn = await payload.find({
248+
collection: 'categories',
249+
where: {
250+
id: { equals: otherCategory.id },
251+
},
252+
})
253+
254+
const [categoryWithPostsEn] = resultEn.docs
255+
const [otherCategoryWithPostsEn] = otherResultEn.docs
256+
257+
expect(categoryWithPostsEn.hasManyPostsLocalized.docs).toHaveLength(10)
258+
expect(categoryWithPostsEn.hasManyPostsLocalized.docs[0]).toHaveProperty('title')
259+
expect(categoryWithPostsEn.hasManyPostsLocalized.docs[0].title).toBe('test 14')
260+
expect(otherCategoryWithPostsEn.hasManyPostsLocalized.docs).toHaveLength(8)
261+
expect(otherCategoryWithPostsEn.hasManyPostsLocalized.docs[0]).toHaveProperty('title')
262+
expect(otherCategoryWithPostsEn.hasManyPostsLocalized.docs[0].title).toBe('test 14')
263+
264+
const resultEs = await payload.find({
265+
collection: 'categories',
266+
locale: 'es',
267+
where: {
268+
id: { equals: category.id },
269+
},
270+
})
271+
const otherResultEs = await payload.find({
272+
collection: 'categories',
273+
locale: 'es',
274+
where: {
275+
id: { equals: otherCategory.id },
276+
},
277+
})
278+
279+
const [categoryWithPostsEs] = resultEs.docs
280+
const [otherCategoryWithPostsEs] = otherResultEs.docs
281+
282+
expect(categoryWithPostsEs.hasManyPostsLocalized.docs).toHaveLength(1)
283+
expect(categoryWithPostsEs.hasManyPostsLocalized.docs[0].title).toBe('test es localized 1')
284+
285+
expect(otherCategoryWithPostsEs.hasManyPostsLocalized.docs).toHaveLength(1)
286+
expect(otherCategoryWithPostsEs.hasManyPostsLocalized.docs[0].title).toBe('test es localized 2')
287+
288+
// clean up
289+
await payload.delete({
290+
collection: 'posts',
291+
where: {
292+
id: {
293+
in: [post_1.id, post_2.id],
294+
},
295+
},
296+
})
297+
})
298+
215299
it('should not error when deleting documents with joins', async () => {
216300
const category = await payload.create({
217301
collection: 'categories',
@@ -499,9 +583,10 @@ describe('Joins Field', () => {
499583
})
500584
})
501585

502-
async function createPost(overrides?: Partial<Post>) {
586+
async function createPost(overrides?: Partial<Post>, locale?: Config['locale']) {
503587
return payload.create({
504588
collection: 'posts',
589+
locale,
505590
data: {
506591
title: 'test',
507592
...overrides,

test/joins/payload-types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export interface Post {
5858
upload?: (string | null) | Upload;
5959
category?: (string | null) | Category;
6060
categories?: (string | Category)[] | null;
61+
categoriesLocalized?: (string | Category)[] | null;
6162
group?: {
6263
category?: (string | null) | Category;
6364
camelCaseCategory?: (string | null) | Category;
@@ -102,6 +103,10 @@ export interface Category {
102103
docs?: (string | Post)[] | null;
103104
hasNextPage?: boolean | null;
104105
} | null;
106+
hasManyPostsLocalized?: {
107+
docs?: (string | Post)[] | null;
108+
hasNextPage?: boolean | null;
109+
} | null;
105110
group?: {
106111
relatedPosts?: {
107112
docs?: (string | Post)[] | null;

0 commit comments

Comments
 (0)