fix: join field works on collections with versions enabled (#8715)

- Fixes errors with drizzle when building the schema
https://github.com/payloadcms/payload/issues/8680
- Adds `joins` to `db.queryDrafts` to have them when doing `.find` with
`draft: true`
This commit is contained in:
Sasha
2024-10-22 18:05:55 +03:00
committed by GitHub
parent 4c396c720e
commit 8af00f2deb
19 changed files with 343 additions and 198 deletions

View File

@@ -6,12 +6,23 @@ import { combineQueries, flattenWhereToOperators } from 'payload'
import type { MongooseAdapter } from './index.js' import type { MongooseAdapter } from './index.js'
import { buildSortParam } from './queries/buildSortParam.js' import { buildSortParam } from './queries/buildSortParam.js'
import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
import { withSession } from './withSession.js' import { withSession } from './withSession.js'
export const queryDrafts: QueryDrafts = async function queryDrafts( export const queryDrafts: QueryDrafts = async function queryDrafts(
this: MongooseAdapter, this: MongooseAdapter,
{ collection, limit, locale, page, pagination, req = {} as PayloadRequest, sort: sortArg, where }, {
collection,
joins,
limit,
locale,
page,
pagination,
req = {} as PayloadRequest,
sort: sortArg,
where,
},
) { ) {
const VersionModel = this.versions[collection] const VersionModel = this.versions[collection]
const collectionConfig = this.payload.collections[collection].config const collectionConfig = this.payload.collections[collection].config
@@ -89,7 +100,29 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
paginationOptions.options.limit = limit paginationOptions.options.limit = limit
} }
const result = await VersionModel.paginate(versionQuery, paginationOptions) let result
const aggregate = await buildJoinAggregation({
adapter: this,
collection,
collectionConfig,
joins,
limit,
locale,
query: versionQuery,
versions: true,
})
// build join aggregation
if (aggregate) {
result = await VersionModel.aggregatePaginate(
VersionModel.aggregate(aggregate),
paginationOptions,
)
} else {
result = await VersionModel.paginate(versionQuery, paginationOptions)
}
const docs = JSON.parse(JSON.stringify(result.docs)) const docs = JSON.parse(JSON.stringify(result.docs))
return { return {

View File

@@ -15,6 +15,8 @@ type BuildJoinAggregationArgs = {
locale: string locale: string
// the where clause for the top collection // the where clause for the top collection
query?: Where query?: Where
/** whether the query is from drafts */
versions?: boolean
} }
export const buildJoinAggregation = async ({ export const buildJoinAggregation = async ({
@@ -25,6 +27,7 @@ export const buildJoinAggregation = async ({
limit, limit,
locale, locale,
query, query,
versions,
}: BuildJoinAggregationArgs): Promise<PipelineStage[] | undefined> => { }: BuildJoinAggregationArgs): Promise<PipelineStage[] | undefined> => {
if (Object.keys(collectionConfig.joins).length === 0 || joins === false) { if (Object.keys(collectionConfig.joins).length === 0 || joins === false) {
return return
@@ -90,7 +93,7 @@ export const buildJoinAggregation = async ({
if (adapter.payload.config.localization && locale === 'all') { if (adapter.payload.config.localization && locale === 'all') {
adapter.payload.config.localization.localeCodes.forEach((code) => { adapter.payload.config.localization.localeCodes.forEach((code) => {
const as = `${join.schemaPath}${code}` const as = `${versions ? `version.${join.schemaPath}` : join.schemaPath}${code}`
aggregate.push( aggregate.push(
{ {
@@ -98,7 +101,7 @@ export const buildJoinAggregation = async ({
as: `${as}.docs`, as: `${as}.docs`,
foreignField: `${join.field.on}${code}`, foreignField: `${join.field.on}${code}`,
from: slug, from: slug,
localField: '_id', localField: versions ? 'parent' : '_id',
pipeline, pipeline,
}, },
}, },
@@ -131,7 +134,7 @@ export const buildJoinAggregation = async ({
} else { } else {
const localeSuffix = const localeSuffix =
join.field.localized && adapter.payload.config.localization && locale ? `.${locale}` : '' join.field.localized && adapter.payload.config.localization && locale ? `.${locale}` : ''
const as = `${join.schemaPath}${localeSuffix}` const as = `${versions ? `version.${join.schemaPath}` : join.schemaPath}${localeSuffix}`
aggregate.push( aggregate.push(
{ {
@@ -139,7 +142,7 @@ export const buildJoinAggregation = async ({
as: `${as}.docs`, as: `${as}.docs`,
foreignField: `${join.field.on}${localeSuffix}`, foreignField: `${join.field.on}${localeSuffix}`,
from: slug, from: slug,
localField: '_id', localField: versions ? 'parent' : '_id',
pipeline, pipeline,
}, },
}, },

View File

@@ -70,7 +70,6 @@ export const init: Init = async function init(this: SQLiteAdapter) {
disableNotNull: !!collection?.versions?.drafts, disableNotNull: !!collection?.versions?.drafts,
disableUnique: false, disableUnique: false,
fields: collection.fields, fields: collection.fields,
joins: collection.joins,
locales, locales,
tableName, tableName,
timestamps: collection.timestamps, timestamps: collection.timestamps,

View File

@@ -61,7 +61,6 @@ type Args = {
disableRelsTableUnique?: boolean disableRelsTableUnique?: boolean
disableUnique: boolean disableUnique: boolean
fields: Field[] fields: Field[]
joins?: SanitizedJoins
locales?: [string, ...string[]] locales?: [string, ...string[]]
rootRelationships?: Set<string> rootRelationships?: Set<string>
rootRelationsToBuild?: RelationMap rootRelationsToBuild?: RelationMap
@@ -95,7 +94,6 @@ export const buildTable = ({
disableRelsTableUnique, disableRelsTableUnique,
disableUnique = false, disableUnique = false,
fields, fields,
joins,
locales, locales,
rootRelationships, rootRelationships,
rootRelationsToBuild, rootRelationsToBuild,
@@ -144,7 +142,6 @@ export const buildTable = ({
disableUnique, disableUnique,
fields, fields,
indexes, indexes,
joins,
locales, locales,
localesColumns, localesColumns,
localesIndexes, localesIndexes,

View File

@@ -44,7 +44,6 @@ type Args = {
fields: (Field | TabAsField)[] fields: (Field | TabAsField)[]
forceLocalized?: boolean forceLocalized?: boolean
indexes: Record<string, (cols: GenericColumns) => IndexBuilder> indexes: Record<string, (cols: GenericColumns) => IndexBuilder>
joins?: SanitizedJoins
locales: [string, ...string[]] locales: [string, ...string[]]
localesColumns: Record<string, SQLiteColumnBuilder> localesColumns: Record<string, SQLiteColumnBuilder>
localesIndexes: Record<string, (cols: GenericColumns) => IndexBuilder> localesIndexes: Record<string, (cols: GenericColumns) => IndexBuilder>
@@ -84,7 +83,6 @@ export const traverseFields = ({
fields, fields,
forceLocalized, forceLocalized,
indexes, indexes,
joins,
locales, locales,
localesColumns, localesColumns,
localesIndexes, localesIndexes,
@@ -669,7 +667,6 @@ export const traverseFields = ({
fields: field.fields, fields: field.fields,
forceLocalized, forceLocalized,
indexes, indexes,
joins,
locales, locales,
localesColumns, localesColumns,
localesIndexes, localesIndexes,
@@ -725,7 +722,6 @@ export const traverseFields = ({
fields: field.fields, fields: field.fields,
forceLocalized: field.localized, forceLocalized: field.localized,
indexes, indexes,
joins,
locales, locales,
localesColumns, localesColumns,
localesIndexes, localesIndexes,
@@ -782,7 +778,6 @@ export const traverseFields = ({
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })), fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
forceLocalized, forceLocalized,
indexes, indexes,
joins,
locales, locales,
localesColumns, localesColumns,
localesIndexes, localesIndexes,
@@ -839,7 +834,6 @@ export const traverseFields = ({
fields: field.fields, fields: field.fields,
forceLocalized, forceLocalized,
indexes, indexes,
joins,
locales, locales,
localesColumns, localesColumns,
localesIndexes, localesIndexes,
@@ -937,30 +931,6 @@ export const traverseFields = ({
break break
case 'join': {
// fieldName could be 'posts' or 'group_posts'
// using `on` as the key for the relation
const localized = adapter.payload.config.localization && field.localized
const fieldSchemaPath = `${fieldPrefix || ''}${field.name}`
let target: string
const joinConfig = joins[field.collection].find(
({ schemaPath }) => fieldSchemaPath === schemaPath,
)
if (joinConfig.targetField.hasMany) {
target = `${adapter.tableNameMap.get(toSnakeCase(field.collection))}${adapter.relationshipsSuffix}`
} else {
target = `${adapter.tableNameMap.get(toSnakeCase(field.collection))}${localized ? adapter.localesSuffix : ''}`
}
relationsToBuild.set(fieldName, {
type: 'many',
// joins are not localized on the parent table
localized: false,
relationName: field.on.replaceAll('.', '_'),
target,
})
break
}
default: default:
break break
} }

View File

@@ -16,6 +16,7 @@ type BuildFindQueryArgs = {
joins?: BuildQueryJoinAliases joins?: BuildQueryJoinAliases
locale?: string locale?: string
tableName: string tableName: string
versions?: boolean
} }
export type Result = { export type Result = {
@@ -34,6 +35,7 @@ export const buildFindManyArgs = ({
joins = [], joins = [],
locale, locale,
tableName, tableName,
versions,
}: BuildFindQueryArgs): Record<string, unknown> => { }: BuildFindQueryArgs): Record<string, unknown> => {
const result: Result = { const result: Result = {
extras: {}, extras: {},
@@ -97,6 +99,7 @@ export const buildFindManyArgs = ({
tablePath: '', tablePath: '',
topLevelArgs: result, topLevelArgs: result,
topLevelTableName: tableName, topLevelTableName: tableName,
versions,
}) })
return result return result

View File

@@ -14,6 +14,7 @@ type Args = {
adapter: DrizzleAdapter adapter: DrizzleAdapter
fields: Field[] fields: Field[]
tableName: string tableName: string
versions?: boolean
} & Omit<FindArgs, 'collection'> } & Omit<FindArgs, 'collection'>
export const findMany = async function find({ export const findMany = async function find({
@@ -28,6 +29,7 @@ export const findMany = async function find({
skip, skip,
sort, sort,
tableName, tableName,
versions,
where: whereArg, where: whereArg,
}: Args) { }: Args) {
const db = adapter.sessions[await req.transactionID]?.db || adapter.drizzle const db = adapter.sessions[await req.transactionID]?.db || adapter.drizzle
@@ -71,6 +73,7 @@ export const findMany = async function find({
joinQuery, joinQuery,
joins, joins,
tableName, tableName,
versions,
}) })
selectDistinctMethods.push({ args: [offset], method: 'offset' }) selectDistinctMethods.push({ args: [offset], method: 'offset' })

View File

@@ -1,4 +1,3 @@
import type { DBQueryConfig } from 'drizzle-orm'
import type { LibSQLDatabase } from 'drizzle-orm/libsql' import type { LibSQLDatabase } from 'drizzle-orm/libsql'
import type { Field, JoinQuery } from 'payload' import type { Field, JoinQuery } from 'payload'
@@ -26,6 +25,7 @@ type TraverseFieldArgs = {
tablePath: string tablePath: string
topLevelArgs: Record<string, unknown> topLevelArgs: Record<string, unknown>
topLevelTableName: string topLevelTableName: string
versions?: boolean
} }
export const traverseFields = ({ export const traverseFields = ({
@@ -42,6 +42,7 @@ export const traverseFields = ({
tablePath, tablePath,
topLevelArgs, topLevelArgs,
topLevelTableName, topLevelTableName,
versions,
}: TraverseFieldArgs) => { }: TraverseFieldArgs) => {
fields.forEach((field) => { fields.forEach((field) => {
if (fieldIsVirtual(field)) { if (fieldIsVirtual(field)) {
@@ -99,6 +100,7 @@ export const traverseFields = ({
tablePath: tabTablePath, tablePath: tabTablePath,
topLevelArgs, topLevelArgs,
topLevelTableName, topLevelTableName,
versions,
}) })
}) })
@@ -223,6 +225,7 @@ export const traverseFields = ({
tablePath: `${tablePath}${toSnakeCase(field.name)}_`, tablePath: `${tablePath}${toSnakeCase(field.name)}_`,
topLevelArgs, topLevelArgs,
topLevelTableName, topLevelTableName,
versions,
}) })
break break
@@ -233,87 +236,156 @@ export const traverseFields = ({
if (joinQuery === false) { if (joinQuery === false) {
break break
} }
const { const {
limit: limitArg = 10, limit: limitArg = 10,
sort, sort,
where, where,
} = joinQuery[`${path.replaceAll('_', '.')}${field.name}`] || {} } = joinQuery[`${path.replaceAll('_', '.')}${field.name}`] || {}
let limit = limitArg let limit = limitArg
if (limit !== 0) { if (limit !== 0) {
// get an additional document and slice it later to determine if there is a next page // get an additional document and slice it later to determine if there is a next page
limit += 1 limit += 1
} }
const fields = adapter.payload.collections[field.collection].config.fields const fields = adapter.payload.collections[field.collection].config.fields
const joinCollectionTableName = adapter.tableNameMap.get(toSnakeCase(field.collection)) const joinCollectionTableName = adapter.tableNameMap.get(toSnakeCase(field.collection))
let joinTableName = `${adapter.tableNameMap.get(toSnakeCase(field.collection))}${
field.localized && adapter.payload.config.localization ? adapter.localesSuffix : ''
}`
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) { if (field.hasMany) {
const db = adapter.drizzle as LibSQLDatabase const joinRelsCollectionTableName = `${joinCollectionTableName}${adapter.relationshipsSuffix}`
if (field.localized) {
joinTableName = adapter.tableNameMap.get(toSnakeCase(field.collection))
}
const joinTable = `${joinTableName}${adapter.relationshipsSuffix}`
const joins: BuildQueryJoinAliases = [ 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', type: 'innerJoin',
condition: and( condition: and(
eq(adapter.tables[joinTable].parent, adapter.tables[joinTableName].id),
eq( eq(
sql.raw(`"${joinTable}"."${topLevelTableName}_id"`), adapter.tables[joinLocalesCollectionTableName]._parentID,
adapter.tables[currentTableName].id, adapter.tables[joinCollectionTableName].id,
),
eq(
adapter.tables[joinLocalesCollectionTableName][foreignColumn],
currentIDColumn,
), ),
eq(adapter.tables[joinTable].path, field.on),
), ),
table: adapter.tables[joinTable], table: adapter.tables[joinLocalesCollectionTableName],
},
]
const { orderBy, where: subQueryWhere } = buildQuery({
adapter,
fields,
joins,
locale,
sort,
tableName: joinCollectionTableName,
where: {},
})
const chainedMethods: ChainedMethods = []
joins.forEach(({ type, condition, table }) => {
chainedMethods.push({
args: [table, condition],
method: type ?? 'leftJoin',
}) })
// 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 chainedMethods: ChainedMethods = []
joins.forEach(({ type, condition, table }) => {
chainedMethods.push({
args: [table, condition],
method: type ?? 'leftJoin',
}) })
})
const subQuery = chainMethods({ if (limit !== 0) {
methods: chainedMethods, chainedMethods.push({
query: db args: [limit],
.select({ method: 'limit',
id: adapter.tables[joinTableName].id,
...(field.localized && {
locale: adapter.tables[joinTable].locale,
}),
})
.from(adapter.tables[joinTableName])
.where(subQueryWhere)
.orderBy(orderBy.order(orderBy.column))
.limit(limit),
}) })
}
const columnName = `${path.replaceAll('.', '_')}${field.name}` const db = adapter.drizzle as LibSQLDatabase
const jsonObjectSelect = field.localized const subQuery = chainMethods({
? sql.raw(`'_parentID', "id", '_locale', "locale"`) methods: chainedMethods,
: sql.raw(`'id', "id"`) query: db
.select({
id: adapter.tables[joinCollectionTableName].id,
...(joinLocalesCollectionTableName && {
locale:
adapter.tables[joinLocalesCollectionTableName].locale ||
adapter.tables[joinLocalesCollectionTableName]._locale,
}),
})
.from(adapter.tables[joinCollectionTableName])
.where(subQueryWhere)
.orderBy(orderBy.order(orderBy.column)),
})
if (adapter.name === 'sqlite') { const columnName = `${path.replaceAll('.', '_')}${field.name}`
currentArgs.extras[columnName] = sql`
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(( COALESCE((
SELECT json_group_array(json_object(${jsonObjectSelect})) SELECT json_group_array(json_object(${jsonObjectSelect}))
FROM ( FROM (
@@ -321,8 +393,8 @@ export const traverseFields = ({
) AS ${sql.raw(`${columnName}_sub`)} ) AS ${sql.raw(`${columnName}_sub`)}
), '[]') ), '[]')
`.as(columnName) `.as(columnName)
} else { } else {
currentArgs.extras[columnName] = sql` currentArgs.extras[columnName] = sql`
COALESCE(( COALESCE((
SELECT json_agg(json_build_object(${jsonObjectSelect})) SELECT json_agg(json_build_object(${jsonObjectSelect}))
FROM ( FROM (
@@ -330,41 +402,8 @@ export const traverseFields = ({
) AS ${sql.raw(`${columnName}_sub`)} ) AS ${sql.raw(`${columnName}_sub`)}
), '[]'::json) ), '[]'::json)
`.as(columnName) `.as(columnName)
}
break
} }
const selectFields = {}
const withJoin: DBQueryConfig<'many', true, any, any> = {
columns: selectFields,
}
if (limit) {
withJoin.limit = limit
}
if (field.localized) {
withJoin.columns._locale = true
withJoin.columns._parentID = true
} else {
withJoin.columns.id = true
withJoin.columns.parent = true
}
const { orderBy, where: joinWhere } = buildQuery({
adapter,
fields,
joins,
locale,
sort,
tableName: joinTableName,
where,
})
if (joinWhere) {
withJoin.where = () => joinWhere
}
withJoin.orderBy = orderBy.order(orderBy.column)
currentArgs.with[`${path.replaceAll('.', '_')}${field.name}`] = withJoin
break break
} }

View File

@@ -57,7 +57,6 @@ export const init: Init = async function init(this: BasePostgresAdapter) {
disableNotNull: !!collection?.versions?.drafts, disableNotNull: !!collection?.versions?.drafts,
disableUnique: false, disableUnique: false,
fields: collection.fields, fields: collection.fields,
joins: collection.joins,
tableName, tableName,
timestamps: collection.timestamps, timestamps: collection.timestamps,
versions: false, versions: false,

View File

@@ -50,7 +50,6 @@ type Args = {
disableRelsTableUnique?: boolean disableRelsTableUnique?: boolean
disableUnique: boolean disableUnique: boolean
fields: Field[] fields: Field[]
joins?: SanitizedJoins
rootRelationships?: Set<string> rootRelationships?: Set<string>
rootRelationsToBuild?: RelationMap rootRelationsToBuild?: RelationMap
rootTableIDColType?: string rootTableIDColType?: string
@@ -83,7 +82,6 @@ export const buildTable = ({
disableRelsTableUnique = false, disableRelsTableUnique = false,
disableUnique = false, disableUnique = false,
fields, fields,
joins,
rootRelationships, rootRelationships,
rootRelationsToBuild, rootRelationsToBuild,
rootTableIDColType, rootTableIDColType,
@@ -133,7 +131,6 @@ export const buildTable = ({
disableUnique, disableUnique,
fields, fields,
indexes, indexes,
joins,
localesColumns, localesColumns,
localesIndexes, localesIndexes,
newTableName: tableName, newTableName: tableName,

View File

@@ -50,7 +50,6 @@ type Args = {
fields: (Field | TabAsField)[] fields: (Field | TabAsField)[]
forceLocalized?: boolean forceLocalized?: boolean
indexes: Record<string, (cols: GenericColumns) => IndexBuilder> indexes: Record<string, (cols: GenericColumns) => IndexBuilder>
joins?: SanitizedJoins
localesColumns: Record<string, PgColumnBuilder> localesColumns: Record<string, PgColumnBuilder>
localesIndexes: Record<string, (cols: GenericColumns) => IndexBuilder> localesIndexes: Record<string, (cols: GenericColumns) => IndexBuilder>
newTableName: string newTableName: string
@@ -89,7 +88,6 @@ export const traverseFields = ({
fields, fields,
forceLocalized, forceLocalized,
indexes, indexes,
joins,
localesColumns, localesColumns,
localesIndexes, localesIndexes,
newTableName, newTableName,
@@ -672,7 +670,6 @@ export const traverseFields = ({
fields: field.fields, fields: field.fields,
forceLocalized, forceLocalized,
indexes, indexes,
joins,
localesColumns, localesColumns,
localesIndexes, localesIndexes,
newTableName, newTableName,
@@ -727,7 +724,6 @@ export const traverseFields = ({
fields: field.fields, fields: field.fields,
forceLocalized: field.localized, forceLocalized: field.localized,
indexes, indexes,
joins,
localesColumns, localesColumns,
localesIndexes, localesIndexes,
newTableName: `${parentTableName}_${columnName}`, newTableName: `${parentTableName}_${columnName}`,
@@ -783,7 +779,6 @@ export const traverseFields = ({
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })), fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
forceLocalized, forceLocalized,
indexes, indexes,
joins,
localesColumns, localesColumns,
localesIndexes, localesIndexes,
newTableName, newTableName,
@@ -839,7 +834,6 @@ export const traverseFields = ({
fields: field.fields, fields: field.fields,
forceLocalized, forceLocalized,
indexes, indexes,
joins,
localesColumns, localesColumns,
localesIndexes, localesIndexes,
newTableName, newTableName,
@@ -936,30 +930,6 @@ export const traverseFields = ({
break break
case 'join': {
// fieldName could be 'posts' or 'group_posts'
// using `on` as the key for the relation
const localized = adapter.payload.config.localization && field.localized
const fieldSchemaPath = `${fieldPrefix || ''}${field.name}`
let target: string
const joinConfig = joins[field.collection].find(
({ schemaPath }) => fieldSchemaPath === schemaPath,
)
if (joinConfig.targetField.hasMany) {
target = `${adapter.tableNameMap.get(toSnakeCase(field.collection))}${adapter.relationshipsSuffix}`
} else {
target = `${adapter.tableNameMap.get(toSnakeCase(field.collection))}${localized ? adapter.localesSuffix : ''}`
}
relationsToBuild.set(fieldName, {
type: 'many',
// joins are not localized on the parent table
localized: false,
relationName: field.on.replaceAll('.', '_'),
target,
})
break
}
default: default:
break break
} }

View File

@@ -1,4 +1,4 @@
import type { PayloadRequest, QueryDrafts, SanitizedCollectionConfig } from 'payload' import type { JoinQuery, PayloadRequest, QueryDrafts, SanitizedCollectionConfig } from 'payload'
import { buildVersionCollectionFields, combineQueries } from 'payload' import { buildVersionCollectionFields, combineQueries } from 'payload'
import toSnakeCase from 'to-snake-case' import toSnakeCase from 'to-snake-case'
@@ -9,7 +9,17 @@ import { findMany } from './find/findMany.js'
export const queryDrafts: QueryDrafts = async function queryDrafts( export const queryDrafts: QueryDrafts = async function queryDrafts(
this: DrizzleAdapter, this: DrizzleAdapter,
{ collection, limit, locale, page = 1, pagination, req = {} as PayloadRequest, sort, where }, {
collection,
joins,
limit,
locale,
page = 1,
pagination,
req = {} as PayloadRequest,
sort,
where,
},
) { ) {
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const tableName = this.tableNameMap.get( const tableName = this.tableNameMap.get(
@@ -22,6 +32,7 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
const result = await findMany({ const result = await findMany({
adapter: this, adapter: this,
fields, fields,
joins,
limit, limit,
locale, locale,
page, page,
@@ -29,6 +40,7 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
req, req,
sort, sort,
tableName, tableName,
versions: true,
where: combinedWhere, where: combinedWhere,
}) })

View File

@@ -126,6 +126,7 @@ export const findOperation = async <TSlug extends CollectionSlug>(
result = await payload.db.queryDrafts<DataFromCollectionSlug<TSlug>>({ result = await payload.db.queryDrafts<DataFromCollectionSlug<TSlug>>({
collection: collectionConfig.slug, collection: collectionConfig.slug,
joins: req.payloadAPI === 'GraphQL' ? false : joins,
limit: sanitizedLimit, limit: sanitizedLimit,
locale, locale,
page: sanitizedPage, page: sanitizedPage,

View File

@@ -174,6 +174,7 @@ export type CommitTransaction = (id: number | Promise<number | string> | string)
export type QueryDraftsArgs = { export type QueryDraftsArgs = {
collection: string collection: string
joins?: JoinQuery
limit?: number limit?: number
locale?: string locale?: string
page?: number page?: number

View File

@@ -0,0 +1,20 @@
import type { CollectionConfig } from 'payload'
import { versionsSlug } from './Versions.js'
export const categoriesVersionsSlug = 'categories-versions'
export const CategoriesVersions: CollectionConfig = {
slug: categoriesVersionsSlug,
fields: [
{
name: 'relatedVersions',
type: 'join',
collection: versionsSlug,
on: 'categoryVersion',
},
],
versions: {
drafts: true,
},
}

View File

@@ -0,0 +1,22 @@
import type { CollectionConfig } from 'payload'
export const versionsSlug = 'versions'
export const Versions: CollectionConfig = {
slug: versionsSlug,
fields: [
{
name: 'category',
relationTo: 'categories',
type: 'relationship',
},
{
name: 'categoryVersion',
relationTo: 'categories-versions',
type: 'relationship',
},
],
versions: {
drafts: true,
},
}

View File

@@ -3,8 +3,10 @@ import path from 'path'
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js' import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
import { Categories } from './collections/Categories.js' import { Categories } from './collections/Categories.js'
import { CategoriesVersions } from './collections/CategoriesVersions.js'
import { Posts } from './collections/Posts.js' import { Posts } from './collections/Posts.js'
import { Uploads } from './collections/Uploads.js' import { Uploads } from './collections/Uploads.js'
import { Versions } from './collections/Versions.js'
import { seed } from './seed.js' import { seed } from './seed.js'
import { localizedCategoriesSlug, localizedPostsSlug } from './shared.js' import { localizedCategoriesSlug, localizedPostsSlug } from './shared.js'
@@ -16,6 +18,8 @@ export default buildConfigWithDefaults({
Posts, Posts,
Categories, Categories,
Uploads, Uploads,
Versions,
CategoriesVersions,
{ {
slug: localizedPostsSlug, slug: localizedPostsSlug,
admin: { admin: {

View File

@@ -1,4 +1,4 @@
import type { Payload } from 'payload' import type { Payload, TypeWithID } from 'payload'
import path from 'path' import path from 'path'
import { getFileByPath } from 'payload' import { getFileByPath } from 'payload'
@@ -373,6 +373,42 @@ describe('Joins Field', () => {
}) })
}) })
describe('Joins with versions', () => {
afterEach(async () => {
await payload.delete({ collection: 'versions', where: {} })
await payload.delete({ collection: 'categories-versions', where: {} })
})
it('should populate joins when versions on both sides draft false', async () => {
const category = await payload.create({ collection: 'categories-versions', data: {} })
const version = await payload.create({
collection: 'versions',
data: { categoryVersion: category.id },
})
const res = await payload.find({ collection: 'categories-versions', draft: false })
expect(res.docs[0].relatedVersions.docs[0].id).toBe(version.id)
})
it('should populate joins when versions on both sides draft true payload.db.queryDrafts', async () => {
const category = await payload.create({ collection: 'categories-versions', data: {} })
const version = await payload.create({
collection: 'versions',
data: { categoryVersion: category.id },
})
const res = await payload.find({
collection: 'categories-versions',
draft: true,
})
expect(res.docs[0].relatedVersions.docs[0].id).toBe(version.id)
})
})
describe('REST', () => { describe('REST', () => {
it('should have simple paginate for joins', async () => { it('should have simple paginate for joins', async () => {
const query = { const query = {

View File

@@ -14,6 +14,8 @@ export interface Config {
posts: Post; posts: Post;
categories: Category; categories: Category;
uploads: Upload; uploads: Upload;
versions: Version;
'categories-versions': CategoriesVersion;
'localized-posts': LocalizedPost; 'localized-posts': LocalizedPost;
'localized-categories': LocalizedCategory; 'localized-categories': LocalizedCategory;
users: User; users: User;
@@ -22,7 +24,7 @@ export interface Config {
'payload-migrations': PayloadMigration; 'payload-migrations': PayloadMigration;
}; };
db: { db: {
defaultIDType: number; defaultIDType: string;
}; };
globals: {}; globals: {};
locale: 'en' | 'es'; locale: 'en' | 'es';
@@ -53,15 +55,15 @@ export interface UserAuthOperations {
* via the `definition` "posts". * via the `definition` "posts".
*/ */
export interface Post { export interface Post {
id: number; id: string;
title?: string | null; title?: string | null;
upload?: (number | null) | Upload; upload?: (string | null) | Upload;
category?: (number | null) | Category; category?: (string | null) | Category;
categories?: (number | Category)[] | null; categories?: (string | Category)[] | null;
categoriesLocalized?: (number | Category)[] | null; categoriesLocalized?: (string | Category)[] | null;
group?: { group?: {
category?: (number | null) | Category; category?: (string | null) | Category;
camelCaseCategory?: (number | null) | Category; camelCaseCategory?: (string | null) | Category;
}; };
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
@@ -71,9 +73,9 @@ export interface Post {
* via the `definition` "uploads". * via the `definition` "uploads".
*/ */
export interface Upload { export interface Upload {
id: number; id: string;
relatedPosts?: { relatedPosts?: {
docs?: (number | Post)[] | null; docs?: (string | Post)[] | null;
hasNextPage?: boolean | null; hasNextPage?: boolean | null;
} | null; } | null;
updatedAt: string; updatedAt: string;
@@ -93,41 +95,67 @@ export interface Upload {
* via the `definition` "categories". * via the `definition` "categories".
*/ */
export interface Category { export interface Category {
id: number; id: string;
name?: string | null; name?: string | null;
relatedPosts?: { relatedPosts?: {
docs?: (number | Post)[] | null; docs?: (string | Post)[] | null;
hasNextPage?: boolean | null; hasNextPage?: boolean | null;
} | null; } | null;
hasManyPosts?: { hasManyPosts?: {
docs?: (number | Post)[] | null; docs?: (string | Post)[] | null;
hasNextPage?: boolean | null; hasNextPage?: boolean | null;
} | null; } | null;
hasManyPostsLocalized?: { hasManyPostsLocalized?: {
docs?: (number | Post)[] | null; docs?: (string | Post)[] | null;
hasNextPage?: boolean | null; hasNextPage?: boolean | null;
} | null; } | null;
group?: { group?: {
relatedPosts?: { relatedPosts?: {
docs?: (number | Post)[] | null; docs?: (string | Post)[] | null;
hasNextPage?: boolean | null; hasNextPage?: boolean | null;
} | null; } | null;
camelCasePosts?: { camelCasePosts?: {
docs?: (number | Post)[] | null; docs?: (string | Post)[] | null;
hasNextPage?: boolean | null; hasNextPage?: boolean | null;
} | null; } | null;
}; };
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "versions".
*/
export interface Version {
id: string;
category?: (string | null) | Category;
categoryVersion?: (string | null) | CategoriesVersion;
updatedAt: string;
createdAt: string;
_status?: ('draft' | 'published') | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "categories-versions".
*/
export interface CategoriesVersion {
id: string;
relatedVersions?: {
docs?: (string | Version)[] | null;
hasNextPage?: boolean | null;
} | null;
updatedAt: string;
createdAt: string;
_status?: ('draft' | 'published') | null;
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "localized-posts". * via the `definition` "localized-posts".
*/ */
export interface LocalizedPost { export interface LocalizedPost {
id: number; id: string;
title?: string | null; title?: string | null;
category?: (number | null) | LocalizedCategory; category?: (string | null) | LocalizedCategory;
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
} }
@@ -136,10 +164,10 @@ export interface LocalizedPost {
* via the `definition` "localized-categories". * via the `definition` "localized-categories".
*/ */
export interface LocalizedCategory { export interface LocalizedCategory {
id: number; id: string;
name?: string | null; name?: string | null;
relatedPosts?: { relatedPosts?: {
docs?: (number | LocalizedPost)[] | null; docs?: (string | LocalizedPost)[] | null;
hasNextPage?: boolean | null; hasNextPage?: boolean | null;
} | null; } | null;
updatedAt: string; updatedAt: string;
@@ -150,7 +178,7 @@ export interface LocalizedCategory {
* via the `definition` "users". * via the `definition` "users".
*/ */
export interface User { export interface User {
id: number; id: string;
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
email: string; email: string;
@@ -167,36 +195,44 @@ export interface User {
* via the `definition` "payload-locked-documents". * via the `definition` "payload-locked-documents".
*/ */
export interface PayloadLockedDocument { export interface PayloadLockedDocument {
id: number; id: string;
document?: document?:
| ({ | ({
relationTo: 'posts'; relationTo: 'posts';
value: number | Post; value: string | Post;
} | null) } | null)
| ({ | ({
relationTo: 'categories'; relationTo: 'categories';
value: number | Category; value: string | Category;
} | null) } | null)
| ({ | ({
relationTo: 'uploads'; relationTo: 'uploads';
value: number | Upload; value: string | Upload;
} | null)
| ({
relationTo: 'versions';
value: string | Version;
} | null)
| ({
relationTo: 'categories-versions';
value: string | CategoriesVersion;
} | null) } | null)
| ({ | ({
relationTo: 'localized-posts'; relationTo: 'localized-posts';
value: number | LocalizedPost; value: string | LocalizedPost;
} | null) } | null)
| ({ | ({
relationTo: 'localized-categories'; relationTo: 'localized-categories';
value: number | LocalizedCategory; value: string | LocalizedCategory;
} | null) } | null)
| ({ | ({
relationTo: 'users'; relationTo: 'users';
value: number | User; value: string | User;
} | null); } | null);
globalSlug?: string | null; globalSlug?: string | null;
user: { user: {
relationTo: 'users'; relationTo: 'users';
value: number | User; value: string | User;
}; };
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
@@ -206,10 +242,10 @@ export interface PayloadLockedDocument {
* via the `definition` "payload-preferences". * via the `definition` "payload-preferences".
*/ */
export interface PayloadPreference { export interface PayloadPreference {
id: number; id: string;
user: { user: {
relationTo: 'users'; relationTo: 'users';
value: number | User; value: string | User;
}; };
key?: string | null; key?: string | null;
value?: value?:
@@ -229,7 +265,7 @@ export interface PayloadPreference {
* via the `definition` "payload-migrations". * via the `definition` "payload-migrations".
*/ */
export interface PayloadMigration { export interface PayloadMigration {
id: number; id: string;
name?: string | null; name?: string | null;
batch?: number | null; batch?: number | null;
updatedAt: string; updatedAt: string;