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 https://github.com/payloadcms/payload/pull/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 # https://github.com/payloadcms/payload/discussions/9643 --------- Co-authored-by: Because789 <thomas@because789.ch> Co-authored-by: Sasha <64744993+r1tsuu@users.noreply.github.com>
This commit is contained in:
@@ -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) |
|
| **`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. |
|
| **`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'. |
|
| **`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. |
|
| **`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). |
|
| **`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. |
|
| **`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). |
|
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import type { QueryPromise } from 'drizzle-orm'
|
|
||||||
|
|
||||||
export type ChainedMethods = {
|
export type ChainedMethods = {
|
||||||
args: unknown[]
|
args: unknown[]
|
||||||
method: string
|
method: string
|
||||||
@@ -10,7 +8,7 @@ export type ChainedMethods = {
|
|||||||
* @param methods
|
* @param methods
|
||||||
* @param query
|
* @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 methods.reduce((query, { args, method }) => {
|
||||||
return query[method](...args)
|
return query[method](...args)
|
||||||
}, query)
|
}, query)
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ export const findMany = async function find({
|
|||||||
fields,
|
fields,
|
||||||
joinQuery,
|
joinQuery,
|
||||||
joins,
|
joins,
|
||||||
|
locale,
|
||||||
select,
|
select,
|
||||||
tableName,
|
tableName,
|
||||||
versions,
|
versions,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { LibSQLDatabase } from 'drizzle-orm/libsql'
|
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 { fieldIsVirtual } from 'payload/shared'
|
||||||
import toSnakeCase from 'to-snake-case'
|
import toSnakeCase from 'to-snake-case'
|
||||||
|
|
||||||
@@ -9,6 +9,8 @@ import type { BuildQueryJoinAliases, ChainedMethods, DrizzleAdapter } from '../t
|
|||||||
import type { Result } from './buildFindManyArgs.js'
|
import type { Result } from './buildFindManyArgs.js'
|
||||||
|
|
||||||
import buildQuery from '../queries/buildQuery.js'
|
import buildQuery from '../queries/buildQuery.js'
|
||||||
|
import { jsonAggBuildObject } from '../utilities/json.js'
|
||||||
|
import { rawConstraint } from '../utilities/rawConstraint.js'
|
||||||
import { chainMethods } from './chainMethods.js'
|
import { chainMethods } from './chainMethods.js'
|
||||||
|
|
||||||
type TraverseFieldArgs = {
|
type TraverseFieldArgs = {
|
||||||
@@ -145,6 +147,7 @@ export const traverseFields = ({
|
|||||||
depth,
|
depth,
|
||||||
fields: field.flattenedFields,
|
fields: field.flattenedFields,
|
||||||
joinQuery,
|
joinQuery,
|
||||||
|
locale,
|
||||||
path: '',
|
path: '',
|
||||||
select: typeof arraySelect === 'object' ? arraySelect : undefined,
|
select: typeof arraySelect === 'object' ? arraySelect : undefined,
|
||||||
selectMode,
|
selectMode,
|
||||||
@@ -254,6 +257,7 @@ export const traverseFields = ({
|
|||||||
depth,
|
depth,
|
||||||
fields: block.flattenedFields,
|
fields: block.flattenedFields,
|
||||||
joinQuery,
|
joinQuery,
|
||||||
|
locale,
|
||||||
path: '',
|
path: '',
|
||||||
select: typeof blockSelect === 'object' ? blockSelect : undefined,
|
select: typeof blockSelect === 'object' ? blockSelect : undefined,
|
||||||
selectMode: blockSelectMode,
|
selectMode: blockSelectMode,
|
||||||
@@ -294,6 +298,7 @@ export const traverseFields = ({
|
|||||||
fields: field.flattenedFields,
|
fields: field.flattenedFields,
|
||||||
joinQuery,
|
joinQuery,
|
||||||
joins,
|
joins,
|
||||||
|
locale,
|
||||||
path: `${path}${field.name}_`,
|
path: `${path}${field.name}_`,
|
||||||
select: typeof fieldSelect === 'object' ? fieldSelect : undefined,
|
select: typeof fieldSelect === 'object' ? fieldSelect : undefined,
|
||||||
selectAllOnCurrentLevel:
|
selectAllOnCurrentLevel:
|
||||||
@@ -348,92 +353,37 @@ export const traverseFields = ({
|
|||||||
|
|
||||||
const joins: BuildQueryJoinAliases = []
|
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
|
const currentIDColumn = versions
|
||||||
? adapter.tables[currentTableName].parent
|
? adapter.tables[currentTableName].parent
|
||||||
: adapter.tables[currentTableName].id
|
: adapter.tables[currentTableName].id
|
||||||
|
|
||||||
// Handle hasMany _rels table
|
let joinQueryWhere: Where = {
|
||||||
if (field.hasMany) {
|
[field.on]: {
|
||||||
const joinRelsCollectionTableName = `${joinCollectionTableName}${adapter.relationshipsSuffix}`
|
equals: rawConstraint(currentIDColumn),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
if (field.localized) {
|
if (where) {
|
||||||
joinLocalesCollectionTableName = joinRelsCollectionTableName
|
joinQueryWhere = {
|
||||||
}
|
and: [joinQueryWhere, where],
|
||||||
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (subQueryWhere) {
|
|
||||||
subQueryWhere = and(subQueryWhere, constraint)
|
|
||||||
} else {
|
|
||||||
subQueryWhere = constraint
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
orderBy,
|
||||||
|
selectFields,
|
||||||
|
where: subQueryWhere,
|
||||||
|
} = buildQuery({
|
||||||
|
adapter,
|
||||||
|
fields,
|
||||||
|
joins,
|
||||||
|
locale,
|
||||||
|
selectLocale: true,
|
||||||
|
sort,
|
||||||
|
tableName: joinCollectionTableName,
|
||||||
|
where: joinQueryWhere,
|
||||||
|
})
|
||||||
|
|
||||||
const chainedMethods: ChainedMethods = []
|
const chainedMethods: ChainedMethods = []
|
||||||
|
|
||||||
joins.forEach(({ type, condition, table }) => {
|
joins.forEach(({ type, condition, table }) => {
|
||||||
@@ -452,49 +402,29 @@ export const traverseFields = ({
|
|||||||
|
|
||||||
const db = adapter.drizzle as LibSQLDatabase
|
const db = adapter.drizzle as LibSQLDatabase
|
||||||
|
|
||||||
|
const columnName = `${path.replaceAll('.', '_')}${field.name}`
|
||||||
|
|
||||||
|
const subQueryAlias = `${columnName}_alias`
|
||||||
|
|
||||||
const subQuery = chainMethods({
|
const subQuery = chainMethods({
|
||||||
methods: chainedMethods,
|
methods: chainedMethods,
|
||||||
query: db
|
query: db
|
||||||
.select({
|
.select(selectFields as any)
|
||||||
id: adapter.tables[joinCollectionTableName].id,
|
|
||||||
...(joinLocalesCollectionTableName && {
|
|
||||||
locale:
|
|
||||||
adapter.tables[joinLocalesCollectionTableName].locale ||
|
|
||||||
adapter.tables[joinLocalesCollectionTableName]._locale,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.from(adapter.tables[joinCollectionTableName])
|
.from(adapter.tables[joinCollectionTableName])
|
||||||
.where(subQueryWhere)
|
.where(subQueryWhere)
|
||||||
.orderBy(() => orderBy.map(({ column, order }) => order(column))),
|
.orderBy(() => orderBy.map(({ column, order }) => order(column))),
|
||||||
})
|
}).as(subQueryAlias)
|
||||||
|
|
||||||
const columnName = `${path.replaceAll('.', '_')}${field.name}`
|
currentArgs.extras[columnName] = sql`${db
|
||||||
|
.select({
|
||||||
const jsonObjectSelect = field.localized
|
result: jsonAggBuildObject(adapter, {
|
||||||
? sql.raw(
|
id: sql.raw(`"${subQueryAlias}".id`),
|
||||||
`'_parentID', "id", '_locale', "${adapter.tables[joinLocalesCollectionTableName].locale ? 'locale' : '_locale'}"`,
|
...(selectFields._locale && {
|
||||||
)
|
locale: sql.raw(`"${subQueryAlias}".${selectFields._locale.name}`),
|
||||||
: sql.raw(`'id', "id"`)
|
}),
|
||||||
|
}),
|
||||||
if (adapter.name === 'sqlite') {
|
})
|
||||||
currentArgs.extras[columnName] = sql`
|
.from(sql`${subQuery}`)}`.as(columnName)
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export function buildAndOrConditions({
|
|||||||
joins,
|
joins,
|
||||||
locale,
|
locale,
|
||||||
selectFields,
|
selectFields,
|
||||||
|
selectLocale,
|
||||||
tableName,
|
tableName,
|
||||||
where,
|
where,
|
||||||
}: {
|
}: {
|
||||||
@@ -22,6 +23,7 @@ export function buildAndOrConditions({
|
|||||||
joins: BuildQueryJoinAliases
|
joins: BuildQueryJoinAliases
|
||||||
locale?: string
|
locale?: string
|
||||||
selectFields: Record<string, GenericColumn>
|
selectFields: Record<string, GenericColumn>
|
||||||
|
selectLocale?: boolean
|
||||||
tableName: string
|
tableName: string
|
||||||
where: Where[]
|
where: Where[]
|
||||||
}): SQL[] {
|
}): SQL[] {
|
||||||
@@ -38,6 +40,7 @@ export function buildAndOrConditions({
|
|||||||
joins,
|
joins,
|
||||||
locale,
|
locale,
|
||||||
selectFields,
|
selectFields,
|
||||||
|
selectLocale,
|
||||||
tableName,
|
tableName,
|
||||||
where: condition,
|
where: condition,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ type BuildQueryArgs = {
|
|||||||
fields: FlattenedField[]
|
fields: FlattenedField[]
|
||||||
joins?: BuildQueryJoinAliases
|
joins?: BuildQueryJoinAliases
|
||||||
locale?: string
|
locale?: string
|
||||||
|
selectLocale?: boolean
|
||||||
sort?: Sort
|
sort?: Sort
|
||||||
tableName: string
|
tableName: string
|
||||||
where: Where
|
where: Where
|
||||||
@@ -37,6 +38,7 @@ const buildQuery = function buildQuery({
|
|||||||
fields,
|
fields,
|
||||||
joins = [],
|
joins = [],
|
||||||
locale,
|
locale,
|
||||||
|
selectLocale,
|
||||||
sort,
|
sort,
|
||||||
tableName,
|
tableName,
|
||||||
where: incomingWhere,
|
where: incomingWhere,
|
||||||
@@ -64,6 +66,7 @@ const buildQuery = function buildQuery({
|
|||||||
joins,
|
joins,
|
||||||
locale,
|
locale,
|
||||||
selectFields,
|
selectFields,
|
||||||
|
selectLocale,
|
||||||
tableName,
|
tableName,
|
||||||
where: incomingWhere,
|
where: incomingWhere,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ type Args = {
|
|||||||
pathSegments: string[]
|
pathSegments: string[]
|
||||||
rootTableName?: string
|
rootTableName?: string
|
||||||
selectFields: Record<string, GenericColumn>
|
selectFields: Record<string, GenericColumn>
|
||||||
|
selectLocale?: boolean
|
||||||
tableName: string
|
tableName: string
|
||||||
/**
|
/**
|
||||||
* If creating a new table name for arrays and blocks, this suffix should be appended to the table name
|
* If creating a new table name for arrays and blocks, this suffix should be appended to the table name
|
||||||
@@ -77,6 +78,7 @@ export const getTableColumnFromPath = ({
|
|||||||
pathSegments: incomingSegments,
|
pathSegments: incomingSegments,
|
||||||
rootTableName: incomingRootTableName,
|
rootTableName: incomingRootTableName,
|
||||||
selectFields,
|
selectFields,
|
||||||
|
selectLocale,
|
||||||
tableName,
|
tableName,
|
||||||
tableNameSuffix = '',
|
tableNameSuffix = '',
|
||||||
value,
|
value,
|
||||||
@@ -130,6 +132,10 @@ export const getTableColumnFromPath = ({
|
|||||||
if (locale && field.localized && adapter.payload.config.localization) {
|
if (locale && field.localized && adapter.payload.config.localization) {
|
||||||
const conditions = [eq(arrayParentTable.id, adapter.tables[newTableName]._parentID)]
|
const conditions = [eq(arrayParentTable.id, adapter.tables[newTableName]._parentID)]
|
||||||
|
|
||||||
|
if (selectLocale) {
|
||||||
|
selectFields._locale = adapter.tables[newTableName]._locale
|
||||||
|
}
|
||||||
|
|
||||||
if (locale !== 'all') {
|
if (locale !== 'all') {
|
||||||
conditions.push(eq(adapter.tables[newTableName]._locale, locale))
|
conditions.push(eq(adapter.tables[newTableName]._locale, locale))
|
||||||
}
|
}
|
||||||
@@ -156,6 +162,7 @@ export const getTableColumnFromPath = ({
|
|||||||
pathSegments: pathSegments.slice(1),
|
pathSegments: pathSegments.slice(1),
|
||||||
rootTableName,
|
rootTableName,
|
||||||
selectFields,
|
selectFields,
|
||||||
|
selectLocale,
|
||||||
tableName: newTableName,
|
tableName: newTableName,
|
||||||
value,
|
value,
|
||||||
})
|
})
|
||||||
@@ -213,6 +220,7 @@ export const getTableColumnFromPath = ({
|
|||||||
pathSegments: pathSegments.slice(1),
|
pathSegments: pathSegments.slice(1),
|
||||||
rootTableName,
|
rootTableName,
|
||||||
selectFields: blockSelectFields,
|
selectFields: blockSelectFields,
|
||||||
|
selectLocale,
|
||||||
tableName: newTableName,
|
tableName: newTableName,
|
||||||
value,
|
value,
|
||||||
})
|
})
|
||||||
@@ -294,6 +302,7 @@ export const getTableColumnFromPath = ({
|
|||||||
pathSegments: pathSegments.slice(1),
|
pathSegments: pathSegments.slice(1),
|
||||||
rootTableName,
|
rootTableName,
|
||||||
selectFields,
|
selectFields,
|
||||||
|
selectLocale,
|
||||||
tableName: newTableName,
|
tableName: newTableName,
|
||||||
tableNameSuffix: `${tableNameSuffix}${toSnakeCase(field.name)}_`,
|
tableNameSuffix: `${tableNameSuffix}${toSnakeCase(field.name)}_`,
|
||||||
value,
|
value,
|
||||||
@@ -347,6 +356,7 @@ export const getTableColumnFromPath = ({
|
|||||||
case 'relationship':
|
case 'relationship':
|
||||||
case 'upload': {
|
case 'upload': {
|
||||||
const newCollectionPath = pathSegments.slice(1).join('.')
|
const newCollectionPath = pathSegments.slice(1).join('.')
|
||||||
|
|
||||||
if (Array.isArray(field.relationTo) || field.hasMany) {
|
if (Array.isArray(field.relationTo) || field.hasMany) {
|
||||||
let relationshipFields
|
let relationshipFields
|
||||||
const relationTableName = `${rootTableName}${adapter.relationshipsSuffix}`
|
const relationTableName = `${rootTableName}${adapter.relationshipsSuffix}`
|
||||||
@@ -355,6 +365,10 @@ export const getTableColumnFromPath = ({
|
|||||||
newAliasTableName: aliasRelationshipTableName,
|
newAliasTableName: aliasRelationshipTableName,
|
||||||
} = getTableAlias({ adapter, tableName: relationTableName })
|
} = getTableAlias({ adapter, tableName: relationTableName })
|
||||||
|
|
||||||
|
if (selectLocale && field.localized && adapter.payload.config.localization) {
|
||||||
|
selectFields._locale = aliasRelationshipTable.locale
|
||||||
|
}
|
||||||
|
|
||||||
// Join in the relationships table
|
// Join in the relationships table
|
||||||
if (locale && field.localized && adapter.payload.config.localization) {
|
if (locale && field.localized && adapter.payload.config.localization) {
|
||||||
const conditions = [
|
const conditions = [
|
||||||
@@ -365,6 +379,7 @@ export const getTableColumnFromPath = ({
|
|||||||
if (locale !== 'all') {
|
if (locale !== 'all') {
|
||||||
conditions.push(eq(aliasRelationshipTable.locale, locale))
|
conditions.push(eq(aliasRelationshipTable.locale, locale))
|
||||||
}
|
}
|
||||||
|
|
||||||
joins.push({
|
joins.push({
|
||||||
condition: and(...conditions),
|
condition: and(...conditions),
|
||||||
table: aliasRelationshipTable,
|
table: aliasRelationshipTable,
|
||||||
@@ -523,6 +538,7 @@ export const getTableColumnFromPath = ({
|
|||||||
pathSegments: pathSegments.slice(1),
|
pathSegments: pathSegments.slice(1),
|
||||||
rootTableName: newTableName,
|
rootTableName: newTableName,
|
||||||
selectFields,
|
selectFields,
|
||||||
|
selectLocale,
|
||||||
tableName: newTableName,
|
tableName: newTableName,
|
||||||
value,
|
value,
|
||||||
})
|
})
|
||||||
@@ -545,6 +561,10 @@ export const getTableColumnFromPath = ({
|
|||||||
|
|
||||||
const condtions = [eq(aliasLocaleTable._parentID, adapter.tables[rootTableName].id)]
|
const condtions = [eq(aliasLocaleTable._parentID, adapter.tables[rootTableName].id)]
|
||||||
|
|
||||||
|
if (selectLocale) {
|
||||||
|
selectFields._locale = aliasLocaleTable._locale
|
||||||
|
}
|
||||||
|
|
||||||
if (locale !== 'all') {
|
if (locale !== 'all') {
|
||||||
condtions.push(eq(aliasLocaleTable._locale, locale))
|
condtions.push(eq(aliasLocaleTable._locale, locale))
|
||||||
}
|
}
|
||||||
@@ -643,6 +663,7 @@ export const getTableColumnFromPath = ({
|
|||||||
pathSegments: pathSegments.slice(1),
|
pathSegments: pathSegments.slice(1),
|
||||||
rootTableName,
|
rootTableName,
|
||||||
selectFields,
|
selectFields,
|
||||||
|
selectLocale,
|
||||||
tableName: newTableName,
|
tableName: newTableName,
|
||||||
tableNameSuffix: `${tableNameSuffix}${toSnakeCase(field.name)}_`,
|
tableNameSuffix: `${tableNameSuffix}${toSnakeCase(field.name)}_`,
|
||||||
value,
|
value,
|
||||||
@@ -661,6 +682,7 @@ export const getTableColumnFromPath = ({
|
|||||||
pathSegments: pathSegments.slice(1),
|
pathSegments: pathSegments.slice(1),
|
||||||
rootTableName,
|
rootTableName,
|
||||||
selectFields,
|
selectFields,
|
||||||
|
selectLocale,
|
||||||
tableName: newTableName,
|
tableName: newTableName,
|
||||||
tableNameSuffix,
|
tableNameSuffix,
|
||||||
value,
|
value,
|
||||||
@@ -689,6 +711,10 @@ export const getTableColumnFromPath = ({
|
|||||||
condition = and(condition, eq(newTable._locale, locale))
|
condition = and(condition, eq(newTable._locale, locale))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (selectLocale) {
|
||||||
|
selectFields._locale = newTable._locale
|
||||||
|
}
|
||||||
|
|
||||||
addJoinTable({
|
addJoinTable({
|
||||||
condition,
|
condition,
|
||||||
joins,
|
joins,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ type Args = {
|
|||||||
joins: BuildQueryJoinAliases
|
joins: BuildQueryJoinAliases
|
||||||
locale: string
|
locale: string
|
||||||
selectFields: Record<string, GenericColumn>
|
selectFields: Record<string, GenericColumn>
|
||||||
|
selectLocale?: boolean
|
||||||
tableName: string
|
tableName: string
|
||||||
where: Where
|
where: Where
|
||||||
}
|
}
|
||||||
@@ -29,6 +30,7 @@ export function parseParams({
|
|||||||
joins,
|
joins,
|
||||||
locale,
|
locale,
|
||||||
selectFields,
|
selectFields,
|
||||||
|
selectLocale,
|
||||||
tableName,
|
tableName,
|
||||||
where,
|
where,
|
||||||
}: Args): SQL {
|
}: Args): SQL {
|
||||||
@@ -53,6 +55,7 @@ export function parseParams({
|
|||||||
joins,
|
joins,
|
||||||
locale,
|
locale,
|
||||||
selectFields,
|
selectFields,
|
||||||
|
selectLocale,
|
||||||
tableName,
|
tableName,
|
||||||
where: condition,
|
where: condition,
|
||||||
})
|
})
|
||||||
@@ -86,6 +89,7 @@ export function parseParams({
|
|||||||
locale,
|
locale,
|
||||||
pathSegments: relationOrPath.replace(/__/g, '.').split('.'),
|
pathSegments: relationOrPath.replace(/__/g, '.').split('.'),
|
||||||
selectFields,
|
selectFields,
|
||||||
|
selectLocale,
|
||||||
tableName,
|
tableName,
|
||||||
value: val,
|
value: val,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import type { DrizzleAdapter } from '../types.js'
|
|||||||
|
|
||||||
import { getCollectionIdType } from '../utilities/getCollectionIdType.js'
|
import { getCollectionIdType } from '../utilities/getCollectionIdType.js'
|
||||||
import { isPolymorphicRelationship } from '../utilities/isPolymorphicRelationship.js'
|
import { isPolymorphicRelationship } from '../utilities/isPolymorphicRelationship.js'
|
||||||
|
import { isRawConstraint } from '../utilities/rawConstraint.js'
|
||||||
|
|
||||||
type SanitizeQueryValueArgs = {
|
type SanitizeQueryValueArgs = {
|
||||||
adapter: DrizzleAdapter
|
adapter: DrizzleAdapter
|
||||||
@@ -48,6 +49,9 @@ export const sanitizeQueryValue = ({
|
|||||||
return { operator, value: formattedValue }
|
return { operator, value: formattedValue }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isRawConstraint(val)) {
|
||||||
|
return { operator, value: val.value }
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
(field.type === 'relationship' || field.type === 'upload') &&
|
(field.type === 'relationship' || field.type === 'upload') &&
|
||||||
!relationOrPath.endsWith('relationTo') &&
|
!relationOrPath.endsWith('relationTo') &&
|
||||||
|
|||||||
@@ -389,20 +389,25 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
|||||||
| { docs: unknown[]; hasNextPage: boolean }
|
| { docs: unknown[]; hasNextPage: boolean }
|
||||||
| Record<string, { docs: unknown[]; hasNextPage: boolean }>
|
| Record<string, { docs: unknown[]; hasNextPage: boolean }>
|
||||||
if (Array.isArray(fieldData)) {
|
if (Array.isArray(fieldData)) {
|
||||||
if (field.localized) {
|
if (field.localized && adapter.payload.config.localization) {
|
||||||
fieldResult = fieldData.reduce((joinResult, row) => {
|
fieldResult = fieldData.reduce(
|
||||||
if (typeof row._locale === 'string') {
|
(joinResult, row) => {
|
||||||
if (!joinResult[row._locale]) {
|
if (typeof row.locale === 'string') {
|
||||||
joinResult[row._locale] = {
|
joinResult[row.locale].docs.push(row.id)
|
||||||
docs: [],
|
|
||||||
hasNextPage: false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
joinResult[row._locale].docs.push(row._parentID)
|
|
||||||
}
|
|
||||||
|
|
||||||
return joinResult
|
return joinResult
|
||||||
}, {})
|
},
|
||||||
|
|
||||||
|
// initialize with defaults so empty won't be undefined
|
||||||
|
adapter.payload.config.localization.localeCodes.reduce((acc, code) => {
|
||||||
|
acc[code] = {
|
||||||
|
docs: [],
|
||||||
|
hasNextPage: false,
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, {}),
|
||||||
|
)
|
||||||
Object.keys(fieldResult).forEach((locale) => {
|
Object.keys(fieldResult).forEach((locale) => {
|
||||||
fieldResult[locale].hasNextPage = fieldResult[locale].docs.length > limit
|
fieldResult[locale].hasNextPage = fieldResult[locale].docs.length > limit
|
||||||
fieldResult[locale].docs = fieldResult[locale].docs.slice(0, limit)
|
fieldResult[locale].docs = fieldResult[locale].docs.slice(0, limit)
|
||||||
|
|||||||
44
packages/drizzle/src/utilities/json.ts
Normal file
44
packages/drizzle/src/utilities/json.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import type { Column, SQL } from 'drizzle-orm'
|
||||||
|
|
||||||
|
import { sql } from 'drizzle-orm'
|
||||||
|
|
||||||
|
import type { DrizzleAdapter } from '../types.js'
|
||||||
|
|
||||||
|
export function jsonAgg(adapter: DrizzleAdapter, expression: SQL) {
|
||||||
|
if (adapter.name === 'sqlite') {
|
||||||
|
return sql`coalesce(json_group_array(${expression}), '[]')`
|
||||||
|
}
|
||||||
|
|
||||||
|
return sql`coalesce(json_agg(${expression}), '[]'::json)`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param shape Potential for SQL injections, so you shouldn't allow user-specified key names
|
||||||
|
*/
|
||||||
|
export function jsonBuildObject<T extends Record<string, Column | SQL>>(
|
||||||
|
adapter: DrizzleAdapter,
|
||||||
|
shape: T,
|
||||||
|
) {
|
||||||
|
const chunks: SQL[] = []
|
||||||
|
|
||||||
|
Object.entries(shape).forEach(([key, value]) => {
|
||||||
|
if (chunks.length > 0) {
|
||||||
|
chunks.push(sql.raw(','))
|
||||||
|
}
|
||||||
|
chunks.push(sql.raw(`'${key}',`))
|
||||||
|
chunks.push(sql`${value}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (adapter.name === 'sqlite') {
|
||||||
|
return sql`json_object(${sql.join(chunks)})`
|
||||||
|
}
|
||||||
|
|
||||||
|
return sql`json_build_object(${sql.join(chunks)})`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const jsonAggBuildObject = <T extends Record<string, Column | SQL>>(
|
||||||
|
adapter: DrizzleAdapter,
|
||||||
|
shape: T,
|
||||||
|
) => {
|
||||||
|
return jsonAgg(adapter, jsonBuildObject(adapter, shape))
|
||||||
|
}
|
||||||
13
packages/drizzle/src/utilities/rawConstraint.ts
Normal file
13
packages/drizzle/src/utilities/rawConstraint.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
const RawConstraintSymbol = Symbol('RawConstraint')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* You can use this to inject a raw query to where
|
||||||
|
*/
|
||||||
|
export const rawConstraint = (value: unknown) => ({
|
||||||
|
type: RawConstraintSymbol,
|
||||||
|
value,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const isRawConstraint = (value: unknown): value is ReturnType<typeof rawConstraint> => {
|
||||||
|
return value && typeof value === 'object' && 'type' in value && value.type === RawConstraintSymbol
|
||||||
|
}
|
||||||
@@ -6,13 +6,26 @@ const traverseArrayOrBlocksField = ({
|
|||||||
callback,
|
callback,
|
||||||
data,
|
data,
|
||||||
field,
|
field,
|
||||||
|
fillEmpty,
|
||||||
parentRef,
|
parentRef,
|
||||||
}: {
|
}: {
|
||||||
callback: TraverseFieldsCallback
|
callback: TraverseFieldsCallback
|
||||||
data: Record<string, unknown>[]
|
data: Record<string, unknown>[]
|
||||||
field: ArrayField | BlocksField
|
field: ArrayField | BlocksField
|
||||||
|
fillEmpty: boolean
|
||||||
parentRef?: unknown
|
parentRef?: unknown
|
||||||
}) => {
|
}) => {
|
||||||
|
if (fillEmpty) {
|
||||||
|
if (field.type === 'array') {
|
||||||
|
traverseFields({ callback, fields: field.fields, parentRef })
|
||||||
|
}
|
||||||
|
if (field.type === 'blocks') {
|
||||||
|
field.blocks.forEach((block) => {
|
||||||
|
traverseFields({ callback, fields: block.fields, parentRef })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
for (const ref of data) {
|
for (const ref of data) {
|
||||||
let fields: Field[]
|
let fields: Field[]
|
||||||
if (field.type === 'blocks' && typeof ref?.blockType === 'string') {
|
if (field.type === 'blocks' && typeof ref?.blockType === 'string') {
|
||||||
@@ -23,7 +36,7 @@ const traverseArrayOrBlocksField = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (fields) {
|
if (fields) {
|
||||||
traverseFields({ callback, fields, parentRef, ref })
|
traverseFields({ callback, fields, fillEmpty, parentRef, ref })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -50,7 +63,6 @@ export type TraverseFieldsCallback = (args: {
|
|||||||
type TraverseFieldsArgs = {
|
type TraverseFieldsArgs = {
|
||||||
callback: TraverseFieldsCallback
|
callback: TraverseFieldsCallback
|
||||||
fields: (Field | TabAsField)[]
|
fields: (Field | TabAsField)[]
|
||||||
/** fill empty properties to use this without data */
|
|
||||||
fillEmpty?: boolean
|
fillEmpty?: boolean
|
||||||
parentRef?: Record<string, unknown> | unknown
|
parentRef?: Record<string, unknown> | unknown
|
||||||
ref?: Record<string, unknown> | unknown
|
ref?: Record<string, unknown> | unknown
|
||||||
@@ -61,8 +73,9 @@ type TraverseFieldsArgs = {
|
|||||||
*
|
*
|
||||||
* @param fields
|
* @param fields
|
||||||
* @param callback callback called for each field, discontinue looping if callback returns truthy
|
* @param callback callback called for each field, discontinue looping if callback returns truthy
|
||||||
* @param ref
|
* @param fillEmpty fill empty properties to use this without data
|
||||||
* @param parentRef
|
* @param ref the data or any artifacts assigned in the callback during field recursion
|
||||||
|
* @param parentRef the data or any artifacts assigned in the callback during field recursion one level up
|
||||||
*/
|
*/
|
||||||
export const traverseFields = ({
|
export const traverseFields = ({
|
||||||
callback,
|
callback,
|
||||||
@@ -113,6 +126,7 @@ export const traverseFields = ({
|
|||||||
traverseFields({
|
traverseFields({
|
||||||
callback,
|
callback,
|
||||||
fields: tab.fields,
|
fields: tab.fields,
|
||||||
|
fillEmpty,
|
||||||
parentRef: currentParentRef,
|
parentRef: currentParentRef,
|
||||||
ref: tabRef[key],
|
ref: tabRef[key],
|
||||||
})
|
})
|
||||||
@@ -137,6 +151,7 @@ export const traverseFields = ({
|
|||||||
traverseFields({
|
traverseFields({
|
||||||
callback,
|
callback,
|
||||||
fields: tab.fields,
|
fields: tab.fields,
|
||||||
|
fillEmpty,
|
||||||
parentRef: currentParentRef,
|
parentRef: currentParentRef,
|
||||||
ref: tabRef,
|
ref: tabRef,
|
||||||
})
|
})
|
||||||
@@ -177,6 +192,7 @@ export const traverseFields = ({
|
|||||||
traverseFields({
|
traverseFields({
|
||||||
callback,
|
callback,
|
||||||
fields: field.fields,
|
fields: field.fields,
|
||||||
|
fillEmpty,
|
||||||
parentRef: currentParentRef,
|
parentRef: currentParentRef,
|
||||||
ref: currentRef[key],
|
ref: currentRef[key],
|
||||||
})
|
})
|
||||||
@@ -205,6 +221,7 @@ export const traverseFields = ({
|
|||||||
callback,
|
callback,
|
||||||
data: localeData,
|
data: localeData,
|
||||||
field,
|
field,
|
||||||
|
fillEmpty,
|
||||||
parentRef: currentParentRef,
|
parentRef: currentParentRef,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -213,6 +230,7 @@ export const traverseFields = ({
|
|||||||
callback,
|
callback,
|
||||||
data: currentRef as Record<string, unknown>[],
|
data: currentRef as Record<string, unknown>[],
|
||||||
field,
|
field,
|
||||||
|
fillEmpty,
|
||||||
parentRef: currentParentRef,
|
parentRef: currentParentRef,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -220,6 +238,7 @@ export const traverseFields = ({
|
|||||||
traverseFields({
|
traverseFields({
|
||||||
callback,
|
callback,
|
||||||
fields: field.fields,
|
fields: field.fields,
|
||||||
|
fillEmpty,
|
||||||
parentRef: currentParentRef,
|
parentRef: currentParentRef,
|
||||||
ref: currentRef,
|
ref: currentRef,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,18 +1,78 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import type { JoinFieldClient, JoinFieldClientComponent, PaginatedDocs, Where } from 'payload'
|
import type {
|
||||||
|
ClientField,
|
||||||
|
JoinFieldClient,
|
||||||
|
JoinFieldClientComponent,
|
||||||
|
PaginatedDocs,
|
||||||
|
Where,
|
||||||
|
} from 'payload'
|
||||||
|
|
||||||
|
import ObjectIdImport from 'bson-objectid'
|
||||||
|
import { flattenTopLevelFields } from 'payload/shared'
|
||||||
import React, { useMemo } from 'react'
|
import React, { useMemo } from 'react'
|
||||||
|
|
||||||
import { RelationshipTable } from '../../elements/RelationshipTable/index.js'
|
import { RelationshipTable } from '../../elements/RelationshipTable/index.js'
|
||||||
import { RenderCustomComponent } from '../../elements/RenderCustomComponent/index.js'
|
import { RenderCustomComponent } from '../../elements/RenderCustomComponent/index.js'
|
||||||
import { useField } from '../../forms/useField/index.js'
|
import { useField } from '../../forms/useField/index.js'
|
||||||
import { withCondition } from '../../forms/withCondition/index.js'
|
import { withCondition } from '../../forms/withCondition/index.js'
|
||||||
|
import { useConfig } from '../../providers/Config/index.js'
|
||||||
import { useDocumentInfo } from '../../providers/DocumentInfo/index.js'
|
import { useDocumentInfo } from '../../providers/DocumentInfo/index.js'
|
||||||
import { FieldDescription } from '../FieldDescription/index.js'
|
import { FieldDescription } from '../FieldDescription/index.js'
|
||||||
import { FieldLabel } from '../FieldLabel/index.js'
|
import { FieldLabel } from '../FieldLabel/index.js'
|
||||||
import { fieldBaseClass } from '../index.js'
|
import { fieldBaseClass } from '../index.js'
|
||||||
|
|
||||||
|
const ObjectId = (ObjectIdImport.default ||
|
||||||
|
ObjectIdImport) as unknown as typeof ObjectIdImport.default
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively builds the default data for joined collection
|
||||||
|
*/
|
||||||
|
const getInitialDrawerData = ({
|
||||||
|
docID,
|
||||||
|
fields,
|
||||||
|
segments,
|
||||||
|
}: {
|
||||||
|
docID: number | string
|
||||||
|
fields: ClientField[]
|
||||||
|
segments: string[]
|
||||||
|
}) => {
|
||||||
|
const flattenedFields = flattenTopLevelFields(fields)
|
||||||
|
|
||||||
|
const path = segments[0]
|
||||||
|
|
||||||
|
const field = flattenedFields.find((field) => field.name === path)
|
||||||
|
|
||||||
|
if (field.type === 'relationship' || field.type === 'upload') {
|
||||||
|
return {
|
||||||
|
// TODO: Handle polymorphic https://github.com/payloadcms/payload/pull/9990
|
||||||
|
[field.name]: field.hasMany ? [docID] : docID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextSegments = segments.slice(1, segments.length)
|
||||||
|
|
||||||
|
if (field.type === 'tab' || field.type === 'group') {
|
||||||
|
return {
|
||||||
|
[field.name]: getInitialDrawerData({ docID, fields: field.fields, segments: nextSegments }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.type === 'array') {
|
||||||
|
const initialData = getInitialDrawerData({
|
||||||
|
docID,
|
||||||
|
fields: field.fields,
|
||||||
|
segments: nextSegments,
|
||||||
|
})
|
||||||
|
|
||||||
|
initialData.id = ObjectId().toHexString()
|
||||||
|
|
||||||
|
return {
|
||||||
|
[field.name]: [initialData],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const JoinFieldComponent: JoinFieldClientComponent = (props) => {
|
const JoinFieldComponent: JoinFieldClientComponent = (props) => {
|
||||||
const {
|
const {
|
||||||
field,
|
field,
|
||||||
@@ -29,6 +89,10 @@ const JoinFieldComponent: JoinFieldClientComponent = (props) => {
|
|||||||
|
|
||||||
const { id: docID } = useDocumentInfo()
|
const { id: docID } = useDocumentInfo()
|
||||||
|
|
||||||
|
const {
|
||||||
|
config: { collections },
|
||||||
|
} = useConfig()
|
||||||
|
|
||||||
const { customComponents: { AfterInput, BeforeInput, Description, Label } = {}, value } =
|
const { customComponents: { AfterInput, BeforeInput, Description, Label } = {}, value } =
|
||||||
useField<PaginatedDocs>({
|
useField<PaginatedDocs>({
|
||||||
path,
|
path,
|
||||||
@@ -54,6 +118,16 @@ const JoinFieldComponent: JoinFieldClientComponent = (props) => {
|
|||||||
return where
|
return where
|
||||||
}, [docID, on, field.where])
|
}, [docID, on, field.where])
|
||||||
|
|
||||||
|
const initialDrawerData = useMemo(() => {
|
||||||
|
const relatedCollection = collections.find((collection) => collection.slug === field.collection)
|
||||||
|
|
||||||
|
return getInitialDrawerData({
|
||||||
|
docID,
|
||||||
|
fields: relatedCollection.fields,
|
||||||
|
segments: field.on.split('.'),
|
||||||
|
})
|
||||||
|
}, [collections, field.on, docID, field.collection])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={[fieldBaseClass, 'join'].filter(Boolean).join(' ')}
|
className={[fieldBaseClass, 'join'].filter(Boolean).join(' ')}
|
||||||
@@ -67,9 +141,7 @@ const JoinFieldComponent: JoinFieldClientComponent = (props) => {
|
|||||||
field={field as JoinFieldClient}
|
field={field as JoinFieldClient}
|
||||||
filterOptions={filterOptions}
|
filterOptions={filterOptions}
|
||||||
initialData={docID && value ? value : ({ docs: [] } as PaginatedDocs)}
|
initialData={docID && value ? value : ({ docs: [] } as PaginatedDocs)}
|
||||||
initialDrawerData={{
|
initialDrawerData={initialDrawerData}
|
||||||
[on]: docID,
|
|
||||||
}}
|
|
||||||
Label={
|
Label={
|
||||||
<h4 style={{ margin: 0 }}>
|
<h4 style={{ margin: 0 }}>
|
||||||
{Label || (
|
{Label || (
|
||||||
|
|||||||
@@ -103,6 +103,12 @@ export const Categories: CollectionConfig = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'arrayPosts',
|
||||||
|
type: 'join',
|
||||||
|
collection: 'posts',
|
||||||
|
on: 'array.category',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'singulars',
|
name: 'singulars',
|
||||||
type: 'join',
|
type: 'join',
|
||||||
|
|||||||
@@ -69,5 +69,16 @@ export const Posts: CollectionConfig = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'array',
|
||||||
|
type: 'array',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'category',
|
||||||
|
type: 'relationship',
|
||||||
|
relationTo: categoriesSlug,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ describe('Joins Field', () => {
|
|||||||
category: category.id,
|
category: category.id,
|
||||||
camelCaseCategory: category.id,
|
camelCaseCategory: category.id,
|
||||||
},
|
},
|
||||||
|
array: [{ category: category.id }],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -183,6 +184,15 @@ describe('Joins Field', () => {
|
|||||||
expect(docs[0].group.camelCaseCategory.group.camelCasePosts.docs).toHaveLength(10)
|
expect(docs[0].group.camelCaseCategory.group.camelCasePosts.docs).toHaveLength(10)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should populate joins with array relationships', async () => {
|
||||||
|
const categoryWithPosts = await payload.findByID({
|
||||||
|
id: category.id,
|
||||||
|
collection: categoriesSlug,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(categoryWithPosts.arrayPosts.docs).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
it('should populate uploads in joins', async () => {
|
it('should populate uploads in joins', async () => {
|
||||||
const { docs } = await payload.find({
|
const { docs } = await payload.find({
|
||||||
limit: 1,
|
limit: 1,
|
||||||
|
|||||||
Reference in New Issue
Block a user