Skip to content

Commit b0b2fc6

Browse files
DanRibbensBecause789r1tsuu
authored
feat: join field support relationships inside arrays (#9773)
### What? Allow the join field to have a configuration `on` relationships inside of an array, ie `on: 'myArray.myRelationship'`. ### Why? This is a more powerful and expressive way to use the join field and not be limited by usage of array data. For example, if you have a roles array for multinant sites, you could add a join field on the sites to show who the admins are. ### How? This fixes the traverseFields function to allow the configuration to pass sanitization. In addition, the function for querying the drizzle tables needed to be ehanced. Additional changes from #9995: - Significantly improves traverseFields and the 'join' case with a raw query injection pattern, right now it's internal but we could expose it at some point, for example for querying vectors. - Fixes potential issues with not passed locale to traverseFields (it was undefined always) - Adds an empty array fallback for joins with localized relationships Fixes # #9643 --------- Co-authored-by: Because789 <thomas@because789.ch> Co-authored-by: Sasha <64744993+r1tsuu@users.noreply.github.com>
1 parent eb037a0 commit b0b2fc6

File tree

17 files changed

+291
-142
lines changed

17 files changed

+291
-142
lines changed

docs/fields/join.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,8 @@ powerful Admin UI.
125125
|------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
126126
| **`name`** \* | To be used as the property name when retrieved from the database. [More](/docs/fields/overview#field-names) |
127127
| **`collection`** \* | The `slug`s having the relationship field. |
128-
| **`on`** \* | The name of the relationship or upload field that relates to the collection document. Use dot notation for nested paths, like 'myGroup.relationName'. |
129-
| **`where`** \* | A `Where` query to hide related documents from appearing. Will be merged with any `where` specified in the request. |
128+
| **`on`** \* | The name of the relationship or upload field that relates to the collection document. Use dot notation for nested paths, like 'myGroup.relationName'. |
129+
| **`where`** | A `Where` query to hide related documents from appearing. Will be merged with any `where` specified in the request. |
130130
| **`maxDepth`** | Default is 1, Sets a maximum population depth for this field, regardless of the remaining depth when this field is reached. [Max Depth](/docs/getting-started/concepts#field-level-max-depth). |
131131
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
132132
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |

packages/drizzle/src/find/chainMethods.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import type { QueryPromise } from 'drizzle-orm'
2-
31
export type ChainedMethods = {
42
args: unknown[]
53
method: string
@@ -10,7 +8,7 @@ export type ChainedMethods = {
108
* @param methods
119
* @param query
1210
*/
13-
const chainMethods = <T>({ methods, query }): QueryPromise<T> => {
11+
const chainMethods = <T>({ methods, query }: { methods: ChainedMethods; query: T }): T => {
1412
return methods.reduce((query, { args, method }) => {
1513
return query[method](...args)
1614
}, query)

packages/drizzle/src/find/findMany.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export const findMany = async function find({
7373
fields,
7474
joinQuery,
7575
joins,
76+
locale,
7677
select,
7778
tableName,
7879
versions,

packages/drizzle/src/find/traverseFields.ts

Lines changed: 47 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import type { LibSQLDatabase } from 'drizzle-orm/libsql'
2-
import type { FlattenedField, JoinQuery, SelectMode, SelectType } from 'payload'
2+
import type { FlattenedField, JoinQuery, SelectMode, SelectType, Where } from 'payload'
33

4-
import { and, eq, sql } from 'drizzle-orm'
4+
import { sql } from 'drizzle-orm'
55
import { fieldIsVirtual } from 'payload/shared'
66
import toSnakeCase from 'to-snake-case'
77

88
import type { BuildQueryJoinAliases, ChainedMethods, DrizzleAdapter } from '../types.js'
99
import type { Result } from './buildFindManyArgs.js'
1010

1111
import buildQuery from '../queries/buildQuery.js'
12+
import { jsonAggBuildObject } from '../utilities/json.js'
13+
import { rawConstraint } from '../utilities/rawConstraint.js'
1214
import { chainMethods } from './chainMethods.js'
1315

1416
type TraverseFieldArgs = {
@@ -145,6 +147,7 @@ export const traverseFields = ({
145147
depth,
146148
fields: field.flattenedFields,
147149
joinQuery,
150+
locale,
148151
path: '',
149152
select: typeof arraySelect === 'object' ? arraySelect : undefined,
150153
selectMode,
@@ -254,6 +257,7 @@ export const traverseFields = ({
254257
depth,
255258
fields: block.flattenedFields,
256259
joinQuery,
260+
locale,
257261
path: '',
258262
select: typeof blockSelect === 'object' ? blockSelect : undefined,
259263
selectMode: blockSelectMode,
@@ -294,6 +298,7 @@ export const traverseFields = ({
294298
fields: field.flattenedFields,
295299
joinQuery,
296300
joins,
301+
locale,
297302
path: `${path}${field.name}_`,
298303
select: typeof fieldSelect === 'object' ? fieldSelect : undefined,
299304
selectAllOnCurrentLevel:
@@ -348,92 +353,37 @@ export const traverseFields = ({
348353

349354
const joins: BuildQueryJoinAliases = []
350355

351-
const buildQueryResult = buildQuery({
352-
adapter,
353-
fields,
354-
joins,
355-
locale,
356-
sort,
357-
tableName: joinCollectionTableName,
358-
where,
359-
})
360-
361-
let subQueryWhere = buildQueryResult.where
362-
const orderBy = buildQueryResult.orderBy
363-
364-
let joinLocalesCollectionTableName: string | undefined
365-
366356
const currentIDColumn = versions
367357
? adapter.tables[currentTableName].parent
368358
: adapter.tables[currentTableName].id
369359

370-
// Handle hasMany _rels table
371-
if (field.hasMany) {
372-
const joinRelsCollectionTableName = `${joinCollectionTableName}${adapter.relationshipsSuffix}`
373-
374-
if (field.localized) {
375-
joinLocalesCollectionTableName = joinRelsCollectionTableName
376-
}
377-
378-
let columnReferenceToCurrentID: string
379-
380-
if (versions) {
381-
columnReferenceToCurrentID = `${topLevelTableName
382-
.replace('_', '')
383-
.replace(new RegExp(`${adapter.versionsSuffix}$`), '')}_id`
384-
} else {
385-
columnReferenceToCurrentID = `${topLevelTableName}_id`
386-
}
387-
388-
joins.push({
389-
type: 'innerJoin',
390-
condition: and(
391-
eq(
392-
adapter.tables[joinRelsCollectionTableName].parent,
393-
adapter.tables[joinCollectionTableName].id,
394-
),
395-
eq(
396-
sql.raw(`"${joinRelsCollectionTableName}"."${columnReferenceToCurrentID}"`),
397-
currentIDColumn,
398-
),
399-
eq(adapter.tables[joinRelsCollectionTableName].path, field.on),
400-
),
401-
table: adapter.tables[joinRelsCollectionTableName],
402-
})
403-
} else {
404-
// Handle localized without hasMany
405-
406-
const foreignColumn = field.on.replaceAll('.', '_')
407-
408-
if (field.localized) {
409-
joinLocalesCollectionTableName = `${joinCollectionTableName}${adapter.localesSuffix}`
410-
411-
joins.push({
412-
type: 'innerJoin',
413-
condition: and(
414-
eq(
415-
adapter.tables[joinLocalesCollectionTableName]._parentID,
416-
adapter.tables[joinCollectionTableName].id,
417-
),
418-
eq(adapter.tables[joinLocalesCollectionTableName][foreignColumn], currentIDColumn),
419-
),
420-
table: adapter.tables[joinLocalesCollectionTableName],
421-
})
422-
// Handle without localized and without hasMany, just a condition append to where. With localized the inner join handles eq.
423-
} else {
424-
const constraint = eq(
425-
adapter.tables[joinCollectionTableName][foreignColumn],
426-
currentIDColumn,
427-
)
360+
let joinQueryWhere: Where = {
361+
[field.on]: {
362+
equals: rawConstraint(currentIDColumn),
363+
},
364+
}
428365

429-
if (subQueryWhere) {
430-
subQueryWhere = and(subQueryWhere, constraint)
431-
} else {
432-
subQueryWhere = constraint
433-
}
366+
if (where) {
367+
joinQueryWhere = {
368+
and: [joinQueryWhere, where],
434369
}
435370
}
436371

372+
const {
373+
orderBy,
374+
selectFields,
375+
where: subQueryWhere,
376+
} = buildQuery({
377+
adapter,
378+
fields,
379+
joins,
380+
locale,
381+
selectLocale: true,
382+
sort,
383+
tableName: joinCollectionTableName,
384+
where: joinQueryWhere,
385+
})
386+
437387
const chainedMethods: ChainedMethods = []
438388

439389
joins.forEach(({ type, condition, table }) => {
@@ -452,49 +402,29 @@ export const traverseFields = ({
452402

453403
const db = adapter.drizzle as LibSQLDatabase
454404

405+
const columnName = `${path.replaceAll('.', '_')}${field.name}`
406+
407+
const subQueryAlias = `${columnName}_alias`
408+
455409
const subQuery = chainMethods({
456410
methods: chainedMethods,
457411
query: db
458-
.select({
459-
id: adapter.tables[joinCollectionTableName].id,
460-
...(joinLocalesCollectionTableName && {
461-
locale:
462-
adapter.tables[joinLocalesCollectionTableName].locale ||
463-
adapter.tables[joinLocalesCollectionTableName]._locale,
464-
}),
465-
})
412+
.select(selectFields as any)
466413
.from(adapter.tables[joinCollectionTableName])
467414
.where(subQueryWhere)
468415
.orderBy(() => orderBy.map(({ column, order }) => order(column))),
469-
})
470-
471-
const columnName = `${path.replaceAll('.', '_')}${field.name}`
472-
473-
const jsonObjectSelect = field.localized
474-
? sql.raw(
475-
`'_parentID', "id", '_locale', "${adapter.tables[joinLocalesCollectionTableName].locale ? 'locale' : '_locale'}"`,
476-
)
477-
: sql.raw(`'id', "id"`)
478-
479-
if (adapter.name === 'sqlite') {
480-
currentArgs.extras[columnName] = sql`
481-
COALESCE((
482-
SELECT json_group_array(json_object(${jsonObjectSelect}))
483-
FROM (
484-
${subQuery}
485-
) AS ${sql.raw(`${columnName}_sub`)}
486-
), '[]')
487-
`.as(columnName)
488-
} else {
489-
currentArgs.extras[columnName] = sql`
490-
COALESCE((
491-
SELECT json_agg(json_build_object(${jsonObjectSelect}))
492-
FROM (
493-
${subQuery}
494-
) AS ${sql.raw(`${columnName}_sub`)}
495-
), '[]'::json)
496-
`.as(columnName)
497-
}
416+
}).as(subQueryAlias)
417+
418+
currentArgs.extras[columnName] = sql`${db
419+
.select({
420+
result: jsonAggBuildObject(adapter, {
421+
id: sql.raw(`"${subQueryAlias}".id`),
422+
...(selectFields._locale && {
423+
locale: sql.raw(`"${subQueryAlias}".${selectFields._locale.name}`),
424+
}),
425+
}),
426+
})
427+
.from(sql`${subQuery}`)}`.as(columnName)
498428

499429
break
500430
}

packages/drizzle/src/queries/buildAndOrConditions.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export function buildAndOrConditions({
1212
joins,
1313
locale,
1414
selectFields,
15+
selectLocale,
1516
tableName,
1617
where,
1718
}: {
@@ -22,6 +23,7 @@ export function buildAndOrConditions({
2223
joins: BuildQueryJoinAliases
2324
locale?: string
2425
selectFields: Record<string, GenericColumn>
26+
selectLocale?: boolean
2527
tableName: string
2628
where: Where[]
2729
}): SQL[] {
@@ -38,6 +40,7 @@ export function buildAndOrConditions({
3840
joins,
3941
locale,
4042
selectFields,
43+
selectLocale,
4144
tableName,
4245
where: condition,
4346
})

packages/drizzle/src/queries/buildQuery.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ type BuildQueryArgs = {
1818
fields: FlattenedField[]
1919
joins?: BuildQueryJoinAliases
2020
locale?: string
21+
selectLocale?: boolean
2122
sort?: Sort
2223
tableName: string
2324
where: Where
@@ -37,6 +38,7 @@ const buildQuery = function buildQuery({
3738
fields,
3839
joins = [],
3940
locale,
41+
selectLocale,
4042
sort,
4143
tableName,
4244
where: incomingWhere,
@@ -64,6 +66,7 @@ const buildQuery = function buildQuery({
6466
joins,
6567
locale,
6668
selectFields,
69+
selectLocale,
6770
tableName,
6871
where: incomingWhere,
6972
})

0 commit comments

Comments
 (0)