Skip to content

Commit

Permalink
feat: join field support relationships inside arrays (#9773)
Browse files Browse the repository at this point in the history
### 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>
  • Loading branch information
3 people authored Dec 17, 2024
1 parent eb037a0 commit b0b2fc6
Show file tree
Hide file tree
Showing 17 changed files with 291 additions and 142 deletions.
4 changes: 2 additions & 2 deletions docs/fields/join.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,8 @@ powerful Admin UI.
|------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **`name`** \* | To be used as the property name when retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`collection`** \* | The `slug`s having the relationship field. |
| **`on`** \* | The name of the relationship or upload field that relates to the collection document. Use dot notation for nested paths, like 'myGroup.relationName'. |
| **`where`** \* | A `Where` query to hide related documents from appearing. Will be merged with any `where` specified in the request. |
| **`on`** \* | The name of the relationship or upload field that relates to the collection document. Use dot notation for nested paths, like 'myGroup.relationName'. |
| **`where`** | A `Where` query to hide related documents from appearing. Will be merged with any `where` specified in the request. |
| **`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). |
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
Expand Down
4 changes: 1 addition & 3 deletions packages/drizzle/src/find/chainMethods.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import type { QueryPromise } from 'drizzle-orm'

export type ChainedMethods = {
args: unknown[]
method: string
Expand All @@ -10,7 +8,7 @@ export type ChainedMethods = {
* @param methods
* @param query
*/
const chainMethods = <T>({ methods, query }): QueryPromise<T> => {
const chainMethods = <T>({ methods, query }: { methods: ChainedMethods; query: T }): T => {
return methods.reduce((query, { args, method }) => {
return query[method](...args)
}, query)
Expand Down
1 change: 1 addition & 0 deletions packages/drizzle/src/find/findMany.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export const findMany = async function find({
fields,
joinQuery,
joins,
locale,
select,
tableName,
versions,
Expand Down
164 changes: 47 additions & 117 deletions packages/drizzle/src/find/traverseFields.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import type { LibSQLDatabase } from 'drizzle-orm/libsql'
import type { FlattenedField, JoinQuery, SelectMode, SelectType } from 'payload'
import type { FlattenedField, JoinQuery, SelectMode, SelectType, Where } from 'payload'

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

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

import buildQuery from '../queries/buildQuery.js'
import { jsonAggBuildObject } from '../utilities/json.js'
import { rawConstraint } from '../utilities/rawConstraint.js'
import { chainMethods } from './chainMethods.js'

type TraverseFieldArgs = {
Expand Down Expand Up @@ -145,6 +147,7 @@ export const traverseFields = ({
depth,
fields: field.flattenedFields,
joinQuery,
locale,
path: '',
select: typeof arraySelect === 'object' ? arraySelect : undefined,
selectMode,
Expand Down Expand Up @@ -254,6 +257,7 @@ export const traverseFields = ({
depth,
fields: block.flattenedFields,
joinQuery,
locale,
path: '',
select: typeof blockSelect === 'object' ? blockSelect : undefined,
selectMode: blockSelectMode,
Expand Down Expand Up @@ -294,6 +298,7 @@ export const traverseFields = ({
fields: field.flattenedFields,
joinQuery,
joins,
locale,
path: `${path}${field.name}_`,
select: typeof fieldSelect === 'object' ? fieldSelect : undefined,
selectAllOnCurrentLevel:
Expand Down Expand Up @@ -348,92 +353,37 @@ export const traverseFields = ({

const joins: BuildQueryJoinAliases = []

const buildQueryResult = buildQuery({
adapter,
fields,
joins,
locale,
sort,
tableName: joinCollectionTableName,
where,
})

let subQueryWhere = buildQueryResult.where
const orderBy = buildQueryResult.orderBy

let joinLocalesCollectionTableName: string | undefined

const currentIDColumn = versions
? adapter.tables[currentTableName].parent
: adapter.tables[currentTableName].id

// Handle hasMany _rels table
if (field.hasMany) {
const joinRelsCollectionTableName = `${joinCollectionTableName}${adapter.relationshipsSuffix}`

if (field.localized) {
joinLocalesCollectionTableName = joinRelsCollectionTableName
}

let columnReferenceToCurrentID: string

if (versions) {
columnReferenceToCurrentID = `${topLevelTableName
.replace('_', '')
.replace(new RegExp(`${adapter.versionsSuffix}$`), '')}_id`
} else {
columnReferenceToCurrentID = `${topLevelTableName}_id`
}

joins.push({
type: 'innerJoin',
condition: and(
eq(
adapter.tables[joinRelsCollectionTableName].parent,
adapter.tables[joinCollectionTableName].id,
),
eq(
sql.raw(`"${joinRelsCollectionTableName}"."${columnReferenceToCurrentID}"`),
currentIDColumn,
),
eq(adapter.tables[joinRelsCollectionTableName].path, field.on),
),
table: adapter.tables[joinRelsCollectionTableName],
})
} else {
// Handle localized without hasMany

const foreignColumn = field.on.replaceAll('.', '_')

if (field.localized) {
joinLocalesCollectionTableName = `${joinCollectionTableName}${adapter.localesSuffix}`

joins.push({
type: 'innerJoin',
condition: and(
eq(
adapter.tables[joinLocalesCollectionTableName]._parentID,
adapter.tables[joinCollectionTableName].id,
),
eq(adapter.tables[joinLocalesCollectionTableName][foreignColumn], currentIDColumn),
),
table: adapter.tables[joinLocalesCollectionTableName],
})
// Handle without localized and without hasMany, just a condition append to where. With localized the inner join handles eq.
} else {
const constraint = eq(
adapter.tables[joinCollectionTableName][foreignColumn],
currentIDColumn,
)
let joinQueryWhere: Where = {
[field.on]: {
equals: rawConstraint(currentIDColumn),
},
}

if (subQueryWhere) {
subQueryWhere = and(subQueryWhere, constraint)
} else {
subQueryWhere = constraint
}
if (where) {
joinQueryWhere = {
and: [joinQueryWhere, where],
}
}

const {
orderBy,
selectFields,
where: subQueryWhere,
} = buildQuery({
adapter,
fields,
joins,
locale,
selectLocale: true,
sort,
tableName: joinCollectionTableName,
where: joinQueryWhere,
})

const chainedMethods: ChainedMethods = []

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

const db = adapter.drizzle as LibSQLDatabase

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

const subQueryAlias = `${columnName}_alias`

const subQuery = chainMethods({
methods: chainedMethods,
query: db
.select({
id: adapter.tables[joinCollectionTableName].id,
...(joinLocalesCollectionTableName && {
locale:
adapter.tables[joinLocalesCollectionTableName].locale ||
adapter.tables[joinLocalesCollectionTableName]._locale,
}),
})
.select(selectFields as any)
.from(adapter.tables[joinCollectionTableName])
.where(subQueryWhere)
.orderBy(() => orderBy.map(({ column, order }) => order(column))),
})

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

const jsonObjectSelect = field.localized
? sql.raw(
`'_parentID', "id", '_locale', "${adapter.tables[joinLocalesCollectionTableName].locale ? 'locale' : '_locale'}"`,
)
: sql.raw(`'id', "id"`)

if (adapter.name === 'sqlite') {
currentArgs.extras[columnName] = sql`
COALESCE((
SELECT json_group_array(json_object(${jsonObjectSelect}))
FROM (
${subQuery}
) AS ${sql.raw(`${columnName}_sub`)}
), '[]')
`.as(columnName)
} else {
currentArgs.extras[columnName] = sql`
COALESCE((
SELECT json_agg(json_build_object(${jsonObjectSelect}))
FROM (
${subQuery}
) AS ${sql.raw(`${columnName}_sub`)}
), '[]'::json)
`.as(columnName)
}
}).as(subQueryAlias)

currentArgs.extras[columnName] = sql`${db
.select({
result: jsonAggBuildObject(adapter, {
id: sql.raw(`"${subQueryAlias}".id`),
...(selectFields._locale && {
locale: sql.raw(`"${subQueryAlias}".${selectFields._locale.name}`),
}),
}),
})
.from(sql`${subQuery}`)}`.as(columnName)

break
}
Expand Down
3 changes: 3 additions & 0 deletions packages/drizzle/src/queries/buildAndOrConditions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export function buildAndOrConditions({
joins,
locale,
selectFields,
selectLocale,
tableName,
where,
}: {
Expand All @@ -22,6 +23,7 @@ export function buildAndOrConditions({
joins: BuildQueryJoinAliases
locale?: string
selectFields: Record<string, GenericColumn>
selectLocale?: boolean
tableName: string
where: Where[]
}): SQL[] {
Expand All @@ -38,6 +40,7 @@ export function buildAndOrConditions({
joins,
locale,
selectFields,
selectLocale,
tableName,
where: condition,
})
Expand Down
3 changes: 3 additions & 0 deletions packages/drizzle/src/queries/buildQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type BuildQueryArgs = {
fields: FlattenedField[]
joins?: BuildQueryJoinAliases
locale?: string
selectLocale?: boolean
sort?: Sort
tableName: string
where: Where
Expand All @@ -37,6 +38,7 @@ const buildQuery = function buildQuery({
fields,
joins = [],
locale,
selectLocale,
sort,
tableName,
where: incomingWhere,
Expand Down Expand Up @@ -64,6 +66,7 @@ const buildQuery = function buildQuery({
joins,
locale,
selectFields,
selectLocale,
tableName,
where: incomingWhere,
})
Expand Down
Loading

0 comments on commit b0b2fc6

Please sign in to comment.