fix: respect draft: true when querying docs for the join field (#11763)

Previously, if you were querying a collection that has a join field with
`draft: true`, and the join field's collection also has
`versions.drafts: true` our db adapter would still query the original
SQL table / mongodb collection instead of the versions one which isn't
quite right since we respect `draft: true` when populating relationships
This commit is contained in:
Sasha
2025-03-24 15:49:30 +02:00
committed by GitHub
parent 5f6bb92501
commit 1b2b6a1b15
17 changed files with 165 additions and 29 deletions

View File

@@ -18,6 +18,7 @@ export const find: Find = async function find(
this: MongooseAdapter,
{
collection: collectionSlug,
draftsEnabled,
joins = {},
limit = 0,
locale,
@@ -128,6 +129,7 @@ export const find: Find = async function find(
adapter: this,
collection: collectionSlug,
collectionConfig,
draftsEnabled,
joins,
locale,
query,

View File

@@ -14,7 +14,7 @@ import { transform } from './utilities/transform.js'
export const findOne: FindOne = async function findOne(
this: MongooseAdapter,
{ collection: collectionSlug, joins, locale, req, select, where = {} },
{ collection: collectionSlug, draftsEnabled, joins, locale, req, select, where = {} },
) {
const { collectionConfig, Model } = getCollection({ adapter: this, collectionSlug })
@@ -42,6 +42,7 @@ export const findOne: FindOne = async function findOne(
adapter: this,
collection: collectionSlug,
collectionConfig,
draftsEnabled,
joins,
locale,
projection,

View File

@@ -2,14 +2,19 @@ import type { PipelineStage } from 'mongoose'
import {
APIError,
appendVersionToQueryKey,
buildVersionCollectionFields,
type CollectionSlug,
combineQueries,
type FlattenedField,
getQueryDraftsSort,
type JoinQuery,
type SanitizedCollectionConfig,
} from 'payload'
import { fieldShouldBeLocalized } from 'payload/shared'
import type { MongooseAdapter } from '../index.js'
import type { CollectionModel } from '../types.js'
import { buildQuery } from '../queries/buildQuery.js'
import { buildSortParam } from '../queries/buildSortParam.js'
@@ -19,6 +24,7 @@ type BuildJoinAggregationArgs = {
adapter: MongooseAdapter
collection: CollectionSlug
collectionConfig: SanitizedCollectionConfig
draftsEnabled?: boolean
joins?: JoinQuery
locale?: string
projection?: Record<string, true>
@@ -32,6 +38,7 @@ export const buildJoinAggregation = async ({
adapter,
collection,
collectionConfig,
draftsEnabled,
joins,
locale,
projection,
@@ -262,10 +269,27 @@ export const buildJoinAggregation = async ({
continue
}
const { collectionConfig, Model: JoinModel } = getCollection({
adapter,
collectionSlug: join.field.collection as string,
})
const collectionConfig = adapter.payload.collections[join.field.collection as string]?.config
if (!collectionConfig) {
throw new APIError(
`Collection config for ${join.field.collection.toString()} was not found`,
)
}
let JoinModel: CollectionModel | undefined
const useDrafts = (draftsEnabled || versions) && Boolean(collectionConfig.versions.drafts)
if (useDrafts) {
JoinModel = adapter.versions[collectionConfig.slug]
} else {
JoinModel = adapter.collections[collectionConfig.slug]
}
if (!JoinModel) {
throw new APIError(`Join Model was not found for ${collectionConfig.slug}`)
}
const {
count,
@@ -279,12 +303,16 @@ export const buildJoinAggregation = async ({
throw new Error('Unreachable')
}
const fields = useDrafts
? buildVersionCollectionFields(adapter.payload.config, collectionConfig, true)
: collectionConfig.flattenedFields
const sort = buildSortParam({
adapter,
config: adapter.payload.config,
fields: collectionConfig.flattenedFields,
fields,
locale,
sort: sortJoin,
sort: useDrafts ? getQueryDraftsSort({ collectionConfig, sort: sortJoin }) : sortJoin,
timestamps: true,
})
const sortProperty = Object.keys(sort)[0]!
@@ -293,7 +321,13 @@ export const buildJoinAggregation = async ({
const $match = await JoinModel.buildQuery({
locale,
payload: adapter.payload,
where: whereJoin,
where: useDrafts
? combineQueries(appendVersionToQueryKey(whereJoin), {
latest: {
equals: true,
},
})
: whereJoin,
})
const pipeline: Exclude<PipelineStage, PipelineStage.Merge | PipelineStage.Out>[] = [
@@ -345,6 +379,12 @@ export const buildJoinAggregation = async ({
},
)
let foreignFieldPrefix = ''
if (useDrafts) {
foreignFieldPrefix = 'version.'
}
if (adapter.payload.config.localization && locale === 'all') {
adapter.payload.config.localization.localeCodes.forEach((code) => {
const as = `${versions ? `version.${join.joinPath}` : join.joinPath}${code}`
@@ -353,7 +393,7 @@ export const buildJoinAggregation = async ({
{
$lookup: {
as: `${as}.docs`,
foreignField: `${join.field.on}${code}${polymorphicSuffix}`,
foreignField: `${foreignFieldPrefix}${join.field.on}${code}${polymorphicSuffix}`,
from: JoinModel.collection.name,
localField: versions ? 'parent' : '_id',
pipeline,
@@ -364,7 +404,7 @@ export const buildJoinAggregation = async ({
[`${as}.docs`]: {
$map: {
as: 'doc',
in: '$$doc._id',
in: useDrafts ? `$$doc.parent` : '$$doc._id',
input: `$${as}.docs`,
},
}, // Slicing the docs to match the limit
@@ -387,7 +427,10 @@ export const buildJoinAggregation = async ({
}
if (count) {
addTotalDocsAggregation(as, `${join.field.on}${code}${polymorphicSuffix}`)
addTotalDocsAggregation(
as,
`${foreignFieldPrefix}${join.field.on}${code}${polymorphicSuffix}`,
)
}
})
} else {
@@ -414,7 +457,7 @@ export const buildJoinAggregation = async ({
{
$lookup: {
as: `${as}.docs`,
foreignField,
foreignField: `${foreignFieldPrefix}${foreignField}`,
from: JoinModel.collection.name,
localField: versions ? 'parent' : '_id',
pipeline,
@@ -425,7 +468,7 @@ export const buildJoinAggregation = async ({
[`${as}.docs`]: {
$map: {
as: 'doc',
in: '$$doc._id',
in: useDrafts ? `$$doc.parent` : '$$doc._id',
input: `$${as}.docs`,
},
}, // Slicing the docs to match the limit
@@ -437,7 +480,7 @@ export const buildJoinAggregation = async ({
)
if (count) {
addTotalDocsAggregation(as, foreignField)
addTotalDocsAggregation(as, `${foreignFieldPrefix}${foreignField}`)
}
if (limitJoin > 0) {

View File

@@ -8,7 +8,19 @@ import { findMany } from './find/findMany.js'
export const find: Find = async function find(
this: DrizzleAdapter,
{ collection, joins, limit, locale, page = 1, pagination, req, select, sort: sortArg, where },
{
collection,
draftsEnabled,
joins,
limit,
locale,
page = 1,
pagination,
req,
select,
sort: sortArg,
where,
},
) {
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const sort = sortArg !== undefined && sortArg !== null ? sortArg : collectionConfig.defaultSort
@@ -18,6 +30,7 @@ export const find: Find = async function find(
return findMany({
adapter: this,
collectionSlug: collectionConfig.slug,
draftsEnabled,
fields: collectionConfig.flattenedFields,
joins,
limit,

View File

@@ -11,6 +11,7 @@ type BuildFindQueryArgs = {
adapter: DrizzleAdapter
collectionSlug?: string
depth: number
draftsEnabled?: boolean
fields: FlattenedField[]
joinQuery?: JoinQuery
/**
@@ -35,6 +36,7 @@ export const buildFindManyArgs = ({
adapter,
collectionSlug,
depth,
draftsEnabled,
fields,
joinQuery,
joins = [],
@@ -80,6 +82,7 @@ export const buildFindManyArgs = ({
currentArgs: result,
currentTableName: tableName,
depth,
draftsEnabled,
fields,
joinQuery,
joins,

View File

@@ -22,6 +22,7 @@ type Args = {
export const findMany = async function find({
adapter,
collectionSlug,
draftsEnabled,
fields,
joins: joinQuery,
limit: limitArg,
@@ -74,6 +75,7 @@ export const findMany = async function find({
adapter,
collectionSlug,
depth: 0,
draftsEnabled,
fields,
joinQuery,
joins,

View File

@@ -1,8 +1,18 @@
import type { LibSQLDatabase } from 'drizzle-orm/libsql'
import type { SQLiteSelectBase } from 'drizzle-orm/sqlite-core'
import type { FlattenedField, JoinQuery, SelectMode, SelectType, Where } from 'payload'
import { and, asc, count, desc, eq, or, sql } from 'drizzle-orm'
import {
appendVersionToQueryKey,
buildVersionCollectionFields,
combineQueries,
type FlattenedField,
getQueryDraftsSort,
type JoinQuery,
type SelectMode,
type SelectType,
type Where,
} from 'payload'
import { fieldIsVirtual, fieldShouldBeLocalized } from 'payload/shared'
import toSnakeCase from 'to-snake-case'
@@ -61,6 +71,7 @@ type TraverseFieldArgs = {
currentArgs: Result
currentTableName: string
depth?: number
draftsEnabled?: boolean
fields: FlattenedField[]
joinQuery: JoinQuery
joins?: BuildQueryJoinAliases
@@ -88,6 +99,7 @@ export const traverseFields = ({
currentArgs,
currentTableName,
depth,
draftsEnabled,
fields,
joinQuery = {},
joins,
@@ -193,6 +205,7 @@ export const traverseFields = ({
currentArgs: withArray,
currentTableName: arrayTableName,
depth,
draftsEnabled,
fields: field.flattenedFields,
joinQuery,
locale,
@@ -304,6 +317,7 @@ export const traverseFields = ({
currentArgs: withBlock,
currentTableName: tableName,
depth,
draftsEnabled,
fields: block.flattenedFields,
joinQuery,
locale,
@@ -345,6 +359,7 @@ export const traverseFields = ({
currentArgs,
currentTableName,
depth,
draftsEnabled,
fields: field.flattenedFields,
joinQuery,
joins,
@@ -511,9 +526,23 @@ export const traverseFields = ({
.from(sql`${currentQuery.as(subQueryAlias)}`)
.where(sqlWhere)}`.as(columnName)
} else {
const fields = adapter.payload.collections[field.collection].config.flattenedFields
const useDrafts =
(versions || draftsEnabled) &&
Boolean(adapter.payload.collections[field.collection].config.versions.drafts)
const joinCollectionTableName = adapter.tableNameMap.get(toSnakeCase(field.collection))
const fields = useDrafts
? buildVersionCollectionFields(
adapter.payload.config,
adapter.payload.collections[field.collection].config,
true,
)
: adapter.payload.collections[field.collection].config.flattenedFields
const joinCollectionTableName = adapter.tableNameMap.get(
useDrafts
? `_${toSnakeCase(field.collection)}${adapter.versionsSuffix}`
: toSnakeCase(field.collection),
)
const joins: BuildQueryJoinAliases = []
@@ -546,6 +575,12 @@ export const traverseFields = ({
}
}
if (useDrafts) {
joinQueryWhere = combineQueries(appendVersionToQueryKey(joinQueryWhere), {
latest: { equals: true },
})
}
const columnName = `${path.replaceAll('.', '_')}${field.name}`
const subQueryAlias = `${columnName}_alias`
@@ -567,7 +602,12 @@ export const traverseFields = ({
locale,
parentIsLocalized,
selectLocale: true,
sort,
sort: useDrafts
? getQueryDraftsSort({
collectionConfig: adapter.payload.collections[field.collection].config,
sort,
})
: sort,
tableName: joinCollectionTableName,
where: joinQueryWhere,
})
@@ -610,6 +650,10 @@ export const traverseFields = ({
}
}
if (useDrafts) {
selectFields.parent = newAliasTable.parent
}
const subQuery = chainMethods({
methods: chainedMethods,
query: db
@@ -636,7 +680,7 @@ export const traverseFields = ({
currentArgs.extras[columnName] = sql`${db
.select({
result: jsonAggBuildObject(adapter, {
id: sql.raw(`"${subQueryAlias}".id`),
id: sql.raw(`"${subQueryAlias}".${useDrafts ? 'parent_id' : 'id'}`),
...(selectFields._locale && {
locale: sql.raw(`"${subQueryAlias}".${selectFields._locale.name}`),
}),

View File

@@ -8,7 +8,7 @@ import { findMany } from './find/findMany.js'
export async function findOne<T extends TypeWithID>(
this: DrizzleAdapter,
{ collection, joins, locale, req, select, where }: FindOneArgs,
{ collection, draftsEnabled, joins, locale, req, select, where }: FindOneArgs,
): Promise<T> {
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
@@ -17,6 +17,7 @@ export async function findOne<T extends TypeWithID>(
const { docs } = await findMany({
adapter: this,
collectionSlug: collection,
draftsEnabled,
fields: collectionConfig.flattenedFields,
joins,
limit: 1,

View File

@@ -1,7 +1,6 @@
import type { FlattenedBlock, FlattenedField, JoinQuery, SanitizedConfig } from 'payload'
import { fieldIsVirtual, fieldShouldBeLocalized } from 'payload/shared'
import toSnakeCase from 'to-snake-case'
import type { DrizzleAdapter } from '../../types.js'
import type { BlocksMap } from '../../utilities/createBlocksMap.js'

View File

@@ -23,15 +23,15 @@ export const metadata = {
export const RootLayout = async ({
children,
config: configPromise,
htmlProps = {},
importMap,
serverFunction,
htmlProps = {},
}: {
readonly children: React.ReactNode
readonly config: Promise<SanitizedConfig>
readonly htmlProps?: React.HtmlHTMLAttributes<HTMLHtmlElement>
readonly importMap: ImportMap
readonly serverFunction: ServerFunctionClient
readonly htmlProps?: React.HtmlHTMLAttributes<HTMLHtmlElement>
}) => {
checkDependencies()

View File

@@ -183,6 +183,7 @@ export const findOperation = async <
result = await payload.db.find<DataFromCollectionSlug<TSlug>>({
collection: collectionConfig.slug,
draftsEnabled,
joins: req.payloadAPI === 'GraphQL' ? false : sanitizedJoins,
limit: sanitizedLimit,
locale,

View File

@@ -117,6 +117,7 @@ export const findByIDOperation = async <
const findOneArgs: FindOneArgs = {
collection: collectionConfig.slug,
draftsEnabled: draftEnabled,
joins: req.payloadAPI === 'GraphQL' ? false : sanitizedJoins,
locale,
req: {

View File

@@ -206,6 +206,7 @@ export type QueryDrafts = <T = TypeWithID>(args: QueryDraftsArgs) => Promise<Pag
export type FindOneArgs = {
collection: CollectionSlug
draftsEnabled?: boolean
joins?: JoinQuery
locale?: string
req?: Partial<PayloadRequest>
@@ -217,6 +218,7 @@ export type FindOne = <T extends TypeWithID>(args: FindOneArgs) => Promise<null
export type FindArgs = {
collection: CollectionSlug
draftsEnabled?: boolean
joins?: JoinQuery
/** Setting limit to 1 is equal to the previous Model.findOne(). Setting limit to 0 disables the limit */
limit?: number

View File

@@ -1008,12 +1008,10 @@ export type {
User,
VerifyConfig,
} from './auth/types.js'
export { generateImportMap } from './bin/generateImportMap/index.js'
export type { ImportMap } from './bin/generateImportMap/index.js'
export { genImportMapIterateFields } from './bin/generateImportMap/iterateFields.js'
export {
type ClientCollectionConfig,
createClientCollectionConfig,
@@ -1060,6 +1058,7 @@ export type {
} from './collections/config/types.js'
export type { CompoundIndex } from './collections/config/types.js'
export type { SanitizedCompoundIndex } from './collections/config/types.js'
export { createDataloaderCacheKey, getDataLoader } from './collections/dataloader.js'
export { countOperation } from './collections/operations/count.js'
@@ -1084,6 +1083,7 @@ export {
serverOnlyConfigProperties,
type UnsanitizedClientConfig,
} from './config/client.js'
export { defaults } from './config/defaults.js'
export { sanitizeConfig } from './config/sanitize.js'
export type * from './config/types.js'
@@ -1313,6 +1313,7 @@ export type {
} from './fields/config/types.js'
export { getDefaultValue } from './fields/getDefaultValue.js'
export { traverseFields as afterChangeTraverseFields } from './fields/hooks/afterChange/traverseFields.js'
export { promise as afterReadPromise } from './fields/hooks/afterRead/promise.js'
export { traverseFields as afterReadTraverseFields } from './fields/hooks/afterRead/traverseFields.js'
@@ -1352,7 +1353,6 @@ export type {
UploadFieldValidation,
UsernameFieldValidation,
} from './fields/validations.js'
export {
type ClientGlobalConfig,
createClientGlobalConfig,
@@ -1374,6 +1374,7 @@ export type {
} from './globals/config/types.js'
export { docAccessOperation as docAccessOperationGlobal } from './globals/operations/docAccess.js'
export { findOneOperation } from './globals/operations/findOne.js'
export { findVersionByIDOperation as findVersionByIDOperationGlobal } from './globals/operations/findVersionByID.js'
export { findVersionsOperation as findVersionsOperationGlobal } from './globals/operations/findVersions.js'
@@ -1423,6 +1424,7 @@ export { getFileByPath } from './uploads/getFileByPath.js'
export type * from './uploads/types.js'
export { addDataAndFileToRequest } from './utilities/addDataAndFileToRequest.js'
export { addLocalesToRequestFromData, sanitizeLocales } from './utilities/addLocalesToRequest.js'
export { commitTransaction } from './utilities/commitTransaction.js'
export {
@@ -1490,6 +1492,8 @@ export { buildVersionGlobalFields } from './versions/buildGlobalFields.js'
export { buildVersionCompoundIndexes } from './versions/buildVersionCompoundIndexes.js'
export { versionDefaults } from './versions/defaults.js'
export { deleteCollectionVersions } from './versions/deleteCollectionVersions.js'
export { appendVersionToQueryKey } from './versions/drafts/appendVersionToQueryKey.js'
export { getQueryDraftsSort } from './versions/drafts/getQueryDraftsSort.js'
export { enforceMaxVersions } from './versions/enforceMaxVersions.js'
export { getLatestCollectionVersion } from './versions/getLatestCollectionVersion.js'
export { getLatestGlobalVersion } from './versions/getLatestGlobalVersion.js'

View File

@@ -8,7 +8,6 @@ import type { PayloadRequest, SelectType, Where } from '../../types/index.js'
import { hasWhereAccessResult } from '../../auth/index.js'
import { combineQueries } from '../../database/combineQueries.js'
import { docHasTimestamps } from '../../types/index.js'
import { deepCopyObjectSimple } from '../../utilities/deepCopyObject.js'
import sanitizeInternalFields from '../../utilities/sanitizeInternalFields.js'
import { appendVersionToQueryKey } from './appendVersionToQueryKey.js'
import { getQueryDraftsSelect } from './getQueryDraftsSelect.js'

View File

@@ -190,7 +190,7 @@ export function UploadInput(props: UploadInputProps) {
const populateDocs = React.useCallback<PopulateDocs>(
async (ids, relatedCollectionSlug) => {
if (!ids.length) {
return;
return
}
const query: {

View File

@@ -606,6 +606,27 @@ describe('Joins Field', () => {
expect(res.docs[0].relatedVersions.docs[0].id).toBe(version.id)
})
it('should populate joins with hasMany when on both sides documents are in draft', async () => {
const category = await payload.create({
collection: 'categories-versions',
data: { _status: 'draft' },
draft: true,
})
const version = await payload.create({
collection: 'versions',
data: { _status: 'draft', categoryVersion: category.id },
draft: true,
})
const res = await payload.find({
collection: 'categories-versions',
draft: true,
})
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: {} })