From cae300e8e38e54c8c8704c180f90d2ed517f5dba Mon Sep 17 00:00:00 2001 From: Sasha <64744993+r1tsuu@users.noreply.github.com> Date: Mon, 25 Nov 2024 17:28:07 +0200 Subject: [PATCH] perf: `flattenedFields` collection/global property, remove deep copying in `validateQueryPaths` (#9299) ### What? Improves querying performance of the Local API, reduces copying overhead ### How? Adds `flattenedFields` property for sanitized collection/global config which contains fields in database schema structure. For example, It removes rows / collapsible / unnamed tabs and merges them to the parent `fields`, ignores UI fields, named tabs are added as `type: 'tab'`. This simplifies code in places like Drizzle `transform/traverseFields`. Also, now we can avoid calling `flattenTopLevelFields` which adds some overhead and use `collection.flattenedFields` / `field.flattenedFields`. By refactoring `configToJSONSchema.ts` with `flattenedFields` it also 1. Fixes https://github.com/payloadcms/payload/issues/9467 2. Fixes types for UI fields were generated for `select` Removes this deep copying for each `where` query constraint https://github.com/payloadcms/payload/blob/58ac784425b411fc28c91dbb3e7df06cc26b6320/packages/payload/src/database/queryValidation/validateQueryPaths.ts#L69-L73 which potentially can add overhead if you have a large collection/global config UPD: The overhead is even much more than in the benchmark below if you have Lexical with blocks. Benchmark in `relationships/int.spec.ts`: ```ts const now = Date.now() for (let i = 0; i < 10; i++) { const now = Date.now() for (let i = 0; i < 300; i++) { const query = await payload.find({ collection: 'chained', where: { 'relation.relation.name': { equals: 'third', }, and: [ { 'relation.relation.name': { equals: 'third', }, }, { 'relation.relation.name': { equals: 'third', }, }, { 'relation.relation.name': { equals: 'third', }, }, { 'relation.relation.name': { equals: 'third', }, }, { 'relation.relation.name': { equals: 'third', }, }, { 'relation.relation.name': { equals: 'third', }, }, { 'relation.relation.name': { equals: 'third', }, }, { 'relation.relation.name': { equals: 'third', }, }, { 'relation.relation.name': { equals: 'third', }, }, ], }, }) } payload.logger.info(`#${i + 1} ${Date.now() - now}`) } payload.logger.info(`Total ${Date.now() - now}`) ``` Before: ``` [02:11:48] INFO: #1 3682 [02:11:50] INFO: #2 2199 [02:11:54] INFO: #3 3483 [02:11:56] INFO: #4 2516 [02:11:59] INFO: #5 2467 [02:12:01] INFO: #6 1987 [02:12:03] INFO: #7 1986 [02:12:05] INFO: #8 2375 [02:12:07] INFO: #9 2040 [02:12:09] INFO: #10 1920 [PASS] Relationships > Querying > Nested Querying > should allow querying two levels deep (24667ms) [02:12:09] INFO: Total 24657 ``` After: ``` [02:12:36] INFO: #1 2113 [02:12:38] INFO: #2 1854 [02:12:40] INFO: #3 1700 [02:12:42] INFO: #4 1797 [02:12:44] INFO: #5 2121 [02:12:46] INFO: #6 1774 [02:12:47] INFO: #7 1670 [02:12:49] INFO: #8 1610 [02:12:50] INFO: #9 1596 [02:12:52] INFO: #10 1576 [PASS] Relationships > Querying > Nested Querying > should allow querying two levels deep (17828ms) [02:12:52] INFO: Total 17818 ``` --- packages/db-mongodb/src/deleteOne.ts | 2 +- packages/db-mongodb/src/find.ts | 4 +- packages/db-mongodb/src/findGlobal.ts | 2 +- packages/db-mongodb/src/findGlobalVersions.ts | 1 + packages/db-mongodb/src/findOne.ts | 2 +- packages/db-mongodb/src/findVersions.ts | 4 +- packages/db-mongodb/src/init.ts | 10 +- .../src/queries/buildAndOrConditions.ts | 4 +- packages/db-mongodb/src/queries/buildQuery.ts | 8 +- .../src/queries/buildSearchParams.ts | 8 +- .../db-mongodb/src/queries/buildSortParam.ts | 4 +- .../queries/getLocalizedSortProperty.spec.ts | 156 ++-- .../src/queries/getLocalizedSortProperty.ts | 16 +- .../db-mongodb/src/queries/parseParams.ts | 4 +- .../src/queries/sanitizeQueryValue.ts | 12 +- packages/db-mongodb/src/queryDrafts.ts | 4 +- packages/db-mongodb/src/updateGlobal.ts | 6 +- .../db-mongodb/src/updateGlobalVersion.ts | 13 +- packages/db-mongodb/src/updateOne.ts | 6 +- packages/db-mongodb/src/updateVersion.ts | 10 +- .../src/utilities/buildJoinAggregation.ts | 2 +- .../utilities/buildProjectionFromSelect.ts | 54 +- .../v2-v3/fetchAndResave/index.ts | 4 +- .../v2-v3/fetchAndResave/traverseFields.ts | 67 +- .../src/predefinedMigrations/v2-v3/index.ts | 12 +- .../v2-v3/migrateRelationships.ts | 4 +- .../v2-v3/traverseFields.ts | 40 +- packages/db-sqlite/src/init.ts | 8 +- packages/db-sqlite/src/schema/build.ts | 4 +- packages/db-sqlite/src/schema/idToUUID.ts | 4 +- packages/db-sqlite/src/schema/setColumnID.ts | 9 +- .../db-sqlite/src/schema/traverseFields.ts | 260 +----- .../v2-v3/fetchAndResave/index.ts | 4 +- .../v2-v3/fetchAndResave/traverseFields.ts | 67 +- .../src/predefinedMigrations/v2-v3/index.ts | 12 +- .../v2-v3/migrateRelationships.ts | 4 +- .../v2-v3/traverseFields.ts | 40 +- packages/drizzle/src/count.ts | 2 +- packages/drizzle/src/countGlobalVersions.ts | 2 +- packages/drizzle/src/countVersions.ts | 2 +- packages/drizzle/src/create.ts | 2 +- packages/drizzle/src/createGlobal.ts | 2 +- packages/drizzle/src/createGlobalVersion.ts | 2 +- packages/drizzle/src/createVersion.ts | 2 +- packages/drizzle/src/deleteMany.ts | 2 +- packages/drizzle/src/deleteOne.ts | 6 +- packages/drizzle/src/deleteVersions.ts | 2 +- packages/drizzle/src/find.ts | 2 +- .../drizzle/src/find/buildFindManyArgs.ts | 4 +- packages/drizzle/src/find/findMany.ts | 4 +- packages/drizzle/src/find/traverseFields.ts | 843 ++++++++--------- packages/drizzle/src/findGlobal.ts | 2 +- packages/drizzle/src/findGlobalVersions.ts | 2 +- packages/drizzle/src/findOne.ts | 2 +- packages/drizzle/src/findVersions.ts | 2 +- packages/drizzle/src/postgres/init.ts | 8 +- packages/drizzle/src/postgres/schema/build.ts | 4 +- .../drizzle/src/postgres/schema/idToUUID.ts | 4 +- .../src/postgres/schema/setColumnID.ts | 9 +- .../src/postgres/schema/traverseFields.ts | 258 +----- .../src/queries/buildAndOrConditions.ts | 4 +- packages/drizzle/src/queries/buildOrderBy.ts | 4 +- packages/drizzle/src/queries/buildQuery.ts | 4 +- .../src/queries/getTableColumnFromPath.ts | 107 +-- packages/drizzle/src/queries/parseParams.ts | 4 +- packages/drizzle/src/queryDrafts.ts | 2 +- packages/drizzle/src/transform/read/index.ts | 4 +- .../src/transform/read/traverseFields.ts | 867 +++++++++--------- packages/drizzle/src/transform/write/array.ts | 6 +- .../drizzle/src/transform/write/blocks.ts | 6 +- packages/drizzle/src/transform/write/index.ts | 4 +- .../src/transform/write/traverseFields.ts | 233 ++--- packages/drizzle/src/update.ts | 4 +- packages/drizzle/src/updateGlobal.ts | 2 +- packages/drizzle/src/updateGlobalVersion.ts | 2 +- packages/drizzle/src/updateVersion.ts | 2 +- packages/drizzle/src/upsertRow/types.ts | 4 +- .../payload/src/collections/config/client.ts | 3 +- .../src/collections/config/sanitize.ts | 3 + .../payload/src/collections/config/types.ts | 17 +- .../collections/operations/countVersions.ts | 2 +- .../src/collections/operations/find.ts | 2 +- .../collections/operations/findVersions.ts | 2 +- .../src/collections/operations/update.ts | 2 +- .../payload/src/database/getLocalizedPaths.ts | 33 +- .../src/database/queryValidation/types.ts | 6 +- .../queryValidation/validateQueryPaths.ts | 17 +- .../queryValidation/validateSearchParams.ts | 10 +- packages/payload/src/fields/config/types.ts | 42 +- packages/payload/src/globals/config/client.ts | 3 +- .../payload/src/globals/config/sanitize.ts | 3 + packages/payload/src/globals/config/types.ts | 9 +- .../globals/operations/countGlobalVersions.ts | 2 +- .../src/globals/operations/findVersions.ts | 2 +- packages/payload/src/index.ts | 7 + .../queues/config/generateJobsJSONSchemas.ts | 7 +- .../src/utilities/configToJSONSchema.ts | 138 +-- .../payload/src/utilities/flattenAllFields.ts | 65 ++ .../src/utilities/getEntityPolicies.ts | 2 +- packages/payload/src/versions/baseFields.ts | 4 +- .../src/versions/buildCollectionFields.ts | 14 +- .../payload/src/versions/buildGlobalFields.ts | 14 +- .../src/features/blocks/server/index.ts | 16 +- test/admin/payload-types.ts | 9 +- test/fields/collections/Tabs/index.ts | 1 + test/fields/payload-types.ts | 39 +- 106 files changed, 1542 insertions(+), 2230 deletions(-) create mode 100644 packages/payload/src/utilities/flattenAllFields.ts diff --git a/packages/db-mongodb/src/deleteOne.ts b/packages/db-mongodb/src/deleteOne.ts index 889b4a8e75b..edf9ebb6dc0 100644 --- a/packages/db-mongodb/src/deleteOne.ts +++ b/packages/db-mongodb/src/deleteOne.ts @@ -22,7 +22,7 @@ export const deleteOne: DeleteOne = async function deleteOne( ...options, projection: buildProjectionFromSelect({ adapter: this, - fields: this.payload.collections[collection].config.fields, + fields: this.payload.collections[collection].config.flattenedFields, select, }), }).lean() diff --git a/packages/db-mongodb/src/find.ts b/packages/db-mongodb/src/find.ts index bad9ec4f54a..9295f8829c8 100644 --- a/packages/db-mongodb/src/find.ts +++ b/packages/db-mongodb/src/find.ts @@ -42,7 +42,7 @@ export const find: Find = async function find( if (!hasNearConstraint) { sort = buildSortParam({ config: this.payload.config, - fields: collectionConfig.fields, + fields: collectionConfig.flattenedFields, locale, sort: sortArg || collectionConfig.defaultSort, timestamps: true, @@ -71,7 +71,7 @@ export const find: Find = async function find( if (select) { paginationOptions.projection = buildProjectionFromSelect({ adapter: this, - fields: collectionConfig.fields, + fields: collectionConfig.flattenedFields, select, }) } diff --git a/packages/db-mongodb/src/findGlobal.ts b/packages/db-mongodb/src/findGlobal.ts index a32c8ca729d..c89ef2e2960 100644 --- a/packages/db-mongodb/src/findGlobal.ts +++ b/packages/db-mongodb/src/findGlobal.ts @@ -18,7 +18,7 @@ export const findGlobal: FindGlobal = async function findGlobal( lean: true, select: buildProjectionFromSelect({ adapter: this, - fields: this.payload.globals.config.find((each) => each.slug === slug).fields, + fields: this.payload.globals.config.find((each) => each.slug === slug).flattenedFields, select, }), } diff --git a/packages/db-mongodb/src/findGlobalVersions.ts b/packages/db-mongodb/src/findGlobalVersions.ts index 07cc45e6577..406bac1f6d4 100644 --- a/packages/db-mongodb/src/findGlobalVersions.ts +++ b/packages/db-mongodb/src/findGlobalVersions.ts @@ -29,6 +29,7 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV const versionFields = buildVersionGlobalFields( this.payload.config, this.payload.globals.config.find(({ slug }) => slug === global), + true, ) const options = { ...(await withSession(this, req)), diff --git a/packages/db-mongodb/src/findOne.ts b/packages/db-mongodb/src/findOne.ts index 47f5615b90e..5b82fd9effa 100644 --- a/packages/db-mongodb/src/findOne.ts +++ b/packages/db-mongodb/src/findOne.ts @@ -27,7 +27,7 @@ export const findOne: FindOne = async function findOne( const projection = buildProjectionFromSelect({ adapter: this, - fields: collectionConfig.fields, + fields: collectionConfig.flattenedFields, select, }) diff --git a/packages/db-mongodb/src/findVersions.ts b/packages/db-mongodb/src/findVersions.ts index e5ffa7ed63c..cb1810bf3bd 100644 --- a/packages/db-mongodb/src/findVersions.ts +++ b/packages/db-mongodb/src/findVersions.ts @@ -44,7 +44,7 @@ export const findVersions: FindVersions = async function findVersions( if (!hasNearConstraint) { sort = buildSortParam({ config: this.payload.config, - fields: collectionConfig.fields, + fields: collectionConfig.flattenedFields, locale, sort: sortArg || '-updatedAt', timestamps: true, @@ -68,7 +68,7 @@ export const findVersions: FindVersions = async function findVersions( pagination, projection: buildProjectionFromSelect({ adapter: this, - fields: buildVersionCollectionFields(this.payload.config, collectionConfig), + fields: buildVersionCollectionFields(this.payload.config, collectionConfig, true), select, }), sort, diff --git a/packages/db-mongodb/src/init.ts b/packages/db-mongodb/src/init.ts index fbcda3adbaa..75eeeb54739 100644 --- a/packages/db-mongodb/src/init.ts +++ b/packages/db-mongodb/src/init.ts @@ -37,7 +37,7 @@ export const init: Init = function init(this: MongooseAdapter) { versionSchema.plugin(paginate, { useEstimatedCount: true }).plugin( getBuildQueryPlugin({ collectionSlug: collection.slug, - versionsFields: versionCollectionFields, + versionsFields: buildVersionCollectionFields(this.payload.config, collection, true), }), ) @@ -84,9 +84,11 @@ export const init: Init = function init(this: MongooseAdapter) { }, }) - versionSchema - .plugin(paginate, { useEstimatedCount: true }) - .plugin(getBuildQueryPlugin({ versionsFields: versionGlobalFields })) + versionSchema.plugin(paginate, { useEstimatedCount: true }).plugin( + getBuildQueryPlugin({ + versionsFields: buildVersionGlobalFields(this.payload.config, global, true), + }), + ) this.versions[global.slug] = mongoose.model( versionModelName, diff --git a/packages/db-mongodb/src/queries/buildAndOrConditions.ts b/packages/db-mongodb/src/queries/buildAndOrConditions.ts index 05e6039b3e2..9ac05296796 100644 --- a/packages/db-mongodb/src/queries/buildAndOrConditions.ts +++ b/packages/db-mongodb/src/queries/buildAndOrConditions.ts @@ -1,4 +1,4 @@ -import type { Field, Payload, Where } from 'payload' +import type { FlattenedField, Payload, Where } from 'payload' import { parseParams } from './parseParams.js' @@ -11,7 +11,7 @@ export async function buildAndOrConditions({ where, }: { collectionSlug?: string - fields: Field[] + fields: FlattenedField[] globalSlug?: string locale?: string payload: Payload diff --git a/packages/db-mongodb/src/queries/buildQuery.ts b/packages/db-mongodb/src/queries/buildQuery.ts index 49ab646b613..64cde6af7aa 100644 --- a/packages/db-mongodb/src/queries/buildQuery.ts +++ b/packages/db-mongodb/src/queries/buildQuery.ts @@ -1,4 +1,4 @@ -import type { Field, Payload, Where } from 'payload' +import type { FlattenedField, Payload, Where } from 'payload' import { QueryError } from 'payload' @@ -6,7 +6,7 @@ import { parseParams } from './parseParams.js' type GetBuildQueryPluginArgs = { collectionSlug?: string - versionsFields?: Field[] + versionsFields?: FlattenedField[] } export type BuildQueryArgs = { @@ -34,11 +34,11 @@ export const getBuildQueryPlugin = ({ if (!fields) { if (globalSlug) { const globalConfig = payload.globals.config.find(({ slug }) => slug === globalSlug) - fields = globalConfig.fields + fields = globalConfig.flattenedFields } if (collectionSlug) { const collectionConfig = payload.collections[collectionSlug].config - fields = collectionConfig.fields + fields = collectionConfig.flattenedFields } } const errors = [] diff --git a/packages/db-mongodb/src/queries/buildSearchParams.ts b/packages/db-mongodb/src/queries/buildSearchParams.ts index 868017cf860..c914b6d7d6c 100644 --- a/packages/db-mongodb/src/queries/buildSearchParams.ts +++ b/packages/db-mongodb/src/queries/buildSearchParams.ts @@ -1,4 +1,4 @@ -import type { Field, Operator, PathToQuery, Payload } from 'payload' +import type { FlattenedField, Operator, PathToQuery, Payload } from 'payload' import { Types } from 'mongoose' import { getLocalizedPaths } from 'payload' @@ -34,7 +34,7 @@ export async function buildSearchParam({ val, }: { collectionSlug?: string - fields: Field[] + fields: FlattenedField[] globalSlug?: string incomingPath: string locale?: string @@ -68,11 +68,11 @@ export async function buildSearchParam({ field: { name: 'id', type: idFieldType, - } as Field, + } as FlattenedField, path: '_id', }) } else { - paths = await getLocalizedPaths({ + paths = getLocalizedPaths({ collectionSlug, fields, globalSlug, diff --git a/packages/db-mongodb/src/queries/buildSortParam.ts b/packages/db-mongodb/src/queries/buildSortParam.ts index de4d4860fe8..6f76cbffef2 100644 --- a/packages/db-mongodb/src/queries/buildSortParam.ts +++ b/packages/db-mongodb/src/queries/buildSortParam.ts @@ -1,11 +1,11 @@ import type { PaginateOptions } from 'mongoose' -import type { Field, SanitizedConfig, Sort } from 'payload' +import type { FlattenedField, SanitizedConfig, Sort } from 'payload' import { getLocalizedSortProperty } from './getLocalizedSortProperty.js' type Args = { config: SanitizedConfig - fields: Field[] + fields: FlattenedField[] locale: string sort: Sort timestamps: boolean diff --git a/packages/db-mongodb/src/queries/getLocalizedSortProperty.spec.ts b/packages/db-mongodb/src/queries/getLocalizedSortProperty.spec.ts index c771fb9fe40..cee0b680847 100644 --- a/packages/db-mongodb/src/queries/getLocalizedSortProperty.spec.ts +++ b/packages/db-mongodb/src/queries/getLocalizedSortProperty.spec.ts @@ -1,6 +1,6 @@ import type { Config, SanitizedConfig } from 'payload' -import { sanitizeConfig } from 'payload' +import { flattenAllFields, sanitizeConfig } from 'payload' import { getLocalizedSortProperty } from './getLocalizedSortProperty.js' @@ -69,19 +69,21 @@ describe('get localized sort property', () => { it('properly localizes nested sort properties', () => { const result = getLocalizedSortProperty({ config, - fields: [ - { - name: 'group', - type: 'group', - fields: [ - { - name: 'title', - type: 'text', - localized: true, - }, - ], - }, - ], + fields: flattenAllFields({ + fields: [ + { + name: 'group', + type: 'group', + fields: [ + { + name: 'title', + type: 'text', + localized: true, + }, + ], + }, + ], + }), locale: 'en', segments: ['group', 'title'], }) @@ -92,19 +94,21 @@ describe('get localized sort property', () => { it('keeps requested locale with nested sort properties', () => { const result = getLocalizedSortProperty({ config, - fields: [ - { - name: 'group', - type: 'group', - fields: [ - { - name: 'title', - type: 'text', - localized: true, - }, - ], - }, - ], + fields: flattenAllFields({ + fields: [ + { + name: 'group', + type: 'group', + fields: [ + { + name: 'title', + type: 'text', + localized: true, + }, + ], + }, + ], + }), locale: 'en', segments: ['group', 'title', 'es'], }) @@ -115,18 +119,20 @@ describe('get localized sort property', () => { it('properly localizes field within row', () => { const result = getLocalizedSortProperty({ config, - fields: [ - { - type: 'row', - fields: [ - { - name: 'title', - type: 'text', - localized: true, - }, - ], - }, - ], + fields: flattenAllFields({ + fields: [ + { + type: 'row', + fields: [ + { + name: 'title', + type: 'text', + localized: true, + }, + ], + }, + ], + }), locale: 'en', segments: ['title'], }) @@ -137,23 +143,25 @@ describe('get localized sort property', () => { it('properly localizes field within named tab', () => { const result = getLocalizedSortProperty({ config, - fields: [ - { - type: 'tabs', - tabs: [ - { - name: 'tab', - fields: [ - { - name: 'title', - type: 'text', - localized: true, - }, - ], - }, - ], - }, - ], + fields: flattenAllFields({ + fields: [ + { + type: 'tabs', + tabs: [ + { + name: 'tab', + fields: [ + { + name: 'title', + type: 'text', + localized: true, + }, + ], + }, + ], + }, + ], + }), locale: 'en', segments: ['tab', 'title'], }) @@ -164,23 +172,25 @@ describe('get localized sort property', () => { it('properly localizes field within unnamed tab', () => { const result = getLocalizedSortProperty({ config, - fields: [ - { - type: 'tabs', - tabs: [ - { - fields: [ - { - name: 'title', - type: 'text', - localized: true, - }, - ], - label: 'Tab', - }, - ], - }, - ], + fields: flattenAllFields({ + fields: [ + { + type: 'tabs', + tabs: [ + { + fields: [ + { + name: 'title', + type: 'text', + localized: true, + }, + ], + label: 'Tab', + }, + ], + }, + ], + }), locale: 'en', segments: ['title'], }) diff --git a/packages/db-mongodb/src/queries/getLocalizedSortProperty.ts b/packages/db-mongodb/src/queries/getLocalizedSortProperty.ts index 4ba9c2eda65..f7aec4bbd4f 100644 --- a/packages/db-mongodb/src/queries/getLocalizedSortProperty.ts +++ b/packages/db-mongodb/src/queries/getLocalizedSortProperty.ts @@ -1,11 +1,10 @@ -import type { Field, SanitizedConfig } from 'payload' +import type { FlattenedField, SanitizedConfig } from 'payload' -import { flattenTopLevelFields } from 'payload' import { fieldAffectsData, fieldIsPresentationalOnly } from 'payload/shared' type Args = { config: SanitizedConfig - fields: Field[] + fields: FlattenedField[] locale: string result?: string segments: string[] @@ -13,7 +12,7 @@ type Args = { export const getLocalizedSortProperty = ({ config, - fields: incomingFields, + fields, locale, result: incomingResult, segments: incomingSegments, @@ -24,9 +23,6 @@ export const getLocalizedSortProperty = ({ return incomingSegments.join('.') } - // Flatten incoming fields (row, etc) - const fields = flattenTopLevelFields(incomingFields) - const segments = [...incomingSegments] // Retrieve first segment, and remove from segments @@ -38,7 +34,7 @@ export const getLocalizedSortProperty = ({ ) if (matchedField && !fieldIsPresentationalOnly(matchedField)) { - let nextFields: Field[] + let nextFields: FlattenedField[] const remainingSegments = [...segments] let localizedSegment = matchedField.name @@ -65,14 +61,14 @@ export const getLocalizedSortProperty = ({ matchedField.type === 'group' || matchedField.type === 'array' ) { - nextFields = matchedField.fields + nextFields = matchedField.flattenedFields } if (matchedField.type === 'blocks') { nextFields = matchedField.blocks.reduce((flattenedBlockFields, block) => { return [ ...flattenedBlockFields, - ...block.fields.filter( + ...block.flattenedFields.filter( (blockField) => (fieldAffectsData(blockField) && blockField.name !== 'blockType' && diff --git a/packages/db-mongodb/src/queries/parseParams.ts b/packages/db-mongodb/src/queries/parseParams.ts index 18e22322527..18dae08d209 100644 --- a/packages/db-mongodb/src/queries/parseParams.ts +++ b/packages/db-mongodb/src/queries/parseParams.ts @@ -1,5 +1,5 @@ import type { FilterQuery } from 'mongoose' -import type { Field, Operator, Payload, Where } from 'payload' +import type { FlattenedField, Operator, Payload, Where } from 'payload' import { deepMergeWithCombinedArrays } from 'payload' import { validOperators } from 'payload/shared' @@ -16,7 +16,7 @@ export async function parseParams({ where, }: { collectionSlug?: string - fields: Field[] + fields: FlattenedField[] globalSlug?: string locale: string payload: Payload diff --git a/packages/db-mongodb/src/queries/sanitizeQueryValue.ts b/packages/db-mongodb/src/queries/sanitizeQueryValue.ts index cab773b71ab..34592123256 100644 --- a/packages/db-mongodb/src/queries/sanitizeQueryValue.ts +++ b/packages/db-mongodb/src/queries/sanitizeQueryValue.ts @@ -1,10 +1,10 @@ -import type { Block, Field, Payload, RelationshipField, TabAsField } from 'payload' +import type { FlattenedBlock, FlattenedField, Payload, RelationshipField } from 'payload' import { Types } from 'mongoose' -import { createArrayFromCommaDelineated, flattenTopLevelFields } from 'payload' +import { createArrayFromCommaDelineated } from 'payload' type SanitizeQueryValueArgs = { - field: Field | TabAsField + field: FlattenedField hasCustomID: boolean operator: string path: string @@ -41,7 +41,7 @@ const getFieldFromSegments = ({ field, segments, }: { - field: Block | Field | TabAsField + field: FlattenedBlock | FlattenedField segments: string[] }) => { if ('blocks' in field) { @@ -55,9 +55,7 @@ const getFieldFromSegments = ({ if ('fields' in field) { for (let i = 0; i < segments.length; i++) { - const foundField = flattenTopLevelFields(field.fields).find( - (each) => each.name === segments[i], - ) + const foundField = field.flattenedFields.find((each) => each.name === segments[i]) if (!foundField) { break diff --git a/packages/db-mongodb/src/queryDrafts.ts b/packages/db-mongodb/src/queryDrafts.ts index 542a6c72f00..8b06e67ed33 100644 --- a/packages/db-mongodb/src/queryDrafts.ts +++ b/packages/db-mongodb/src/queryDrafts.ts @@ -41,7 +41,7 @@ export const queryDrafts: QueryDrafts = async function queryDrafts( if (!hasNearConstraint) { sort = buildSortParam({ config: this.payload.config, - fields: collectionConfig.fields, + fields: collectionConfig.flattenedFields, locale, sort: sortArg || collectionConfig.defaultSort, timestamps: true, @@ -58,7 +58,7 @@ export const queryDrafts: QueryDrafts = async function queryDrafts( const projection = buildProjectionFromSelect({ adapter: this, - fields: buildVersionCollectionFields(this.payload.config, collectionConfig), + fields: buildVersionCollectionFields(this.payload.config, collectionConfig, true), select, }) // useEstimatedCount is faster, but not accurate, as it ignores any filters. It is thus set to true if there are no filters. diff --git a/packages/db-mongodb/src/updateGlobal.ts b/packages/db-mongodb/src/updateGlobal.ts index 0b0f36fc96e..ed090077cb8 100644 --- a/packages/db-mongodb/src/updateGlobal.ts +++ b/packages/db-mongodb/src/updateGlobal.ts @@ -20,7 +20,11 @@ export const updateGlobal: UpdateGlobal = async function updateGlobal( ...(await withSession(this, req)), lean: true, new: true, - projection: buildProjectionFromSelect({ adapter: this, fields, select }), + projection: buildProjectionFromSelect({ + adapter: this, + fields: this.payload.config.globals.find((global) => global.slug === slug).flattenedFields, + select, + }), } let result diff --git a/packages/db-mongodb/src/updateGlobalVersion.ts b/packages/db-mongodb/src/updateGlobalVersion.ts index a83a85ad480..73101f5967b 100644 --- a/packages/db-mongodb/src/updateGlobalVersion.ts +++ b/packages/db-mongodb/src/updateGlobalVersion.ts @@ -28,17 +28,20 @@ export async function updateGlobalVersion( ) { const VersionModel = this.versions[globalSlug] const whereToUse = where || { id: { equals: id } } - const fields = buildVersionGlobalFields( - this.payload.config, - this.payload.config.globals.find((global) => global.slug === globalSlug), - ) + + const currentGlobal = this.payload.config.globals.find((global) => global.slug === globalSlug) + const fields = buildVersionGlobalFields(this.payload.config, currentGlobal) const options: QueryOptions = { ...optionsArgs, ...(await withSession(this, req)), lean: true, new: true, - projection: buildProjectionFromSelect({ adapter: this, fields, select }), + projection: buildProjectionFromSelect({ + adapter: this, + fields: buildVersionGlobalFields(this.payload.config, currentGlobal, true), + select, + }), } const query = await VersionModel.buildQuery({ diff --git a/packages/db-mongodb/src/updateOne.ts b/packages/db-mongodb/src/updateOne.ts index ce6d04ecac4..f70cb149f3b 100644 --- a/packages/db-mongodb/src/updateOne.ts +++ b/packages/db-mongodb/src/updateOne.ts @@ -30,7 +30,11 @@ export const updateOne: UpdateOne = async function updateOne( ...(await withSession(this, req)), lean: true, new: true, - projection: buildProjectionFromSelect({ adapter: this, fields, select }), + projection: buildProjectionFromSelect({ + adapter: this, + fields: this.payload.collections[collection].config.flattenedFields, + select, + }), } const query = await Model.buildQuery({ diff --git a/packages/db-mongodb/src/updateVersion.ts b/packages/db-mongodb/src/updateVersion.ts index b79f6727c75..7f814b4ea5b 100644 --- a/packages/db-mongodb/src/updateVersion.ts +++ b/packages/db-mongodb/src/updateVersion.ts @@ -33,7 +33,15 @@ export const updateVersion: UpdateVersion = async function updateVersion( ...(await withSession(this, req)), lean: true, new: true, - projection: buildProjectionFromSelect({ adapter: this, fields, select }), + projection: buildProjectionFromSelect({ + adapter: this, + fields: buildVersionCollectionFields( + this.payload.config, + this.payload.collections[collection].config, + true, + ), + select, + }), } const query = await VersionModel.buildQuery({ diff --git a/packages/db-mongodb/src/utilities/buildJoinAggregation.ts b/packages/db-mongodb/src/utilities/buildJoinAggregation.ts index f7929b1cfb9..41d5ad8f5cc 100644 --- a/packages/db-mongodb/src/utilities/buildJoinAggregation.ts +++ b/packages/db-mongodb/src/utilities/buildJoinAggregation.ts @@ -74,7 +74,7 @@ export const buildJoinAggregation = async ({ const sort = buildSortParam({ config: adapter.payload.config, - fields: adapter.payload.collections[slug].config.fields, + fields: adapter.payload.collections[slug].config.flattenedFields, locale, sort: sortJoin, timestamps: true, diff --git a/packages/db-mongodb/src/utilities/buildProjectionFromSelect.ts b/packages/db-mongodb/src/utilities/buildProjectionFromSelect.ts index a2ca3625819..51215d7c6fa 100644 --- a/packages/db-mongodb/src/utilities/buildProjectionFromSelect.ts +++ b/packages/db-mongodb/src/utilities/buildProjectionFromSelect.ts @@ -1,12 +1,6 @@ -import { - deepCopyObjectSimple, - type Field, - type FieldAffectingData, - type SelectMode, - type SelectType, - type TabAsField, -} from 'payload' -import { fieldAffectsData, getSelectMode, tabHasName } from 'payload/shared' +import type { FieldAffectingData, FlattenedField, SelectMode, SelectType } from 'payload' + +import { deepCopyObjectSimple, fieldAffectsData, getSelectMode } from 'payload/shared' import type { MongooseAdapter } from '../index.js' @@ -47,7 +41,7 @@ const traverseFields = ({ }: { adapter: MongooseAdapter databaseSchemaPath?: string - fields: (Field | TabAsField)[] + fields: FlattenedField[] projection: Record select: SelectType selectAllOnCurrentLevel?: boolean @@ -107,13 +101,7 @@ const traverseFields = ({ case 'array': case 'group': case 'tab': { - let fieldSelect: SelectType - - if (field.type === 'tab' && !tabHasName(field)) { - fieldSelect = select - } else { - fieldSelect = select[field.name] as SelectType - } + const fieldSelect = select[field.name] as SelectType if (field.type === 'array' && selectMode === 'include') { fieldSelect['id'] = true @@ -122,7 +110,7 @@ const traverseFields = ({ traverseFields({ adapter, databaseSchemaPath: fieldDatabaseSchemaPath, - fields: field.fields, + fields: field.flattenedFields, projection, select: fieldSelect, selectMode, @@ -143,7 +131,7 @@ const traverseFields = ({ traverseFields({ adapter, databaseSchemaPath: fieldDatabaseSchemaPath, - fields: block.fields, + fields: block.flattenedFields, projection, select: {}, selectAllOnCurrentLevel: true, @@ -171,7 +159,7 @@ const traverseFields = ({ traverseFields({ adapter, databaseSchemaPath: fieldDatabaseSchemaPath, - fields: block.fields, + fields: block.flattenedFields, projection, select: blocksSelect[block.slug] as SelectType, selectMode: blockSelectMode, @@ -181,30 +169,6 @@ const traverseFields = ({ break } - case 'collapsible': - case 'row': - traverseFields({ - adapter, - databaseSchemaPath, - fields: field.fields, - projection, - select, - selectMode, - withinLocalizedField, - }) - break - - case 'tabs': - traverseFields({ - adapter, - databaseSchemaPath, - fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })), - projection, - select, - selectMode, - withinLocalizedField, - }) - break default: break @@ -218,7 +182,7 @@ export const buildProjectionFromSelect = ({ select, }: { adapter: MongooseAdapter - fields: Field[] + fields: FlattenedField[] select?: SelectType }): Record | undefined => { if (!select) { diff --git a/packages/db-postgres/src/predefinedMigrations/v2-v3/fetchAndResave/index.ts b/packages/db-postgres/src/predefinedMigrations/v2-v3/fetchAndResave/index.ts index 751bf540a43..11140fa3692 100644 --- a/packages/db-postgres/src/predefinedMigrations/v2-v3/fetchAndResave/index.ts +++ b/packages/db-postgres/src/predefinedMigrations/v2-v3/fetchAndResave/index.ts @@ -1,5 +1,5 @@ import type { TransactionPg } from '@payloadcms/drizzle/types' -import type { Field, Payload, PayloadRequest } from 'payload' +import type { FlattenedField, Payload, PayloadRequest } from 'payload' import { upsertRow } from '@payloadcms/drizzle' @@ -14,7 +14,7 @@ type Args = { db: TransactionPg debug: boolean docsToResave: DocsToResave - fields: Field[] + fields: FlattenedField[] globalSlug?: string isVersions: boolean payload: Payload diff --git a/packages/db-postgres/src/predefinedMigrations/v2-v3/fetchAndResave/traverseFields.ts b/packages/db-postgres/src/predefinedMigrations/v2-v3/fetchAndResave/traverseFields.ts index ad1e8a21222..8304c659be7 100644 --- a/packages/db-postgres/src/predefinedMigrations/v2-v3/fetchAndResave/traverseFields.ts +++ b/packages/db-postgres/src/predefinedMigrations/v2-v3/fetchAndResave/traverseFields.ts @@ -1,10 +1,8 @@ -import type { Field } from 'payload' - -import { tabHasName } from 'payload/shared' +import type { FlattenedField } from 'payload' type Args = { doc: Record - fields: Field[] + fields: FlattenedField[] locale?: string path: string rows: Record[] @@ -22,7 +20,7 @@ export const traverseFields = ({ doc, fields, locale, path, rows }: Args) => { localeRows.forEach((row, i) => { return traverseFields({ doc: row as Record, - fields: field.fields, + fields: field.flattenedFields, locale, path: `${path ? `${path}.` : ''}${field.name}.${i}`, rows, @@ -36,7 +34,7 @@ export const traverseFields = ({ doc, fields, locale, path, rows }: Args) => { rowData.forEach((row, i) => { return traverseFields({ doc: row as Record, - fields: field.fields, + fields: field.flattenedFields, path: `${path ? `${path}.` : ''}${field.name}.${i}`, rows, }) @@ -58,7 +56,7 @@ export const traverseFields = ({ doc, fields, locale, path, rows }: Args) => { if (matchedBlock) { return traverseFields({ doc: row as Record, - fields: matchedBlock.fields, + fields: matchedBlock.flattenedFields, locale, path: `${path ? `${path}.` : ''}${field.name}.${i}`, rows, @@ -76,7 +74,7 @@ export const traverseFields = ({ doc, fields, locale, path, rows }: Args) => { if (matchedBlock) { return traverseFields({ doc: row as Record, - fields: matchedBlock.fields, + fields: matchedBlock.flattenedFields, path: `${path ? `${path}.` : ''}${field.name}.${i}`, rows, }) @@ -86,18 +84,9 @@ export const traverseFields = ({ doc, fields, locale, path, rows }: Args) => { break } - case 'collapsible': - // falls through - case 'row': { - return traverseFields({ - doc, - fields: field.fields, - path, - rows, - }) - } - case 'group': { + case 'group': + case 'tab': { const newPath = `${path ? `${path}.` : ''}${field.name}` const newDoc = doc?.[field.name] @@ -106,7 +95,7 @@ export const traverseFields = ({ doc, fields, locale, path, rows }: Args) => { Object.entries(newDoc).forEach(([locale, localeDoc]) => { return traverseFields({ doc: localeDoc, - fields: field.fields, + fields: field.flattenedFields, locale, path: newPath, rows, @@ -115,7 +104,7 @@ export const traverseFields = ({ doc, fields, locale, path, rows }: Args) => { } else { return traverseFields({ doc: newDoc as Record, - fields: field.fields, + fields: field.flattenedFields, path: newPath, rows, }) @@ -177,42 +166,6 @@ export const traverseFields = ({ doc, fields, locale, path, rows }: Args) => { } break } - case 'tabs': { - return field.tabs.forEach((tab) => { - if (tabHasName(tab)) { - const newDoc = doc?.[tab.name] - const newPath = `${path ? `${path}.` : ''}${tab.name}` - - if (typeof newDoc === 'object' && newDoc !== null) { - if (tab.localized) { - Object.entries(newDoc).forEach(([locale, localeDoc]) => { - return traverseFields({ - doc: localeDoc, - fields: tab.fields, - locale, - path: newPath, - rows, - }) - }) - } else { - return traverseFields({ - doc: newDoc as Record, - fields: tab.fields, - path: newPath, - rows, - }) - } - } - } else { - traverseFields({ - doc, - fields: tab.fields, - path, - rows, - }) - } - }) - } } }) } diff --git a/packages/db-postgres/src/predefinedMigrations/v2-v3/index.ts b/packages/db-postgres/src/predefinedMigrations/v2-v3/index.ts index ea9ed42fc5e..19d555e57ec 100644 --- a/packages/db-postgres/src/predefinedMigrations/v2-v3/index.ts +++ b/packages/db-postgres/src/predefinedMigrations/v2-v3/index.ts @@ -91,7 +91,7 @@ export const migratePostgresV2toV3 = async ({ debug, payload, req }: Args) => { columnPrefix: '', db, disableNotNull: false, - fields: collection.fields, + fields: collection.flattenedFields, isVersions: false, newTableName: tableName, parentTableName: tableName, @@ -106,7 +106,7 @@ export const migratePostgresV2toV3 = async ({ debug, payload, req }: Args) => { collectionSlug: collection.slug, db, debug, - fields: collection.fields, + fields: collection.flattenedFields, isVersions: false, pathsToQuery, payload, @@ -118,7 +118,7 @@ export const migratePostgresV2toV3 = async ({ debug, payload, req }: Args) => { const versionsTableName = adapter.tableNameMap.get( `_${toSnakeCase(collection.slug)}${adapter.versionsSuffix}`, ) - const versionFields = buildVersionCollectionFields(payload.config, collection) + const versionFields = buildVersionCollectionFields(payload.config, collection, true) const versionPathsToQuery: PathsToQuery = new Set() traverseFields({ @@ -162,7 +162,7 @@ export const migratePostgresV2toV3 = async ({ debug, payload, req }: Args) => { columnPrefix: '', db, disableNotNull: false, - fields: global.fields, + fields: global.flattenedFields, globalSlug: global.slug, isVersions: false, newTableName: tableName, @@ -177,7 +177,7 @@ export const migratePostgresV2toV3 = async ({ debug, payload, req }: Args) => { adapter, db, debug, - fields: global.fields, + fields: global.flattenedFields, globalSlug: global.slug, isVersions: false, pathsToQuery, @@ -191,7 +191,7 @@ export const migratePostgresV2toV3 = async ({ debug, payload, req }: Args) => { `_${toSnakeCase(global.slug)}${adapter.versionsSuffix}`, ) - const versionFields = buildVersionGlobalFields(payload.config, global) + const versionFields = buildVersionGlobalFields(payload.config, global, true) const versionPathsToQuery: PathsToQuery = new Set() diff --git a/packages/db-postgres/src/predefinedMigrations/v2-v3/migrateRelationships.ts b/packages/db-postgres/src/predefinedMigrations/v2-v3/migrateRelationships.ts index 73b04cedfac..70853259972 100644 --- a/packages/db-postgres/src/predefinedMigrations/v2-v3/migrateRelationships.ts +++ b/packages/db-postgres/src/predefinedMigrations/v2-v3/migrateRelationships.ts @@ -1,5 +1,5 @@ import type { TransactionPg } from '@payloadcms/drizzle/types' -import type { Field, Payload, PayloadRequest } from 'payload' +import type { FlattenedField, Payload, PayloadRequest } from 'payload' import { sql } from 'drizzle-orm' @@ -13,7 +13,7 @@ type Args = { collectionSlug?: string db: TransactionPg debug: boolean - fields: Field[] + fields: FlattenedField[] globalSlug?: string isVersions: boolean pathsToQuery: PathsToQuery diff --git a/packages/db-postgres/src/predefinedMigrations/v2-v3/traverseFields.ts b/packages/db-postgres/src/predefinedMigrations/v2-v3/traverseFields.ts index 7f362a8f755..7e4dea42445 100644 --- a/packages/db-postgres/src/predefinedMigrations/v2-v3/traverseFields.ts +++ b/packages/db-postgres/src/predefinedMigrations/v2-v3/traverseFields.ts @@ -1,7 +1,6 @@ import type { TransactionPg } from '@payloadcms/drizzle/types' -import type { Field, Payload } from 'payload' +import type { FlattenedField, Payload } from 'payload' -import { tabHasName } from 'payload/shared' import toSnakeCase from 'to-snake-case' import type { PostgresAdapter } from '../../types.js' @@ -13,7 +12,7 @@ type Args = { columnPrefix: string db: TransactionPg disableNotNull: boolean - fields: Field[] + fields: FlattenedField[] globalSlug?: string isVersions: boolean newTableName: string @@ -35,7 +34,7 @@ export const traverseFields = (args: Args) => { return traverseFields({ ...args, columnPrefix: '', - fields: field.fields, + fields: field.flattenedFields, newTableName, parentTableName: newTableName, path: `${args.path ? `${args.path}.` : ''}${field.name}.%`, @@ -51,7 +50,7 @@ export const traverseFields = (args: Args) => { traverseFields({ ...args, columnPrefix: '', - fields: block.fields, + fields: block.flattenedFields, newTableName, parentTableName: newTableName, path: `${args.path ? `${args.path}.` : ''}${field.name}.%`, @@ -59,15 +58,8 @@ export const traverseFields = (args: Args) => { }) } - case 'collapsible': - case 'row': { - return traverseFields({ - ...args, - fields: field.fields, - }) - } - - case 'group': { + case 'group': + case 'tab': { let newTableName = `${args.newTableName}_${toSnakeCase(field.name)}` if (field.localized && args.payload.config.localization) { @@ -77,7 +69,7 @@ export const traverseFields = (args: Args) => { return traverseFields({ ...args, columnPrefix: `${args.columnPrefix}${toSnakeCase(field.name)}_`, - fields: field.fields, + fields: field.flattenedFields, newTableName, path: `${args.path ? `${args.path}.` : ''}${field.name}`, }) @@ -93,24 +85,6 @@ export const traverseFields = (args: Args) => { return null } - case 'tabs': { - return field.tabs.forEach((tab) => { - if (tabHasName(tab)) { - args.columnPrefix = `${args.columnPrefix}_${toSnakeCase(tab.name)}_` - args.path = `${args.path ? `${args.path}.` : ''}${tab.name}` - args.newTableName = `${args.newTableName}_${toSnakeCase(tab.name)}` - - if (tab.localized && args.payload.config.localization) { - args.newTableName += args.adapter.localesSuffix - } - } - - traverseFields({ - ...args, - fields: tab.fields, - }) - }) - } } }) } diff --git a/packages/db-sqlite/src/init.ts b/packages/db-sqlite/src/init.ts index 4a2abfcb332..ec435890334 100644 --- a/packages/db-sqlite/src/init.ts +++ b/packages/db-sqlite/src/init.ts @@ -69,7 +69,7 @@ export const init: Init = async function init(this: SQLiteAdapter) { adapter: this, disableNotNull: !!collection?.versions?.drafts, disableUnique: false, - fields: collection.fields, + fields: collection.flattenedFields, locales, tableName, timestamps: collection.timestamps, @@ -80,7 +80,7 @@ export const init: Init = async function init(this: SQLiteAdapter) { const versionsTableName = this.tableNameMap.get( `_${toSnakeCase(collection.slug)}${this.versionsSuffix}`, ) - const versionFields = buildVersionCollectionFields(config, collection) + const versionFields = buildVersionCollectionFields(config, collection, true) buildTable({ adapter: this, @@ -105,7 +105,7 @@ export const init: Init = async function init(this: SQLiteAdapter) { adapter: this, disableNotNull: !!global?.versions?.drafts, disableUnique: false, - fields: global.fields, + fields: global.flattenedFields, locales, tableName, timestamps: false, @@ -120,7 +120,7 @@ export const init: Init = async function init(this: SQLiteAdapter) { versionsCustomName: true, }) const config = this.payload.config - const versionFields = buildVersionGlobalFields(config, global) + const versionFields = buildVersionGlobalFields(config, global, true) buildTable({ adapter: this, diff --git a/packages/db-sqlite/src/schema/build.ts b/packages/db-sqlite/src/schema/build.ts index 1807747e9f0..8ade85794c7 100644 --- a/packages/db-sqlite/src/schema/build.ts +++ b/packages/db-sqlite/src/schema/build.ts @@ -8,7 +8,7 @@ import type { SQLiteTableWithColumns, UniqueConstraintBuilder, } from 'drizzle-orm/sqlite-core' -import type { Field } from 'payload' +import type { FlattenedField } from 'payload' import { buildIndexName, createTableName } from '@payloadcms/drizzle' import { relations, sql } from 'drizzle-orm' @@ -60,7 +60,7 @@ type Args = { disableNotNull: boolean disableRelsTableUnique?: boolean disableUnique: boolean - fields: Field[] + fields: FlattenedField[] locales?: [string, ...string[]] rootRelationships?: Set rootRelationsToBuild?: RelationMap diff --git a/packages/db-sqlite/src/schema/idToUUID.ts b/packages/db-sqlite/src/schema/idToUUID.ts index 466ec63b3d6..aa2b81f9452 100644 --- a/packages/db-sqlite/src/schema/idToUUID.ts +++ b/packages/db-sqlite/src/schema/idToUUID.ts @@ -1,6 +1,6 @@ -import type { Field } from 'payload' +import type { FlattenedField } from 'payload' -export const idToUUID = (fields: Field[]): Field[] => +export const idToUUID = (fields: FlattenedField[]): FlattenedField[] => fields.map((field) => { if ('name' in field && field.name === 'id') { return { diff --git a/packages/db-sqlite/src/schema/setColumnID.ts b/packages/db-sqlite/src/schema/setColumnID.ts index f92adb32b06..95f4561e90c 100644 --- a/packages/db-sqlite/src/schema/setColumnID.ts +++ b/packages/db-sqlite/src/schema/setColumnID.ts @@ -1,19 +1,16 @@ import type { SQLiteColumnBuilder } from 'drizzle-orm/sqlite-core' +import type { FlattenedField } from 'payload' import { integer, numeric, text } from 'drizzle-orm/sqlite-core' -import { type Field, flattenTopLevelFields } from 'payload' -import { fieldAffectsData } from 'payload/shared' import type { IDType } from '../types.js' type Args = { columns: Record - fields: Field[] + fields: FlattenedField[] } export const setColumnID = ({ columns, fields }: Args): IDType => { - const idField = flattenTopLevelFields(fields).find( - (field) => fieldAffectsData(field) && field.name === 'id', - ) + const idField = fields.find((field) => field.name === 'id') if (idField) { if (idField.type === 'number') { columns.id = numeric('id').primaryKey() diff --git a/packages/db-sqlite/src/schema/traverseFields.ts b/packages/db-sqlite/src/schema/traverseFields.ts index b24eefa68dc..1da950ddd43 100644 --- a/packages/db-sqlite/src/schema/traverseFields.ts +++ b/packages/db-sqlite/src/schema/traverseFields.ts @@ -1,7 +1,7 @@ import type { DrizzleAdapter } from '@payloadcms/drizzle/types' import type { Relation } from 'drizzle-orm' import type { IndexBuilder, SQLiteColumnBuilder } from 'drizzle-orm/sqlite-core' -import type { Field, TabAsField } from 'payload' +import type { FlattenedField } from 'payload' import { buildIndexName, @@ -41,7 +41,7 @@ type Args = { disableRelsTableUnique?: boolean disableUnique?: boolean fieldPrefix?: string - fields: (Field | TabAsField)[] + fields: FlattenedField[] forceLocalized?: boolean indexes: Record IndexBuilder> locales: [string, ...string[]] @@ -124,58 +124,53 @@ export const traverseFields = ({ return } - let columnName: string - let fieldName: string - let targetTable = columns let targetIndexes = indexes - if (fieldAffectsData(field)) { - columnName = `${columnPrefix || ''}${field.name[0] === '_' ? '_' : ''}${toSnakeCase( - field.name, - )}` - fieldName = `${fieldPrefix?.replace('.', '_') || ''}${field.name}` - - // If field is localized, - // add the column to the locale table instead of main table - if ( - adapter.payload.config.localization && - (field.localized || forceLocalized) && - field.type !== 'array' && - field.type !== 'blocks' && - (('hasMany' in field && field.hasMany !== true) || !('hasMany' in field)) - ) { - hasLocalizedField = true - targetTable = localesColumns - targetIndexes = localesIndexes - } + const columnName = `${columnPrefix || ''}${field.name[0] === '_' ? '_' : ''}${toSnakeCase( + field.name, + )}` + const fieldName = `${fieldPrefix?.replace('.', '_') || ''}${field.name}` - if ( - (field.unique || field.index || ['relationship', 'upload'].includes(field.type)) && - !['array', 'blocks', 'group', 'point'].includes(field.type) && - !('hasMany' in field && field.hasMany === true) && - !('relationTo' in field && Array.isArray(field.relationTo)) - ) { - const unique = disableUnique !== true && field.unique - if (unique) { - const constraintValue = `${fieldPrefix || ''}${field.name}` - if (!adapter.fieldConstraints?.[rootTableName]) { - adapter.fieldConstraints[rootTableName] = {} - } - adapter.fieldConstraints[rootTableName][`${columnName}_idx`] = constraintValue + // If field is localized, + // add the column to the locale table instead of main table + if ( + adapter.payload.config.localization && + (field.localized || forceLocalized) && + field.type !== 'array' && + field.type !== 'blocks' && + (('hasMany' in field && field.hasMany !== true) || !('hasMany' in field)) + ) { + hasLocalizedField = true + targetTable = localesColumns + targetIndexes = localesIndexes + } + + if ( + (field.unique || field.index || ['relationship', 'upload'].includes(field.type)) && + !['array', 'blocks', 'group', 'point'].includes(field.type) && + !('hasMany' in field && field.hasMany === true) && + !('relationTo' in field && Array.isArray(field.relationTo)) + ) { + const unique = disableUnique !== true && field.unique + if (unique) { + const constraintValue = `${fieldPrefix || ''}${field.name}` + if (!adapter.fieldConstraints?.[rootTableName]) { + adapter.fieldConstraints[rootTableName] = {} } + adapter.fieldConstraints[rootTableName][`${columnName}_idx`] = constraintValue + } - const indexName = buildIndexName({ - name: `${newTableName}_${columnName}`, - adapter: adapter as unknown as DrizzleAdapter, - }) + const indexName = buildIndexName({ + name: `${newTableName}_${columnName}`, + adapter: adapter as unknown as DrizzleAdapter, + }) - targetIndexes[indexName] = createIndex({ - name: field.localized ? [fieldName, '_locale'] : fieldName, - indexName, - unique, - }) - } + targetIndexes[indexName] = createIndex({ + name: field.localized ? [fieldName, '_locale'] : fieldName, + indexName, + unique, + }) } switch (field.type) { @@ -236,7 +231,7 @@ export const traverseFields = ({ disableNotNull: disableNotNullFromHere, disableRelsTableUnique: true, disableUnique, - fields: disableUnique ? idToUUID(field.fields) : field.fields, + fields: disableUnique ? idToUUID(field.flattenedFields) : field.flattenedFields, rootRelationships: relationships, rootRelationsToBuild, rootTableIDColType, @@ -376,7 +371,7 @@ export const traverseFields = ({ disableNotNull: disableNotNullFromHere, disableRelsTableUnique: true, disableUnique, - fields: disableUnique ? idToUUID(block.fields) : block.fields, + fields: disableUnique ? idToUUID(block.flattenedFields) : block.flattenedFields, rootRelationships: relationships, rootRelationsToBuild, rootTableIDColType, @@ -480,62 +475,6 @@ export const traverseFields = ({ break } - case 'collapsible': - case 'row': { - const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull - const { - hasLocalizedField: rowHasLocalizedField, - hasLocalizedManyNumberField: rowHasLocalizedManyNumberField, - hasLocalizedManyTextField: rowHasLocalizedManyTextField, - hasLocalizedRelationshipField: rowHasLocalizedRelationshipField, - hasManyNumberField: rowHasManyNumberField, - hasManyTextField: rowHasManyTextField, - } = traverseFields({ - adapter, - columnPrefix, - columns, - disableNotNull: disableNotNullFromHere, - disableUnique, - fieldPrefix, - fields: field.fields, - forceLocalized, - indexes, - locales, - localesColumns, - localesIndexes, - newTableName, - parentTableName, - relationships, - relationsToBuild, - rootRelationsToBuild, - rootTableIDColType, - rootTableName, - uniqueRelationships, - versions, - withinLocalizedArrayOrBlock, - }) - - if (rowHasLocalizedField) { - hasLocalizedField = true - } - if (rowHasLocalizedRelationshipField) { - hasLocalizedRelationshipField = true - } - if (rowHasManyTextField) { - hasManyTextField = true - } - if (rowHasLocalizedManyTextField) { - hasLocalizedManyTextField = true - } - if (rowHasManyNumberField) { - hasManyNumberField = true - } - if (rowHasLocalizedManyNumberField) { - hasLocalizedManyNumberField = true - } - break - } - case 'date': { targetTable[fieldName] = withDefault(text(columnName), field) break @@ -543,60 +482,6 @@ export const traverseFields = ({ case 'group': case 'tab': { - if (!('name' in field)) { - const { - hasLocalizedField: groupHasLocalizedField, - hasLocalizedManyNumberField: groupHasLocalizedManyNumberField, - hasLocalizedManyTextField: groupHasLocalizedManyTextField, - hasLocalizedRelationshipField: groupHasLocalizedRelationshipField, - hasManyNumberField: groupHasManyNumberField, - hasManyTextField: groupHasManyTextField, - } = traverseFields({ - adapter, - columnPrefix, - columns, - disableNotNull, - disableUnique, - fieldPrefix, - fields: field.fields, - forceLocalized, - indexes, - locales, - localesColumns, - localesIndexes, - newTableName, - parentTableName, - relationships, - relationsToBuild, - rootRelationsToBuild, - rootTableIDColType, - rootTableName, - uniqueRelationships, - versions, - withinLocalizedArrayOrBlock, - }) - - if (groupHasLocalizedField) { - hasLocalizedField = true - } - if (groupHasLocalizedRelationshipField) { - hasLocalizedRelationshipField = true - } - if (groupHasManyTextField) { - hasManyTextField = true - } - if (groupHasLocalizedManyTextField) { - hasLocalizedManyTextField = true - } - if (groupHasManyNumberField) { - hasManyNumberField = true - } - if (groupHasLocalizedManyNumberField) { - hasLocalizedManyNumberField = true - } - break - } - const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull const { @@ -613,7 +498,7 @@ export const traverseFields = ({ disableNotNull: disableNotNullFromHere, disableUnique, fieldPrefix: `${fieldName}.`, - fields: field.fields, + fields: field.flattenedFields, forceLocalized: field.localized, indexes, locales, @@ -846,61 +731,6 @@ export const traverseFields = ({ break - case 'tabs': { - const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull - - const { - hasLocalizedField: tabHasLocalizedField, - hasLocalizedManyNumberField: tabHasLocalizedManyNumberField, - hasLocalizedManyTextField: tabHasLocalizedManyTextField, - hasLocalizedRelationshipField: tabHasLocalizedRelationshipField, - hasManyNumberField: tabHasManyNumberField, - hasManyTextField: tabHasManyTextField, - } = traverseFields({ - adapter, - columnPrefix, - columns, - disableNotNull: disableNotNullFromHere, - disableUnique, - fieldPrefix, - fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })), - forceLocalized, - indexes, - locales, - localesColumns, - localesIndexes, - newTableName, - parentTableName, - relationships, - relationsToBuild, - rootRelationsToBuild, - rootTableIDColType, - rootTableName, - uniqueRelationships, - versions, - withinLocalizedArrayOrBlock, - }) - - if (tabHasLocalizedField) { - hasLocalizedField = true - } - if (tabHasLocalizedRelationshipField) { - hasLocalizedRelationshipField = true - } - if (tabHasManyTextField) { - hasManyTextField = true - } - if (tabHasLocalizedManyTextField) { - hasLocalizedManyTextField = true - } - if (tabHasManyNumberField) { - hasManyNumberField = true - } - if (tabHasLocalizedManyNumberField) { - hasLocalizedManyNumberField = true - } - break - } case 'text': { if (field.hasMany) { const isLocalized = diff --git a/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/fetchAndResave/index.ts b/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/fetchAndResave/index.ts index 9a72f2d6bc5..8d6f3b9bbda 100644 --- a/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/fetchAndResave/index.ts +++ b/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/fetchAndResave/index.ts @@ -1,5 +1,5 @@ import type { TransactionPg } from '@payloadcms/drizzle/types' -import type { Field, Payload, PayloadRequest } from 'payload' +import type { FlattenedField, Payload, PayloadRequest } from 'payload' import { upsertRow } from '@payloadcms/drizzle' @@ -14,7 +14,7 @@ type Args = { db: TransactionPg debug: boolean docsToResave: DocsToResave - fields: Field[] + fields: FlattenedField[] globalSlug?: string isVersions: boolean payload: Payload diff --git a/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/fetchAndResave/traverseFields.ts b/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/fetchAndResave/traverseFields.ts index ad1e8a21222..8304c659be7 100644 --- a/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/fetchAndResave/traverseFields.ts +++ b/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/fetchAndResave/traverseFields.ts @@ -1,10 +1,8 @@ -import type { Field } from 'payload' - -import { tabHasName } from 'payload/shared' +import type { FlattenedField } from 'payload' type Args = { doc: Record - fields: Field[] + fields: FlattenedField[] locale?: string path: string rows: Record[] @@ -22,7 +20,7 @@ export const traverseFields = ({ doc, fields, locale, path, rows }: Args) => { localeRows.forEach((row, i) => { return traverseFields({ doc: row as Record, - fields: field.fields, + fields: field.flattenedFields, locale, path: `${path ? `${path}.` : ''}${field.name}.${i}`, rows, @@ -36,7 +34,7 @@ export const traverseFields = ({ doc, fields, locale, path, rows }: Args) => { rowData.forEach((row, i) => { return traverseFields({ doc: row as Record, - fields: field.fields, + fields: field.flattenedFields, path: `${path ? `${path}.` : ''}${field.name}.${i}`, rows, }) @@ -58,7 +56,7 @@ export const traverseFields = ({ doc, fields, locale, path, rows }: Args) => { if (matchedBlock) { return traverseFields({ doc: row as Record, - fields: matchedBlock.fields, + fields: matchedBlock.flattenedFields, locale, path: `${path ? `${path}.` : ''}${field.name}.${i}`, rows, @@ -76,7 +74,7 @@ export const traverseFields = ({ doc, fields, locale, path, rows }: Args) => { if (matchedBlock) { return traverseFields({ doc: row as Record, - fields: matchedBlock.fields, + fields: matchedBlock.flattenedFields, path: `${path ? `${path}.` : ''}${field.name}.${i}`, rows, }) @@ -86,18 +84,9 @@ export const traverseFields = ({ doc, fields, locale, path, rows }: Args) => { break } - case 'collapsible': - // falls through - case 'row': { - return traverseFields({ - doc, - fields: field.fields, - path, - rows, - }) - } - case 'group': { + case 'group': + case 'tab': { const newPath = `${path ? `${path}.` : ''}${field.name}` const newDoc = doc?.[field.name] @@ -106,7 +95,7 @@ export const traverseFields = ({ doc, fields, locale, path, rows }: Args) => { Object.entries(newDoc).forEach(([locale, localeDoc]) => { return traverseFields({ doc: localeDoc, - fields: field.fields, + fields: field.flattenedFields, locale, path: newPath, rows, @@ -115,7 +104,7 @@ export const traverseFields = ({ doc, fields, locale, path, rows }: Args) => { } else { return traverseFields({ doc: newDoc as Record, - fields: field.fields, + fields: field.flattenedFields, path: newPath, rows, }) @@ -177,42 +166,6 @@ export const traverseFields = ({ doc, fields, locale, path, rows }: Args) => { } break } - case 'tabs': { - return field.tabs.forEach((tab) => { - if (tabHasName(tab)) { - const newDoc = doc?.[tab.name] - const newPath = `${path ? `${path}.` : ''}${tab.name}` - - if (typeof newDoc === 'object' && newDoc !== null) { - if (tab.localized) { - Object.entries(newDoc).forEach(([locale, localeDoc]) => { - return traverseFields({ - doc: localeDoc, - fields: tab.fields, - locale, - path: newPath, - rows, - }) - }) - } else { - return traverseFields({ - doc: newDoc as Record, - fields: tab.fields, - path: newPath, - rows, - }) - } - } - } else { - traverseFields({ - doc, - fields: tab.fields, - path, - rows, - }) - } - }) - } } }) } diff --git a/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/index.ts b/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/index.ts index 38043a2a9a2..de163f4c691 100644 --- a/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/index.ts +++ b/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/index.ts @@ -91,7 +91,7 @@ export const migratePostgresV2toV3 = async ({ debug, payload, req }: Args) => { columnPrefix: '', db, disableNotNull: false, - fields: collection.fields, + fields: collection.flattenedFields, isVersions: false, newTableName: tableName, parentTableName: tableName, @@ -106,7 +106,7 @@ export const migratePostgresV2toV3 = async ({ debug, payload, req }: Args) => { collectionSlug: collection.slug, db, debug, - fields: collection.fields, + fields: collection.flattenedFields, isVersions: false, pathsToQuery, payload, @@ -119,7 +119,7 @@ export const migratePostgresV2toV3 = async ({ debug, payload, req }: Args) => { `_${toSnakeCase(collection.slug)}${adapter.versionsSuffix}`, ) - const versionFields = buildVersionCollectionFields(payload.config, collection) + const versionFields = buildVersionCollectionFields(payload.config, collection, true) const versionPathsToQuery: PathsToQuery = new Set() traverseFields({ @@ -163,7 +163,7 @@ export const migratePostgresV2toV3 = async ({ debug, payload, req }: Args) => { columnPrefix: '', db, disableNotNull: false, - fields: global.fields, + fields: global.flattenedFields, globalSlug: global.slug, isVersions: false, newTableName: tableName, @@ -178,7 +178,7 @@ export const migratePostgresV2toV3 = async ({ debug, payload, req }: Args) => { adapter, db, debug, - fields: global.fields, + fields: global.flattenedFields, globalSlug: global.slug, isVersions: false, pathsToQuery, @@ -192,7 +192,7 @@ export const migratePostgresV2toV3 = async ({ debug, payload, req }: Args) => { `_${toSnakeCase(global.slug)}${adapter.versionsSuffix}`, ) - const versionFields = buildVersionGlobalFields(payload.config, global) + const versionFields = buildVersionGlobalFields(payload.config, global, true) const versionPathsToQuery: PathsToQuery = new Set() diff --git a/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/migrateRelationships.ts b/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/migrateRelationships.ts index e16ed08b61d..fd2b5397cd5 100644 --- a/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/migrateRelationships.ts +++ b/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/migrateRelationships.ts @@ -1,5 +1,5 @@ import type { TransactionPg } from '@payloadcms/drizzle/types' -import type { Field, Payload, PayloadRequest } from 'payload' +import type { FlattenedField, Payload, PayloadRequest } from 'payload' import { sql } from 'drizzle-orm' @@ -13,7 +13,7 @@ type Args = { collectionSlug?: string db: TransactionPg debug: boolean - fields: Field[] + fields: FlattenedField[] globalSlug?: string isVersions: boolean pathsToQuery: PathsToQuery diff --git a/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/traverseFields.ts b/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/traverseFields.ts index 025b492ba17..cc16ee906b5 100644 --- a/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/traverseFields.ts +++ b/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/traverseFields.ts @@ -1,7 +1,6 @@ import type { TransactionPg } from '@payloadcms/drizzle/types' -import type { Field, Payload } from 'payload' +import type { FlattenedField, Payload } from 'payload' -import { tabHasName } from 'payload/shared' import toSnakeCase from 'to-snake-case' import type { VercelPostgresAdapter } from '../../types.js' @@ -13,7 +12,7 @@ type Args = { columnPrefix: string db: TransactionPg disableNotNull: boolean - fields: Field[] + fields: FlattenedField[] globalSlug?: string isVersions: boolean newTableName: string @@ -35,7 +34,7 @@ export const traverseFields = (args: Args) => { return traverseFields({ ...args, columnPrefix: '', - fields: field.fields, + fields: field.flattenedFields, newTableName, parentTableName: newTableName, path: `${args.path ? `${args.path}.` : ''}${field.name}.%`, @@ -51,7 +50,7 @@ export const traverseFields = (args: Args) => { traverseFields({ ...args, columnPrefix: '', - fields: block.fields, + fields: block.flattenedFields, newTableName, parentTableName: newTableName, path: `${args.path ? `${args.path}.` : ''}${field.name}.%`, @@ -59,15 +58,8 @@ export const traverseFields = (args: Args) => { }) } - case 'collapsible': - case 'row': { - return traverseFields({ - ...args, - fields: field.fields, - }) - } - - case 'group': { + case 'group': + case 'tab': { let newTableName = `${args.newTableName}_${toSnakeCase(field.name)}` if (field.localized && args.payload.config.localization) { @@ -77,7 +69,7 @@ export const traverseFields = (args: Args) => { return traverseFields({ ...args, columnPrefix: `${args.columnPrefix}${toSnakeCase(field.name)}_`, - fields: field.fields, + fields: field.flattenedFields, newTableName, path: `${args.path ? `${args.path}.` : ''}${field.name}`, }) @@ -93,24 +85,6 @@ export const traverseFields = (args: Args) => { return null } - case 'tabs': { - return field.tabs.forEach((tab) => { - if (tabHasName(tab)) { - args.columnPrefix = `${args.columnPrefix}_${toSnakeCase(tab.name)}_` - args.path = `${args.path ? `${args.path}.` : ''}${tab.name}` - args.newTableName = `${args.newTableName}_${toSnakeCase(tab.name)}` - - if (tab.localized && args.payload.config.localization) { - args.newTableName += args.adapter.localesSuffix - } - } - - traverseFields({ - ...args, - fields: tab.fields, - }) - }) - } } }) } diff --git a/packages/drizzle/src/count.ts b/packages/drizzle/src/count.ts index 3d015768b21..6f638fd6822 100644 --- a/packages/drizzle/src/count.ts +++ b/packages/drizzle/src/count.ts @@ -18,7 +18,7 @@ export const count: Count = async function count( const { joins, where } = buildQuery({ adapter: this, - fields: collectionConfig.fields, + fields: collectionConfig.flattenedFields, locale, tableName, where: whereArg, diff --git a/packages/drizzle/src/countGlobalVersions.ts b/packages/drizzle/src/countGlobalVersions.ts index ba902026492..a1101faa8a9 100644 --- a/packages/drizzle/src/countGlobalVersions.ts +++ b/packages/drizzle/src/countGlobalVersions.ts @@ -21,7 +21,7 @@ export const countGlobalVersions: CountGlobalVersions = async function countGlob const db = this.sessions[await req?.transactionID]?.db || this.drizzle - const fields = buildVersionGlobalFields(this.payload.config, globalConfig) + const fields = buildVersionGlobalFields(this.payload.config, globalConfig, true) const { joins, where } = buildQuery({ adapter: this, diff --git a/packages/drizzle/src/countVersions.ts b/packages/drizzle/src/countVersions.ts index c4017acfaa6..7b9ea39e500 100644 --- a/packages/drizzle/src/countVersions.ts +++ b/packages/drizzle/src/countVersions.ts @@ -19,7 +19,7 @@ export const countVersions: CountVersions = async function countVersions( const db = this.sessions[await req?.transactionID]?.db || this.drizzle - const fields = buildVersionCollectionFields(this.payload.config, collectionConfig) + const fields = buildVersionCollectionFields(this.payload.config, collectionConfig, true) const { joins, where } = buildQuery({ adapter: this, diff --git a/packages/drizzle/src/create.ts b/packages/drizzle/src/create.ts index 9d40afb01c3..89d3132fc63 100644 --- a/packages/drizzle/src/create.ts +++ b/packages/drizzle/src/create.ts @@ -19,7 +19,7 @@ export const create: Create = async function create( adapter: this, data, db, - fields: collection.fields, + fields: collection.flattenedFields, operation: 'create', req, select, diff --git a/packages/drizzle/src/createGlobal.ts b/packages/drizzle/src/createGlobal.ts index 18b7dc7b88b..4ef9943e679 100644 --- a/packages/drizzle/src/createGlobal.ts +++ b/packages/drizzle/src/createGlobal.ts @@ -19,7 +19,7 @@ export async function createGlobal>( adapter: this, data, db, - fields: globalConfig.fields, + fields: globalConfig.flattenedFields, operation: 'create', req, tableName, diff --git a/packages/drizzle/src/createGlobalVersion.ts b/packages/drizzle/src/createGlobalVersion.ts index 999cfb60f81..f2293f04389 100644 --- a/packages/drizzle/src/createGlobalVersion.ts +++ b/packages/drizzle/src/createGlobalVersion.ts @@ -39,7 +39,7 @@ export async function createGlobalVersion( version: versionData, }, db, - fields: buildVersionGlobalFields(this.payload.config, global), + fields: buildVersionGlobalFields(this.payload.config, global, true), operation: 'create', req, select, diff --git a/packages/drizzle/src/createVersion.ts b/packages/drizzle/src/createVersion.ts index 4f41aa7aca7..c3d637e9835 100644 --- a/packages/drizzle/src/createVersion.ts +++ b/packages/drizzle/src/createVersion.ts @@ -49,7 +49,7 @@ export async function createVersion( adapter: this, data, db, - fields: buildVersionCollectionFields(this.payload.config, collection), + fields: buildVersionCollectionFields(this.payload.config, collection, true), operation: 'create', req, select, diff --git a/packages/drizzle/src/deleteMany.ts b/packages/drizzle/src/deleteMany.ts index 55c878a0824..b228a38416d 100644 --- a/packages/drizzle/src/deleteMany.ts +++ b/packages/drizzle/src/deleteMany.ts @@ -18,7 +18,7 @@ export const deleteMany: DeleteMany = async function deleteMany( const result = await findMany({ adapter: this, - fields: collectionConfig.fields, + fields: collectionConfig.flattenedFields, joins: false, limit: 0, locale: req.locale, diff --git a/packages/drizzle/src/deleteOne.ts b/packages/drizzle/src/deleteOne.ts index 1c3f726a058..581b8760021 100644 --- a/packages/drizzle/src/deleteOne.ts +++ b/packages/drizzle/src/deleteOne.ts @@ -23,7 +23,7 @@ export const deleteOne: DeleteOne = async function deleteOne( const { joins, selectFields, where } = buildQuery({ adapter: this, - fields: collection.fields, + fields: collection.flattenedFields, locale: req.locale, tableName, where: whereArg, @@ -47,7 +47,7 @@ export const deleteOne: DeleteOne = async function deleteOne( const findManyArgs = buildFindManyArgs({ adapter: this, depth: 0, - fields: collection.fields, + fields: collection.flattenedFields, joinQuery: false, select, tableName, @@ -62,7 +62,7 @@ export const deleteOne: DeleteOne = async function deleteOne( adapter: this, config: this.payload.config, data: docToDelete, - fields: collection.fields, + fields: collection.flattenedFields, joinQuery: false, }) diff --git a/packages/drizzle/src/deleteVersions.ts b/packages/drizzle/src/deleteVersions.ts index 5bbcc02df13..a62ac962c23 100644 --- a/packages/drizzle/src/deleteVersions.ts +++ b/packages/drizzle/src/deleteVersions.ts @@ -19,7 +19,7 @@ export const deleteVersions: DeleteVersions = async function deleteVersion( `_${toSnakeCase(collectionConfig.slug)}${this.versionsSuffix}`, ) - const fields = buildVersionCollectionFields(this.payload.config, collectionConfig) + const fields = buildVersionCollectionFields(this.payload.config, collectionConfig, true) const { docs } = await findMany({ adapter: this, diff --git a/packages/drizzle/src/find.ts b/packages/drizzle/src/find.ts index 66b596ddba1..2cd87b486c6 100644 --- a/packages/drizzle/src/find.ts +++ b/packages/drizzle/src/find.ts @@ -28,7 +28,7 @@ export const find: Find = async function find( return findMany({ adapter: this, - fields: collectionConfig.fields, + fields: collectionConfig.flattenedFields, joins, limit, locale, diff --git a/packages/drizzle/src/find/buildFindManyArgs.ts b/packages/drizzle/src/find/buildFindManyArgs.ts index 3614aa84fd6..280dd92cf40 100644 --- a/packages/drizzle/src/find/buildFindManyArgs.ts +++ b/packages/drizzle/src/find/buildFindManyArgs.ts @@ -1,5 +1,5 @@ import type { DBQueryConfig } from 'drizzle-orm' -import type { Field, JoinQuery, SelectType } from 'payload' +import type { FlattenedField, JoinQuery, SelectType } from 'payload' import { getSelectMode } from 'payload/shared' @@ -10,7 +10,7 @@ import { traverseFields } from './traverseFields.js' type BuildFindQueryArgs = { adapter: DrizzleAdapter depth: number - fields: Field[] + fields: FlattenedField[] joinQuery?: JoinQuery /** * The joins array will be mutated by pushing any joins needed for the where queries of join field joins diff --git a/packages/drizzle/src/find/findMany.ts b/packages/drizzle/src/find/findMany.ts index 84491adc97e..0c2b7a1b13b 100644 --- a/packages/drizzle/src/find/findMany.ts +++ b/packages/drizzle/src/find/findMany.ts @@ -1,4 +1,4 @@ -import type { Field, FindArgs, PayloadRequest, TypeWithID } from 'payload' +import type { FindArgs, FlattenedField, PayloadRequest, TypeWithID } from 'payload' import { inArray } from 'drizzle-orm' @@ -12,7 +12,7 @@ import { buildFindManyArgs } from './buildFindManyArgs.js' type Args = { adapter: DrizzleAdapter - fields: Field[] + fields: FlattenedField[] tableName: string versions?: boolean } & Omit diff --git a/packages/drizzle/src/find/traverseFields.ts b/packages/drizzle/src/find/traverseFields.ts index 10e6b0feb85..36a895a3efc 100644 --- a/packages/drizzle/src/find/traverseFields.ts +++ b/packages/drizzle/src/find/traverseFields.ts @@ -1,9 +1,8 @@ import type { LibSQLDatabase } from 'drizzle-orm/libsql' -import type { Field, JoinQuery, SelectMode, SelectType, TabAsField } from 'payload' +import type { FlattenedField, JoinQuery, SelectMode, SelectType } from 'payload' import { and, eq, sql } from 'drizzle-orm' -import { combineQueries } from 'payload' -import { fieldAffectsData, fieldIsVirtual, tabHasName } from 'payload/shared' +import { fieldIsVirtual } from 'payload/shared' import toSnakeCase from 'to-snake-case' import type { BuildQueryJoinAliases, ChainedMethods, DrizzleAdapter } from '../types.js' @@ -18,7 +17,7 @@ type TraverseFieldArgs = { currentArgs: Result currentTableName: string depth?: number - fields: (Field | TabAsField)[] + fields: FlattenedField[] joinQuery: JoinQuery joins?: BuildQueryJoinAliases locale?: string @@ -78,462 +77,407 @@ export const traverseFields = ({ } } - if ( - field.type === 'collapsible' || - field.type === 'row' || - (field.type === 'tab' && !tabHasName(field)) - ) { - traverseFields({ - _locales, - adapter, - currentArgs, - currentTableName, - depth, - fields: field.fields, - joinQuery, - joins, - path, - select, - selectMode, - tablePath, - topLevelArgs, - topLevelTableName, - withTabledFields, - }) + switch (field.type) { + case 'array': { + const arraySelect = selectAllOnCurrentLevel ? true : select?.[field.name] - return - } + if (select) { + if ( + (selectMode === 'include' && typeof arraySelect === 'undefined') || + (selectMode === 'exclude' && arraySelect === false) + ) { + break + } + } - if (field.type === 'tabs') { - traverseFields({ - _locales, - adapter, - currentArgs, - currentTableName, - depth, - fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })), - joinQuery, - joins, - path, - select, - selectAllOnCurrentLevel, - selectMode, - tablePath, - topLevelArgs, - topLevelTableName, - versions, - withTabledFields, - }) + const withArray: Result = { + columns: + typeof arraySelect === 'object' + ? { + id: true, + _order: true, + } + : { + _parentID: false, + }, + orderBy: ({ _order }, { asc }) => [asc(_order)], + with: {}, + } - return - } + const arrayTableName = adapter.tableNameMap.get( + `${currentTableName}_${tablePath}${toSnakeCase(field.name)}`, + ) - if (fieldAffectsData(field)) { - switch (field.type) { - case 'array': { - const arraySelect = selectAllOnCurrentLevel ? true : select?.[field.name] + if (typeof arraySelect === 'object') { + if (adapter.tables[arrayTableName]._locale) { + withArray.columns._locale = true + } - if (select) { - if ( - (selectMode === 'include' && typeof arraySelect === 'undefined') || - (selectMode === 'exclude' && arraySelect === false) - ) { - break - } + if (adapter.tables[arrayTableName]._uuid) { + withArray.columns._uuid = true } + } + + const arrayTableNameWithLocales = `${arrayTableName}${adapter.localesSuffix}` - const withArray: Result = { + if (adapter.tables[arrayTableNameWithLocales]) { + withArray.with._locales = { columns: typeof arraySelect === 'object' ? { - id: true, - _order: true, + _locale: true, } : { + id: false, _parentID: false, }, - orderBy: ({ _order }, { asc }) => [asc(_order)], with: {}, } + } - const arrayTableName = adapter.tableNameMap.get( - `${currentTableName}_${tablePath}${toSnakeCase(field.name)}`, - ) - - if (typeof arraySelect === 'object') { - if (adapter.tables[arrayTableName]._locale) { - withArray.columns._locale = true - } - - if (adapter.tables[arrayTableName]._uuid) { - withArray.columns._uuid = true - } - } - - const arrayTableNameWithLocales = `${arrayTableName}${adapter.localesSuffix}` + currentArgs.with[`${path}${field.name}`] = withArray + + traverseFields({ + _locales: withArray.with._locales, + adapter, + currentArgs: withArray, + currentTableName: arrayTableName, + depth, + fields: field.flattenedFields, + joinQuery, + path: '', + select: typeof arraySelect === 'object' ? arraySelect : undefined, + selectMode, + tablePath: '', + topLevelArgs, + topLevelTableName, + withinLocalizedField: withinLocalizedField || field.localized, + withTabledFields, + }) + + if ( + typeof arraySelect === 'object' && + withArray.with._locales && + Object.keys(withArray.with._locales).length === 1 + ) { + delete withArray.with._locales + } - if (adapter.tables[arrayTableNameWithLocales]) { - withArray.with._locales = { - columns: - typeof arraySelect === 'object' - ? { - _locale: true, - } - : { - id: false, - _parentID: false, - }, - with: {}, - } - } + break + } - currentArgs.with[`${path}${field.name}`] = withArray - - traverseFields({ - _locales: withArray.with._locales, - adapter, - currentArgs: withArray, - currentTableName: arrayTableName, - depth, - fields: field.fields, - joinQuery, - path: '', - select: typeof arraySelect === 'object' ? arraySelect : undefined, - selectMode, - tablePath: '', - topLevelArgs, - topLevelTableName, - withinLocalizedField: withinLocalizedField || field.localized, - withTabledFields, - }) + case 'blocks': { + const blocksSelect = selectAllOnCurrentLevel ? true : select?.[field.name] + if (select) { if ( - typeof arraySelect === 'object' && - withArray.with._locales && - Object.keys(withArray.with._locales).length === 1 + (selectMode === 'include' && !blocksSelect) || + (selectMode === 'exclude' && blocksSelect === false) ) { - delete withArray.with._locales + break } - - break } - case 'blocks': { - const blocksSelect = selectAllOnCurrentLevel ? true : select?.[field.name] + field.blocks.forEach((block) => { + const blockKey = `_blocks_${block.slug}` - if (select) { - if ( - (selectMode === 'include' && !blocksSelect) || - (selectMode === 'exclude' && blocksSelect === false) + let blockSelect: boolean | SelectType | undefined + + let blockSelectMode = selectMode + + if (selectMode === 'include' && blocksSelect === true) { + blockSelect = true + } + + if (typeof blocksSelect === 'object') { + if (typeof blocksSelect[block.slug] === 'object') { + blockSelect = blocksSelect[block.slug] + } else if ( + (selectMode === 'include' && typeof blocksSelect[block.slug] === 'undefined') || + (selectMode === 'exclude' && blocksSelect[block.slug] === false) ) { - break + blockSelect = {} + blockSelectMode = 'include' + } else if (selectMode === 'include' && blocksSelect[block.slug] === true) { + blockSelect = true } } - field.blocks.forEach((block) => { - const blockKey = `_blocks_${block.slug}` - - let blockSelect: boolean | SelectType | undefined + if (!topLevelArgs[blockKey]) { + const withBlock: Result = { + columns: + typeof blockSelect === 'object' + ? { + id: true, + _order: true, + _path: true, + } + : { + _parentID: false, + }, + orderBy: ({ _order }, { asc }) => [asc(_order)], + with: {}, + } - let blockSelectMode = selectMode + const tableName = adapter.tableNameMap.get( + `${topLevelTableName}_blocks_${toSnakeCase(block.slug)}`, + ) - if (selectMode === 'include' && blocksSelect === true) { - blockSelect = true - } + if (typeof blockSelect === 'object') { + if (adapter.tables[tableName]._locale) { + withBlock.columns._locale = true + } - if (typeof blocksSelect === 'object') { - if (typeof blocksSelect[block.slug] === 'object') { - blockSelect = blocksSelect[block.slug] - } else if ( - (selectMode === 'include' && typeof blocksSelect[block.slug] === 'undefined') || - (selectMode === 'exclude' && blocksSelect[block.slug] === false) - ) { - blockSelect = {} - blockSelectMode = 'include' - } else if (selectMode === 'include' && blocksSelect[block.slug] === true) { - blockSelect = true + if (adapter.tables[tableName]._uuid) { + withBlock.columns._uuid = true } } - if (!topLevelArgs[blockKey]) { - const withBlock: Result = { - columns: - typeof blockSelect === 'object' - ? { - id: true, - _order: true, - _path: true, - } - : { - _parentID: false, - }, - orderBy: ({ _order }, { asc }) => [asc(_order)], + if (adapter.tables[`${tableName}${adapter.localesSuffix}`]) { + withBlock.with._locales = { with: {}, } - const tableName = adapter.tableNameMap.get( - `${topLevelTableName}_blocks_${toSnakeCase(block.slug)}`, - ) - if (typeof blockSelect === 'object') { - if (adapter.tables[tableName]._locale) { - withBlock.columns._locale = true - } - - if (adapter.tables[tableName]._uuid) { - withBlock.columns._uuid = true + withBlock.with._locales.columns = { + _locale: true, } } + } + topLevelArgs.with[blockKey] = withBlock + + traverseFields({ + _locales: withBlock.with._locales, + adapter, + currentArgs: withBlock, + currentTableName: tableName, + depth, + fields: block.flattenedFields, + joinQuery, + path: '', + select: typeof blockSelect === 'object' ? blockSelect : undefined, + selectMode: blockSelectMode, + tablePath: '', + topLevelArgs, + topLevelTableName, + withinLocalizedField: withinLocalizedField || field.localized, + withTabledFields, + }) - if (adapter.tables[`${tableName}${adapter.localesSuffix}`]) { - withBlock.with._locales = { - with: {}, - } - - if (typeof blockSelect === 'object') { - withBlock.with._locales.columns = { - _locale: true, - } - } - } - topLevelArgs.with[blockKey] = withBlock - - traverseFields({ - _locales: withBlock.with._locales, - adapter, - currentArgs: withBlock, - currentTableName: tableName, - depth, - fields: block.fields, - joinQuery, - path: '', - select: typeof blockSelect === 'object' ? blockSelect : undefined, - selectMode: blockSelectMode, - tablePath: '', - topLevelArgs, - topLevelTableName, - withinLocalizedField: withinLocalizedField || field.localized, - withTabledFields, - }) - - if ( - typeof blockSelect === 'object' && - withBlock.with._locales && - Object.keys(withBlock.with._locales.columns).length === 1 - ) { - delete withBlock.with._locales - } + if ( + typeof blockSelect === 'object' && + withBlock.with._locales && + Object.keys(withBlock.with._locales.columns).length === 1 + ) { + delete withBlock.with._locales } - }) + } + }) + break + } + + case 'group': + case 'tab': { + const fieldSelect = select?.[field.name] + + if (fieldSelect === false) { break } - case 'group': - case 'tab': { - const fieldSelect = select?.[field.name] + traverseFields({ + _locales, + adapter, + currentArgs, + currentTableName, + depth, + fields: field.flattenedFields, + joinQuery, + joins, + path: `${path}${field.name}_`, + select: typeof fieldSelect === 'object' ? fieldSelect : undefined, + selectAllOnCurrentLevel: + selectAllOnCurrentLevel || + fieldSelect === true || + (selectMode === 'exclude' && typeof fieldSelect === 'undefined'), + selectMode, + tablePath: `${tablePath}${toSnakeCase(field.name)}_`, + topLevelArgs, + topLevelTableName, + versions, + withinLocalizedField: withinLocalizedField || field.localized, + withTabledFields, + }) + + break + } + case 'join': { + // when `joinsQuery` is false, do not join + if (joinQuery === false) { + break + } - if (fieldSelect === false) { - break - } + if ( + (select && selectMode === 'include' && !select[field.name]) || + (selectMode === 'exclude' && select[field.name] === false) + ) { + break + } - traverseFields({ - _locales, - adapter, - currentArgs, - currentTableName, - depth, - fields: field.fields, - joinQuery, - joins, - path: `${path}${field.name}_`, - select: typeof fieldSelect === 'object' ? fieldSelect : undefined, - selectAllOnCurrentLevel: - selectAllOnCurrentLevel || - fieldSelect === true || - (selectMode === 'exclude' && typeof fieldSelect === 'undefined'), - selectMode, - tablePath: `${tablePath}${toSnakeCase(field.name)}_`, - topLevelArgs, - topLevelTableName, - versions, - withinLocalizedField: withinLocalizedField || field.localized, - withTabledFields, - }) + const joinSchemaPath = `${path.replaceAll('_', '.')}${field.name}` + if (joinQuery[joinSchemaPath] === false) { break } - case 'join': { - // when `joinsQuery` is false, do not join - if (joinQuery === false) { - break - } + const { + limit: limitArg = field.defaultLimit ?? 10, + sort = field.defaultSort, + where, + } = joinQuery[joinSchemaPath] || {} + let limit = limitArg - if ( - (select && selectMode === 'include' && !select[field.name]) || - (selectMode === 'exclude' && select[field.name] === false) - ) { - break - } + if (limit !== 0) { + // get an additional document and slice it later to determine if there is a next page + limit += 1 + } - const joinSchemaPath = `${path.replaceAll('_', '.')}${field.name}` + const fields = adapter.payload.collections[field.collection].config.flattenedFields - if (joinQuery[joinSchemaPath] === false) { - break - } + const joinCollectionTableName = adapter.tableNameMap.get(toSnakeCase(field.collection)) - const { - limit: limitArg = field.defaultLimit ?? 10, - sort = field.defaultSort, - where, - } = joinQuery[joinSchemaPath] || {} - let limit = limitArg + const joins: BuildQueryJoinAliases = [] - if (limit !== 0) { - // get an additional document and slice it later to determine if there is a next page - limit += 1 - } + const buildQueryResult = buildQuery({ + adapter, + fields, + joins, + locale, + sort, + tableName: joinCollectionTableName, + where, + }) - const fields = adapter.payload.collections[field.collection].config.fields + let subQueryWhere = buildQueryResult.where + const orderBy = buildQueryResult.orderBy - const joinCollectionTableName = adapter.tableNameMap.get(toSnakeCase(field.collection)) + let joinLocalesCollectionTableName: string | undefined - const joins: BuildQueryJoinAliases = [] + const currentIDColumn = versions + ? adapter.tables[currentTableName].parent + : adapter.tables[currentTableName].id - const buildQueryResult = buildQuery({ - adapter, - fields, - joins, - locale, - sort, - tableName: joinCollectionTableName, - where, - }) + // Handle hasMany _rels table + if (field.hasMany) { + const joinRelsCollectionTableName = `${joinCollectionTableName}${adapter.relationshipsSuffix}` - let subQueryWhere = buildQueryResult.where - const orderBy = buildQueryResult.orderBy - - let joinLocalesCollectionTableName: string | undefined + if (field.localized) { + joinLocalesCollectionTableName = joinRelsCollectionTableName + } - const currentIDColumn = versions - ? adapter.tables[currentTableName].parent - : adapter.tables[currentTableName].id + let columnReferenceToCurrentID: string - // Handle hasMany _rels table - if (field.hasMany) { - const joinRelsCollectionTableName = `${joinCollectionTableName}${adapter.relationshipsSuffix}` + if (versions) { + columnReferenceToCurrentID = `${topLevelTableName + .replace('_', '') + .replace(new RegExp(`${adapter.versionsSuffix}$`), '')}_id` + } else { + columnReferenceToCurrentID = `${topLevelTableName}_id` + } - if (field.localized) { - joinLocalesCollectionTableName = joinRelsCollectionTableName - } + 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 - let columnReferenceToCurrentID: string + const foreignColumn = field.on.replaceAll('.', '_') - if (versions) { - columnReferenceToCurrentID = `${topLevelTableName - .replace('_', '') - .replace(new RegExp(`${adapter.versionsSuffix}$`), '')}_id` - } else { - columnReferenceToCurrentID = `${topLevelTableName}_id` - } + if (field.localized) { + joinLocalesCollectionTableName = `${joinCollectionTableName}${adapter.localesSuffix}` joins.push({ type: 'innerJoin', condition: and( eq( - adapter.tables[joinRelsCollectionTableName].parent, + adapter.tables[joinLocalesCollectionTableName]._parentID, adapter.tables[joinCollectionTableName].id, ), - eq( - sql.raw(`"${joinRelsCollectionTableName}"."${columnReferenceToCurrentID}"`), - currentIDColumn, - ), - eq(adapter.tables[joinRelsCollectionTableName].path, field.on), + eq(adapter.tables[joinLocalesCollectionTableName][foreignColumn], currentIDColumn), ), - table: adapter.tables[joinRelsCollectionTableName], + table: adapter.tables[joinLocalesCollectionTableName], }) + // Handle without localized and without hasMany, just a condition append to where. With localized the inner join handles eq. } 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, - ) + const constraint = eq( + adapter.tables[joinCollectionTableName][foreignColumn], + currentIDColumn, + ) - if (subQueryWhere) { - subQueryWhere = and(subQueryWhere, constraint) - } else { - subQueryWhere = constraint - } + if (subQueryWhere) { + subQueryWhere = and(subQueryWhere, constraint) + } else { + subQueryWhere = constraint } } + } - const chainedMethods: ChainedMethods = [] + const chainedMethods: ChainedMethods = [] - joins.forEach(({ type, condition, table }) => { - chainedMethods.push({ - args: [table, condition], - method: type ?? 'leftJoin', - }) + joins.forEach(({ type, condition, table }) => { + chainedMethods.push({ + args: [table, condition], + method: type ?? 'leftJoin', }) + }) - if (limit !== 0) { - chainedMethods.push({ - args: [limit], - method: 'limit', - }) - } - - const db = adapter.drizzle as LibSQLDatabase - - const subQuery = chainMethods({ - methods: chainedMethods, - 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.map(({ column, order }) => order(column))), + if (limit !== 0) { + chainedMethods.push({ + args: [limit], + method: 'limit', }) + } + + const db = adapter.drizzle as LibSQLDatabase + + const subQuery = chainMethods({ + methods: chainedMethods, + 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.map(({ column, order }) => order(column))), + }) - const columnName = `${path.replaceAll('.', '_')}${field.name}` + const columnName = `${path.replaceAll('.', '_')}${field.name}` - const jsonObjectSelect = field.localized - ? sql.raw( - `'_parentID', "id", '_locale', "${adapter.tables[joinLocalesCollectionTableName].locale ? 'locale' : '_locale'}"`, - ) - : sql.raw(`'id', "id"`) + 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` + if (adapter.name === 'sqlite') { + currentArgs.extras[columnName] = sql` COALESCE(( SELECT json_group_array(json_object(${jsonObjectSelect})) FROM ( @@ -541,8 +485,8 @@ export const traverseFields = ({ ) AS ${sql.raw(`${columnName}_sub`)} ), '[]') `.as(columnName) - } else { - currentArgs.extras[columnName] = sql` + } else { + currentArgs.extras[columnName] = sql` COALESCE(( SELECT json_agg(json_build_object(${jsonObjectSelect})) FROM ( @@ -550,114 +494,113 @@ export const traverseFields = ({ ) AS ${sql.raw(`${columnName}_sub`)} ), '[]'::json) `.as(columnName) - } + } + + break + } + case 'point': { + if (adapter.name === 'sqlite') { break } - case 'point': { - if (adapter.name === 'sqlite') { - break - } + const args = field.localized ? _locales : currentArgs + if (!args.columns) { + args.columns = {} + } - const args = field.localized ? _locales : currentArgs - if (!args.columns) { - args.columns = {} - } + if (!args.extras) { + args.extras = {} + } - if (!args.extras) { - args.extras = {} - } + const name = `${path}${field.name}` - const name = `${path}${field.name}` + // Drizzle handles that poorly. See https://github.com/drizzle-team/drizzle-orm/issues/2526 + // Additionally, this way we format the column value straight in the database using ST_AsGeoJSON + args.columns[name] = false - // Drizzle handles that poorly. See https://github.com/drizzle-team/drizzle-orm/issues/2526 - // Additionally, this way we format the column value straight in the database using ST_AsGeoJSON - args.columns[name] = false + let shouldSelect = false - let shouldSelect = false + if (select || selectAllOnCurrentLevel) { + if ( + selectAllOnCurrentLevel || + (selectMode === 'include' && select[field.name] === true) || + (selectMode === 'exclude' && typeof select[field.name] === 'undefined') + ) { + shouldSelect = true + } + } else { + shouldSelect = true + } - if (select || selectAllOnCurrentLevel) { + if (shouldSelect) { + args.extras[name] = sql.raw(`ST_AsGeoJSON(${toSnakeCase(name)})::jsonb`).as(name) + } + break + } + + case 'select': { + if (field.hasMany) { + if (select) { if ( - selectAllOnCurrentLevel || - (selectMode === 'include' && select[field.name] === true) || - (selectMode === 'exclude' && typeof select[field.name] === 'undefined') + (selectMode === 'include' && !select[field.name]) || + (selectMode === 'exclude' && select[field.name] === false) ) { - shouldSelect = true + break } - } else { - shouldSelect = true } - if (shouldSelect) { - args.extras[name] = sql.raw(`ST_AsGeoJSON(${toSnakeCase(name)})::jsonb`).as(name) + const withSelect: Result = { + columns: { + id: false, + order: false, + parent: false, + }, + orderBy: ({ order }, { asc }) => [asc(order)], } - break - } - case 'select': { - if (field.hasMany) { - if (select) { - if ( - (selectMode === 'include' && !select[field.name]) || - (selectMode === 'exclude' && select[field.name] === false) - ) { - break - } - } - - const withSelect: Result = { - columns: { - id: false, - order: false, - parent: false, - }, - orderBy: ({ order }, { asc }) => [asc(order)], - } + currentArgs.with[`${path}${field.name}`] = withSelect + } - currentArgs.with[`${path}${field.name}`] = withSelect - } + break + } + default: { + if (!select && !selectAllOnCurrentLevel) { break } - default: { - if (!select && !selectAllOnCurrentLevel) { - break + if ( + selectAllOnCurrentLevel || + (selectMode === 'include' && select[field.name] === true) || + (selectMode === 'exclude' && typeof select[field.name] === 'undefined') + ) { + const fieldPath = `${path}${field.name}` + + if ((field.localized || withinLocalizedField) && _locales) { + _locales.columns[fieldPath] = true + } else if (adapter.tables[currentTableName]?.[fieldPath]) { + currentArgs.columns[fieldPath] = true } if ( - selectAllOnCurrentLevel || - (selectMode === 'include' && select[field.name] === true) || - (selectMode === 'exclude' && typeof select[field.name] === 'undefined') + !withTabledFields.rels && + field.type === 'relationship' && + (field.hasMany || Array.isArray(field.relationTo)) ) { - const fieldPath = `${path}${field.name}` - - if ((field.localized || withinLocalizedField) && _locales) { - _locales.columns[fieldPath] = true - } else if (adapter.tables[currentTableName]?.[fieldPath]) { - currentArgs.columns[fieldPath] = true - } - - if ( - !withTabledFields.rels && - field.type === 'relationship' && - (field.hasMany || Array.isArray(field.relationTo)) - ) { - withTabledFields.rels = true - } - - if (!withTabledFields.numbers && field.type === 'number' && field.hasMany) { - withTabledFields.numbers = true - } + withTabledFields.rels = true + } - if (!withTabledFields.texts && field.type === 'text' && field.hasMany) { - withTabledFields.texts = true - } + if (!withTabledFields.numbers && field.type === 'number' && field.hasMany) { + withTabledFields.numbers = true } - break + if (!withTabledFields.texts && field.type === 'text' && field.hasMany) { + withTabledFields.texts = true + } } + + break } } }) diff --git a/packages/drizzle/src/findGlobal.ts b/packages/drizzle/src/findGlobal.ts index f2873cd68a4..42905b5bcf2 100644 --- a/packages/drizzle/src/findGlobal.ts +++ b/packages/drizzle/src/findGlobal.ts @@ -18,7 +18,7 @@ export const findGlobal: FindGlobal = async function findGlobal( docs: [doc], } = await findMany({ adapter: this, - fields: globalConfig.fields, + fields: globalConfig.flattenedFields, limit: 1, locale, pagination: false, diff --git a/packages/drizzle/src/findGlobalVersions.ts b/packages/drizzle/src/findGlobalVersions.ts index 7e47c62d0c3..85b5e4a687b 100644 --- a/packages/drizzle/src/findGlobalVersions.ts +++ b/packages/drizzle/src/findGlobalVersions.ts @@ -31,7 +31,7 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV `_${toSnakeCase(globalConfig.slug)}${this.versionsSuffix}`, ) - const fields = buildVersionGlobalFields(this.payload.config, globalConfig) + const fields = buildVersionGlobalFields(this.payload.config, globalConfig, true) return findMany({ adapter: this, diff --git a/packages/drizzle/src/findOne.ts b/packages/drizzle/src/findOne.ts index fe8357bc134..2bc418fc755 100644 --- a/packages/drizzle/src/findOne.ts +++ b/packages/drizzle/src/findOne.ts @@ -16,7 +16,7 @@ export async function findOne( const { docs } = await findMany({ adapter: this, - fields: collectionConfig.fields, + fields: collectionConfig.flattenedFields, joins, limit: 1, locale, diff --git a/packages/drizzle/src/findVersions.ts b/packages/drizzle/src/findVersions.ts index 93b1807bcad..eee7d864c0e 100644 --- a/packages/drizzle/src/findVersions.ts +++ b/packages/drizzle/src/findVersions.ts @@ -29,7 +29,7 @@ export const findVersions: FindVersions = async function findVersions( `_${toSnakeCase(collectionConfig.slug)}${this.versionsSuffix}`, ) - const fields = buildVersionCollectionFields(this.payload.config, collectionConfig) + const fields = buildVersionCollectionFields(this.payload.config, collectionConfig, true) return findMany({ adapter: this, diff --git a/packages/drizzle/src/postgres/init.ts b/packages/drizzle/src/postgres/init.ts index c76f4d9457f..3de584419e8 100644 --- a/packages/drizzle/src/postgres/init.ts +++ b/packages/drizzle/src/postgres/init.ts @@ -56,7 +56,7 @@ export const init: Init = async function init(this: BasePostgresAdapter) { baseExtraConfig, disableNotNull: !!collection?.versions?.drafts, disableUnique: false, - fields: collection.fields, + fields: collection.flattenedFields, tableName, timestamps: collection.timestamps, versions: false, @@ -66,7 +66,7 @@ export const init: Init = async function init(this: BasePostgresAdapter) { const versionsTableName = this.tableNameMap.get( `_${toSnakeCase(collection.slug)}${this.versionsSuffix}`, ) - const versionFields = buildVersionCollectionFields(this.payload.config, collection) + const versionFields = buildVersionCollectionFields(this.payload.config, collection, true) buildTable({ adapter: this, @@ -87,7 +87,7 @@ export const init: Init = async function init(this: BasePostgresAdapter) { adapter: this, disableNotNull: !!global?.versions?.drafts, disableUnique: false, - fields: global.fields, + fields: global.flattenedFields, tableName, timestamps: false, versions: false, @@ -100,7 +100,7 @@ export const init: Init = async function init(this: BasePostgresAdapter) { versions: true, versionsCustomName: true, }) - const versionFields = buildVersionGlobalFields(this.payload.config, global) + const versionFields = buildVersionGlobalFields(this.payload.config, global, true) buildTable({ adapter: this, diff --git a/packages/drizzle/src/postgres/schema/build.ts b/packages/drizzle/src/postgres/schema/build.ts index e5a69ff2083..e60f1382dc4 100644 --- a/packages/drizzle/src/postgres/schema/build.ts +++ b/packages/drizzle/src/postgres/schema/build.ts @@ -5,7 +5,7 @@ import type { PgColumnBuilder, PgTableWithColumns, } from 'drizzle-orm/pg-core' -import type { Field, SanitizedJoins } from 'payload' +import type { FlattenedField } from 'payload' import { relations } from 'drizzle-orm' import { @@ -49,7 +49,7 @@ type Args = { disableNotNull: boolean disableRelsTableUnique?: boolean disableUnique: boolean - fields: Field[] + fields: FlattenedField[] rootRelationships?: Set rootRelationsToBuild?: RelationMap rootTableIDColType?: string diff --git a/packages/drizzle/src/postgres/schema/idToUUID.ts b/packages/drizzle/src/postgres/schema/idToUUID.ts index 466ec63b3d6..aa2b81f9452 100644 --- a/packages/drizzle/src/postgres/schema/idToUUID.ts +++ b/packages/drizzle/src/postgres/schema/idToUUID.ts @@ -1,6 +1,6 @@ -import type { Field } from 'payload' +import type { FlattenedField } from 'payload' -export const idToUUID = (fields: Field[]): Field[] => +export const idToUUID = (fields: FlattenedField[]): FlattenedField[] => fields.map((field) => { if ('name' in field && field.name === 'id') { return { diff --git a/packages/drizzle/src/postgres/schema/setColumnID.ts b/packages/drizzle/src/postgres/schema/setColumnID.ts index c913c1e9b55..11549a13569 100644 --- a/packages/drizzle/src/postgres/schema/setColumnID.ts +++ b/packages/drizzle/src/postgres/schema/setColumnID.ts @@ -1,20 +1,17 @@ import type { PgColumnBuilder } from 'drizzle-orm/pg-core' +import type { FlattenedField } from 'payload' import { numeric, serial, uuid, varchar } from 'drizzle-orm/pg-core' -import { type Field, flattenTopLevelFields } from 'payload' -import { fieldAffectsData } from 'payload/shared' import type { BasePostgresAdapter, IDType } from '../types.js' type Args = { adapter: BasePostgresAdapter columns: Record - fields: Field[] + fields: FlattenedField[] } export const setColumnID = ({ adapter, columns, fields }: Args): IDType => { - const idField = flattenTopLevelFields(fields).find( - (field) => fieldAffectsData(field) && field.name === 'id', - ) + const idField = fields.find((field) => field.name === 'id') if (idField) { if (idField.type === 'number') { columns.id = numeric('id').primaryKey() diff --git a/packages/drizzle/src/postgres/schema/traverseFields.ts b/packages/drizzle/src/postgres/schema/traverseFields.ts index f670e0f39fe..a1a441a7adc 100644 --- a/packages/drizzle/src/postgres/schema/traverseFields.ts +++ b/packages/drizzle/src/postgres/schema/traverseFields.ts @@ -1,12 +1,11 @@ import type { Relation } from 'drizzle-orm' import type { IndexBuilder, PgColumnBuilder } from 'drizzle-orm/pg-core' -import type { Field, SanitizedJoins, TabAsField } from 'payload' +import type { FlattenedField } from 'payload' import { relations } from 'drizzle-orm' import { boolean, foreignKey, - geometry, index, integer, jsonb, @@ -49,7 +48,7 @@ type Args = { disableRelsTableUnique?: boolean disableUnique?: boolean fieldPrefix?: string - fields: (Field | TabAsField)[] + fields: FlattenedField[] forceLocalized?: boolean indexes: Record IndexBuilder> localesColumns: Record @@ -130,55 +129,50 @@ export const traverseFields = ({ return } - let columnName: string - let fieldName: string - let targetTable = columns let targetIndexes = indexes - if (fieldAffectsData(field)) { - columnName = `${columnPrefix || ''}${field.name[0] === '_' ? '_' : ''}${toSnakeCase( - field.name, - )}` - fieldName = `${fieldPrefix?.replace('.', '_') || ''}${field.name}` - - // If field is localized, - // add the column to the locale table instead of main table - if ( - adapter.payload.config.localization && - (field.localized || forceLocalized) && - field.type !== 'array' && - field.type !== 'blocks' && - (('hasMany' in field && field.hasMany !== true) || !('hasMany' in field)) - ) { - hasLocalizedField = true - targetTable = localesColumns - targetIndexes = localesIndexes - } + const columnName = `${columnPrefix || ''}${field.name[0] === '_' ? '_' : ''}${toSnakeCase( + field.name, + )}` + const fieldName = `${fieldPrefix?.replace('.', '_') || ''}${field.name}` - if ( - (field.unique || field.index || ['relationship', 'upload'].includes(field.type)) && - !['array', 'blocks', 'group'].includes(field.type) && - !('hasMany' in field && field.hasMany === true) && - !('relationTo' in field && Array.isArray(field.relationTo)) - ) { - const unique = disableUnique !== true && field.unique - if (unique) { - const constraintValue = `${fieldPrefix || ''}${field.name}` - if (!adapter.fieldConstraints?.[rootTableName]) { - adapter.fieldConstraints[rootTableName] = {} - } - adapter.fieldConstraints[rootTableName][`${columnName}_idx`] = constraintValue + // If field is localized, + // add the column to the locale table instead of main table + if ( + adapter.payload.config.localization && + (field.localized || forceLocalized) && + field.type !== 'array' && + field.type !== 'blocks' && + (('hasMany' in field && field.hasMany !== true) || !('hasMany' in field)) + ) { + hasLocalizedField = true + targetTable = localesColumns + targetIndexes = localesIndexes + } + + if ( + (field.unique || field.index || ['relationship', 'upload'].includes(field.type)) && + !['array', 'blocks', 'group'].includes(field.type) && + !('hasMany' in field && field.hasMany === true) && + !('relationTo' in field && Array.isArray(field.relationTo)) + ) { + const unique = disableUnique !== true && field.unique + if (unique) { + const constraintValue = `${fieldPrefix || ''}${field.name}` + if (!adapter.fieldConstraints?.[rootTableName]) { + adapter.fieldConstraints[rootTableName] = {} } + adapter.fieldConstraints[rootTableName][`${columnName}_idx`] = constraintValue + } - const indexName = buildIndexName({ name: `${newTableName}_${columnName}`, adapter }) + const indexName = buildIndexName({ name: `${newTableName}_${columnName}`, adapter }) - targetIndexes[indexName] = createIndex({ - name: field.localized ? [fieldName, '_locale'] : fieldName, - indexName, - unique, - }) - } + targetIndexes[indexName] = createIndex({ + name: field.localized ? [fieldName, '_locale'] : fieldName, + indexName, + unique, + }) } switch (field.type) { @@ -235,7 +229,7 @@ export const traverseFields = ({ disableNotNull: disableNotNullFromHere, disableRelsTableUnique: true, disableUnique, - fields: disableUnique ? idToUUID(field.fields) : field.fields, + fields: disableUnique ? idToUUID(field.flattenedFields) : field.flattenedFields, rootRelationships: relationships, rootRelationsToBuild, rootTableIDColType, @@ -371,7 +365,7 @@ export const traverseFields = ({ disableNotNull: disableNotNullFromHere, disableRelsTableUnique: true, disableUnique, - fields: disableUnique ? idToUUID(block.fields) : block.fields, + fields: disableUnique ? idToUUID(block.flattenedFields) : block.flattenedFields, rootRelationships: relationships, rootRelationsToBuild, rootTableIDColType, @@ -467,69 +461,13 @@ export const traverseFields = ({ targetTable[fieldName] = withDefault(boolean(columnName), field) break } - case 'code': + case 'code': case 'email': - case 'textarea': { targetTable[fieldName] = withDefault(varchar(columnName), field) break } - case 'collapsible': - - case 'row': { - const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull - const { - hasLocalizedField: rowHasLocalizedField, - hasLocalizedManyNumberField: rowHasLocalizedManyNumberField, - hasLocalizedManyTextField: rowHasLocalizedManyTextField, - hasLocalizedRelationshipField: rowHasLocalizedRelationshipField, - hasManyNumberField: rowHasManyNumberField, - hasManyTextField: rowHasManyTextField, - } = traverseFields({ - adapter, - columnPrefix, - columns, - disableNotNull: disableNotNullFromHere, - disableUnique, - fieldPrefix, - fields: field.fields, - forceLocalized, - indexes, - localesColumns, - localesIndexes, - newTableName, - parentTableName, - relationships, - relationsToBuild, - rootRelationsToBuild, - rootTableIDColType, - rootTableName, - uniqueRelationships, - versions, - withinLocalizedArrayOrBlock, - }) - - if (rowHasLocalizedField) { - hasLocalizedField = true - } - if (rowHasLocalizedRelationshipField) { - hasLocalizedRelationshipField = true - } - if (rowHasManyTextField) { - hasManyTextField = true - } - if (rowHasLocalizedManyTextField) { - hasLocalizedManyTextField = true - } - if (rowHasManyNumberField) { - hasManyNumberField = true - } - if (rowHasLocalizedManyNumberField) { - hasLocalizedManyNumberField = true - } - break - } case 'date': { targetTable[fieldName] = withDefault( @@ -545,59 +483,6 @@ export const traverseFields = ({ case 'group': case 'tab': { - if (!('name' in field)) { - const { - hasLocalizedField: groupHasLocalizedField, - hasLocalizedManyNumberField: groupHasLocalizedManyNumberField, - hasLocalizedManyTextField: groupHasLocalizedManyTextField, - hasLocalizedRelationshipField: groupHasLocalizedRelationshipField, - hasManyNumberField: groupHasManyNumberField, - hasManyTextField: groupHasManyTextField, - } = traverseFields({ - adapter, - columnPrefix, - columns, - disableNotNull, - disableUnique, - fieldPrefix, - fields: field.fields, - forceLocalized, - indexes, - localesColumns, - localesIndexes, - newTableName, - parentTableName, - relationships, - relationsToBuild, - rootRelationsToBuild, - rootTableIDColType, - rootTableName, - uniqueRelationships, - versions, - withinLocalizedArrayOrBlock, - }) - - if (groupHasLocalizedField) { - hasLocalizedField = true - } - if (groupHasLocalizedRelationshipField) { - hasLocalizedRelationshipField = true - } - if (groupHasManyTextField) { - hasManyTextField = true - } - if (groupHasLocalizedManyTextField) { - hasLocalizedManyTextField = true - } - if (groupHasManyNumberField) { - hasManyNumberField = true - } - if (groupHasLocalizedManyNumberField) { - hasLocalizedManyNumberField = true - } - break - } - const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull const { @@ -614,7 +499,7 @@ export const traverseFields = ({ disableNotNull: disableNotNullFromHere, disableUnique, fieldPrefix: `${fieldName}.`, - fields: field.fields, + fields: field.flattenedFields, forceLocalized: field.localized, indexes, localesColumns, @@ -653,7 +538,6 @@ export const traverseFields = ({ } case 'json': - case 'richText': { targetTable[fieldName] = withDefault(jsonb(columnName), field) break @@ -694,8 +578,8 @@ export const traverseFields = ({ } break } - case 'radio': + case 'radio': case 'select': { const enumName = createTableName({ adapter, @@ -854,60 +738,6 @@ export const traverseFields = ({ break - case 'tabs': { - const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull - - const { - hasLocalizedField: tabHasLocalizedField, - hasLocalizedManyNumberField: tabHasLocalizedManyNumberField, - hasLocalizedManyTextField: tabHasLocalizedManyTextField, - hasLocalizedRelationshipField: tabHasLocalizedRelationshipField, - hasManyNumberField: tabHasManyNumberField, - hasManyTextField: tabHasManyTextField, - } = traverseFields({ - adapter, - columnPrefix, - columns, - disableNotNull: disableNotNullFromHere, - disableUnique, - fieldPrefix, - fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })), - forceLocalized, - indexes, - localesColumns, - localesIndexes, - newTableName, - parentTableName, - relationships, - relationsToBuild, - rootRelationsToBuild, - rootTableIDColType, - rootTableName, - uniqueRelationships, - versions, - withinLocalizedArrayOrBlock, - }) - - if (tabHasLocalizedField) { - hasLocalizedField = true - } - if (tabHasLocalizedRelationshipField) { - hasLocalizedRelationshipField = true - } - if (tabHasManyTextField) { - hasManyTextField = true - } - if (tabHasLocalizedManyTextField) { - hasLocalizedManyTextField = true - } - if (tabHasManyNumberField) { - hasManyNumberField = true - } - if (tabHasLocalizedManyNumberField) { - hasLocalizedManyNumberField = true - } - break - } case 'text': { if (field.hasMany) { const isLocalized = diff --git a/packages/drizzle/src/queries/buildAndOrConditions.ts b/packages/drizzle/src/queries/buildAndOrConditions.ts index 38ac89e29b7..387780f92e5 100644 --- a/packages/drizzle/src/queries/buildAndOrConditions.ts +++ b/packages/drizzle/src/queries/buildAndOrConditions.ts @@ -1,5 +1,5 @@ import type { SQL } from 'drizzle-orm' -import type { Field, Where } from 'payload' +import type { FlattenedField, Where } from 'payload' import type { DrizzleAdapter, GenericColumn } from '../types.js' import type { BuildQueryJoinAliases } from './buildQuery.js' @@ -17,7 +17,7 @@ export function buildAndOrConditions({ }: { adapter: DrizzleAdapter collectionSlug?: string - fields: Field[] + fields: FlattenedField[] globalSlug?: string joins: BuildQueryJoinAliases locale?: string diff --git a/packages/drizzle/src/queries/buildOrderBy.ts b/packages/drizzle/src/queries/buildOrderBy.ts index 30466735bfa..b6cd8c66ac4 100644 --- a/packages/drizzle/src/queries/buildOrderBy.ts +++ b/packages/drizzle/src/queries/buildOrderBy.ts @@ -1,4 +1,4 @@ -import type { Field, Sort } from 'payload' +import type { FlattenedField, Sort } from 'payload' import { asc, desc } from 'drizzle-orm' @@ -9,7 +9,7 @@ import { getTableColumnFromPath } from './getTableColumnFromPath.js' type Args = { adapter: DrizzleAdapter - fields: Field[] + fields: FlattenedField[] joins: BuildQueryJoinAliases locale?: string selectFields: Record diff --git a/packages/drizzle/src/queries/buildQuery.ts b/packages/drizzle/src/queries/buildQuery.ts index 554142796b4..e17d5b230ce 100644 --- a/packages/drizzle/src/queries/buildQuery.ts +++ b/packages/drizzle/src/queries/buildQuery.ts @@ -1,6 +1,6 @@ import type { asc, desc, SQL } from 'drizzle-orm' import type { PgTableWithColumns } from 'drizzle-orm/pg-core' -import type { Field, Sort, Where } from 'payload' +import type { FlattenedField, Sort, Where } from 'payload' import type { DrizzleAdapter, GenericColumn, GenericTable } from '../types.js' @@ -15,7 +15,7 @@ export type BuildQueryJoinAliases = { type BuildQueryArgs = { adapter: DrizzleAdapter - fields: Field[] + fields: FlattenedField[] joins?: BuildQueryJoinAliases locale?: string sort?: Sort diff --git a/packages/drizzle/src/queries/getTableColumnFromPath.ts b/packages/drizzle/src/queries/getTableColumnFromPath.ts index 67a804f3d75..df5f9839a6e 100644 --- a/packages/drizzle/src/queries/getTableColumnFromPath.ts +++ b/packages/drizzle/src/queries/getTableColumnFromPath.ts @@ -1,11 +1,11 @@ import type { SQL } from 'drizzle-orm' import type { SQLiteTableWithColumns } from 'drizzle-orm/sqlite-core' -import type { Field, FieldAffectingData, NumberField, TabAsField, TextField } from 'payload' +import type { FlattenedField, NumberField, TextField } from 'payload' import { and, eq, like, sql } from 'drizzle-orm' import { type PgTableWithColumns } from 'drizzle-orm/pg-core' -import { APIError, flattenTopLevelFields } from 'payload' -import { fieldAffectsData, tabHasName } from 'payload/shared' +import { APIError } from 'payload' +import { tabHasName } from 'payload/shared' import toSnakeCase from 'to-snake-case' import { validate as uuidValidate } from 'uuid' @@ -29,7 +29,7 @@ type TableColumn = { rawColumn: SQL }[] constraints: Constraint[] - field: FieldAffectingData + field: FlattenedField getNotNullColumnByValue?: (val: unknown) => string pathSegments?: string[] rawColumn?: SQL @@ -43,7 +43,7 @@ type Args = { columnPrefix?: string constraintPath?: string constraints?: Constraint[] - fields: (Field | TabAsField)[] + fields: FlattenedField[] joins: BuildQueryJoinAliases locale?: string pathSegments: string[] @@ -86,9 +86,7 @@ export const getTableColumnFromPath = ({ const rootTableName = incomingRootTableName || tableName let constraintPath = incomingConstraintPath || '' - const field = flattenTopLevelFields(fields as Field[]).find( - (fieldToFind) => fieldAffectsData(fieldToFind) && fieldToFind.name === fieldPath, - ) as Field | TabAsField + const field = fields.find((fieldToFind) => fieldToFind.name === fieldPath) let newTableName = tableName if (!field && fieldPath === 'id') { @@ -152,7 +150,7 @@ export const getTableColumnFromPath = ({ collectionPath, constraintPath, constraints, - fields: field.fields, + fields: field.flattenedFields, joins, locale, pathSegments: pathSegments.slice(1), @@ -209,7 +207,7 @@ export const getTableColumnFromPath = ({ collectionPath, constraintPath, constraints: blockConstraints, - fields: block.fields, + fields: block.flattenedFields, joins, locale, pathSegments: pathSegments.slice(1), @@ -290,7 +288,7 @@ export const getTableColumnFromPath = ({ columnPrefix: `${columnPrefix}${field.name}_`, constraintPath: `${constraintPath}${field.name}.`, constraints, - fields: field.fields, + fields: field.flattenedFields, joins, locale, pathSegments: pathSegments.slice(1), @@ -303,7 +301,6 @@ export const getTableColumnFromPath = ({ } case 'number': - case 'text': { if (field.hasMany) { let tableType = 'texts' @@ -346,8 +343,8 @@ export const getTableColumnFromPath = ({ } break } - case 'relationship': + case 'relationship': case 'upload': { const newCollectionPath = pathSegments.slice(1).join('.') if (Array.isArray(field.relationTo) || field.hasMany) { @@ -580,7 +577,7 @@ export const getTableColumnFromPath = ({ collectionPath: newCollectionPath, constraintPath: '', constraints, - fields: adapter.payload.collections[field.relationTo].config.fields, + fields: adapter.payload.collections[field.relationTo].config.flattenedFields, joins, locale, pathSegments: pathSegments.slice(1), @@ -640,7 +637,7 @@ export const getTableColumnFromPath = ({ columnPrefix: `${columnPrefix}${field.name}_`, constraintPath: `${constraintPath}${field.name}.`, constraints, - fields: field.fields, + fields: field.flattenedFields, joins, locale, pathSegments: pathSegments.slice(1), @@ -658,29 +655,7 @@ export const getTableColumnFromPath = ({ columnPrefix, constraintPath, constraints, - fields: field.fields, - joins, - locale, - pathSegments: pathSegments.slice(1), - rootTableName, - selectFields, - tableName: newTableName, - tableNameSuffix, - value, - }) - } - case 'tabs': { - return getTableColumnFromPath({ - adapter, - aliasTable, - collectionPath, - columnPrefix, - constraintPath, - constraints, - fields: field.tabs.map((tab) => ({ - ...tab, - type: 'tab', - })), + fields: field.flattenedFields, joins, locale, pathSegments: pathSegments.slice(1), @@ -698,44 +673,42 @@ export const getTableColumnFromPath = ({ } } - if (fieldAffectsData(field)) { - let newTable = adapter.tables[newTableName] + let newTable = adapter.tables[newTableName] - if (field.localized && adapter.payload.config.localization) { - // If localized, we go to localized table and set aliasTable to undefined - // so it is not picked up below to be used as targetTable - const parentTable = aliasTable || adapter.tables[tableName] - newTableName = `${tableName}${adapter.localesSuffix}` + if (field.localized && adapter.payload.config.localization) { + // If localized, we go to localized table and set aliasTable to undefined + // so it is not picked up below to be used as targetTable + const parentTable = aliasTable || adapter.tables[tableName] + newTableName = `${tableName}${adapter.localesSuffix}` - newTable = adapter.tables[newTableName] + newTable = adapter.tables[newTableName] - let condition = eq(parentTable.id, newTable._parentID) + let condition = eq(parentTable.id, newTable._parentID) - if (locale !== 'all') { - condition = and(condition, eq(newTable._locale, locale)) - } + if (locale !== 'all') { + condition = and(condition, eq(newTable._locale, locale)) + } - addJoinTable({ - condition, - joins, - table: newTable, - }) + addJoinTable({ + condition, + joins, + table: newTable, + }) - aliasTable = undefined - } + aliasTable = undefined + } - const targetTable = aliasTable || newTable + const targetTable = aliasTable || newTable - selectFields[`${newTableName}.${columnPrefix}${field.name}`] = - targetTable[`${columnPrefix}${field.name}`] + selectFields[`${newTableName}.${columnPrefix}${field.name}`] = + targetTable[`${columnPrefix}${field.name}`] - return { - columnName: `${columnPrefix}${field.name}`, - constraints, - field, - pathSegments, - table: targetTable, - } + return { + columnName: `${columnPrefix}${field.name}`, + constraints, + field, + pathSegments, + table: targetTable, } } diff --git a/packages/drizzle/src/queries/parseParams.ts b/packages/drizzle/src/queries/parseParams.ts index f01bac9a52b..947b7b5306e 100644 --- a/packages/drizzle/src/queries/parseParams.ts +++ b/packages/drizzle/src/queries/parseParams.ts @@ -1,5 +1,5 @@ import type { SQL } from 'drizzle-orm' -import type { Field, Operator, Where } from 'payload' +import type { FlattenedField, Operator, Where } from 'payload' import { and, isNotNull, isNull, ne, notInArray, or, sql } from 'drizzle-orm' import { PgUUID } from 'drizzle-orm/pg-core' @@ -15,7 +15,7 @@ import { sanitizeQueryValue } from './sanitizeQueryValue.js' type Args = { adapter: DrizzleAdapter - fields: Field[] + fields: FlattenedField[] joins: BuildQueryJoinAliases locale: string selectFields: Record diff --git a/packages/drizzle/src/queryDrafts.ts b/packages/drizzle/src/queryDrafts.ts index 654f9eaaf38..199be9953a5 100644 --- a/packages/drizzle/src/queryDrafts.ts +++ b/packages/drizzle/src/queryDrafts.ts @@ -26,7 +26,7 @@ export const queryDrafts: QueryDrafts = async function queryDrafts( const tableName = this.tableNameMap.get( `_${toSnakeCase(collectionConfig.slug)}${this.versionsSuffix}`, ) - const fields = buildVersionCollectionFields(this.payload.config, collectionConfig) + const fields = buildVersionCollectionFields(this.payload.config, collectionConfig, true) const combinedWhere = combineQueries({ latest: { equals: true } }, where) diff --git a/packages/drizzle/src/transform/read/index.ts b/packages/drizzle/src/transform/read/index.ts index b418c3fb7aa..9c00af07745 100644 --- a/packages/drizzle/src/transform/read/index.ts +++ b/packages/drizzle/src/transform/read/index.ts @@ -1,4 +1,4 @@ -import type { Field, JoinQuery, SanitizedConfig, TypeWithID } from 'payload' +import type { FlattenedField, JoinQuery, SanitizedConfig, TypeWithID } from 'payload' import type { DrizzleAdapter } from '../../types.js' @@ -11,7 +11,7 @@ type TransformArgs = { config: SanitizedConfig data: Record fallbackLocale?: false | string - fields: Field[] + fields: FlattenedField[] joinQuery?: JoinQuery locale?: string } diff --git a/packages/drizzle/src/transform/read/traverseFields.ts b/packages/drizzle/src/transform/read/traverseFields.ts index 5950d37a439..b0270538d14 100644 --- a/packages/drizzle/src/transform/read/traverseFields.ts +++ b/packages/drizzle/src/transform/read/traverseFields.ts @@ -1,4 +1,4 @@ -import type { Field, JoinQuery, SanitizedConfig, TabAsField } from 'payload' +import type { FlattenedField, JoinQuery, SanitizedConfig } from 'payload' import { fieldAffectsData, fieldIsVirtual } from 'payload/shared' @@ -37,7 +37,7 @@ type TraverseFieldsArgs = { /** * An array of Payload fields to traverse */ - fields: (Field | TabAsField)[] + fields: FlattenedField[] /** * */ @@ -89,117 +89,185 @@ export const traverseFields = >({ const sanitizedPath = path ? `${path}.` : path const formatted = fields.reduce((result, field) => { - if (field.type === 'tabs') { - traverseFields({ - adapter, - blocks, - config, - dataRef, - deletions, - fieldPrefix, - fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })), - joinQuery, - numbers, - path, - relationships, - table, - texts, - withinArrayOrBlockLocale, - }) + if (fieldIsVirtual(field)) { + return result } - if ( - field.type === 'collapsible' || - field.type === 'row' || - (field.type === 'tab' && !('name' in field)) - ) { - traverseFields({ - adapter, - blocks, - config, - dataRef, - deletions, - fieldPrefix, - fields: field.fields, - joinQuery, - numbers, - path, - relationships, - table, - texts, - withinArrayOrBlockLocale, - }) + const fieldName = `${fieldPrefix || ''}${field.name}` + let fieldData = table[fieldName] + const localizedFieldData = {} + const valuesToTransform: { + ref: Record + table: Record + }[] = [] + + if (fieldPrefix) { + deletions.push(() => delete table[fieldName]) } - if (fieldAffectsData(field)) { - if (fieldIsVirtual(field)) { - return result - } + if (field.type === 'array') { + if (Array.isArray(fieldData)) { + if (field.localized) { + result[field.name] = fieldData.reduce((arrayResult, row) => { + if (typeof row._locale === 'string') { + if (!arrayResult[row._locale]) { + arrayResult[row._locale] = [] + } + const locale = row._locale + const data = {} + delete row._locale + if (row._uuid) { + row.id = row._uuid + delete row._uuid + } - const fieldName = `${fieldPrefix || ''}${field.name}` - let fieldData = table[fieldName] - const localizedFieldData = {} - const valuesToTransform: { - ref: Record - table: Record - }[] = [] + const rowResult = traverseFields({ + adapter, + blocks, + config, + dataRef: data, + deletions, + fieldPrefix: '', + fields: field.flattenedFields, + numbers, + path: `${sanitizedPath}${field.name}.${row._order - 1}`, + relationships, + table: row, + texts, + withinArrayOrBlockLocale: locale, + }) - if (fieldPrefix) { - deletions.push(() => delete table[fieldName]) - } + if ('_order' in rowResult) { + delete rowResult._order + } - if (field.type === 'array') { - if (Array.isArray(fieldData)) { - if (field.localized) { - result[field.name] = fieldData.reduce((arrayResult, row) => { - if (typeof row._locale === 'string') { - if (!arrayResult[row._locale]) { - arrayResult[row._locale] = [] - } - const locale = row._locale - const data = {} + arrayResult[locale].push(rowResult) + } + + return arrayResult + }, {}) + } else { + result[field.name] = fieldData.reduce((acc, row, i) => { + if (row._uuid) { + row.id = row._uuid + delete row._uuid + } + + if ('_order' in row) { + delete row._order + } + + if ( + !withinArrayOrBlockLocale || + (withinArrayOrBlockLocale && withinArrayOrBlockLocale === row._locale) + ) { + if (row._locale) { delete row._locale - if (row._uuid) { - row.id = row._uuid - delete row._uuid - } + } - const rowResult = traverseFields({ + acc.push( + traverseFields({ adapter, blocks, config, - dataRef: data, + dataRef: row, deletions, fieldPrefix: '', - fields: field.fields, + fields: field.flattenedFields, numbers, - path: `${sanitizedPath}${field.name}.${row._order - 1}`, + path: `${sanitizedPath}${field.name}.${i}`, relationships, table: row, texts, - withinArrayOrBlockLocale: locale, - }) + withinArrayOrBlockLocale, + }), + ) + } - if ('_order' in rowResult) { - delete rowResult._order - } + return acc + }, []) + } + } + + return result + } + + if (field.type === 'blocks') { + const blockFieldPath = `${sanitizedPath}${field.name}` + const blocksByPath = blocks[blockFieldPath] + + if (Array.isArray(blocksByPath)) { + if (field.localized) { + result[field.name] = {} - arrayResult[locale].push(rowResult) + blocksByPath.forEach((row) => { + if (row._uuid) { + row.id = row._uuid + delete row._uuid + } + if (typeof row._locale === 'string') { + if (!result[field.name][row._locale]) { + result[field.name][row._locale] = [] } + result[field.name][row._locale].push(row) + delete row._locale + } + }) - return arrayResult - }, {}) - } else { - result[field.name] = fieldData.reduce((acc, row, i) => { - if (row._uuid) { - row.id = row._uuid - delete row._uuid + Object.entries(result[field.name]).forEach(([locale, localizedBlocks]) => { + result[field.name][locale] = localizedBlocks.map((row) => { + const block = field.blocks.find(({ slug }) => slug === row.blockType) + + if (block) { + const blockResult = traverseFields({ + adapter, + blocks, + config, + dataRef: row, + deletions, + fieldPrefix: '', + fields: block.flattenedFields, + numbers, + path: `${blockFieldPath}.${row._order - 1}`, + relationships, + table: row, + texts, + withinArrayOrBlockLocale: locale, + }) + + delete blockResult._order + return blockResult } - if ('_order' in row) { - delete row._order + return {} + }) + }) + } else { + // Add locale-specific index to have a proper blockFieldPath for current locale + // because blocks can be in the same array for different locales! + if (withinArrayOrBlockLocale && config.localization) { + for (const locale of config.localization.localeCodes) { + let localeIndex = 0 + + for (let i = 0; i < blocksByPath.length; i++) { + const row = blocksByPath[i] + if (row._locale === locale) { + row._index = localeIndex + localeIndex++ + } } + } + } + result[field.name] = blocksByPath.reduce((acc, row, i) => { + delete row._order + if (row._uuid) { + row.id = row._uuid + delete row._uuid + } + const block = field.blocks.find(({ slug }) => slug === row.blockType) + + if (block) { if ( !withinArrayOrBlockLocale || (withinArrayOrBlockLocale && withinArrayOrBlockLocale === row._locale) @@ -207,6 +275,10 @@ export const traverseFields = >({ if (row._locale) { delete row._locale } + if (typeof row._index === 'number') { + i = row._index + delete row._index + } acc.push( traverseFields({ @@ -216,472 +288,355 @@ export const traverseFields = >({ dataRef: row, deletions, fieldPrefix: '', - fields: field.fields, + fields: block.flattenedFields, numbers, - path: `${sanitizedPath}${field.name}.${i}`, + path: `${blockFieldPath}.${i}`, relationships, table: row, texts, withinArrayOrBlockLocale, }), ) - } - - return acc - }, []) - } - } - - return result - } - - if (field.type === 'blocks') { - const blockFieldPath = `${sanitizedPath}${field.name}` - const blocksByPath = blocks[blockFieldPath] - - if (Array.isArray(blocksByPath)) { - if (field.localized) { - result[field.name] = {} - - blocksByPath.forEach((row) => { - if (row._uuid) { - row.id = row._uuid - delete row._uuid - } - if (typeof row._locale === 'string') { - if (!result[field.name][row._locale]) { - result[field.name][row._locale] = [] - } - result[field.name][row._locale].push(row) - delete row._locale - } - }) - - Object.entries(result[field.name]).forEach(([locale, localizedBlocks]) => { - result[field.name][locale] = localizedBlocks.map((row) => { - const block = field.blocks.find(({ slug }) => slug === row.blockType) - - if (block) { - const blockResult = traverseFields({ - adapter, - blocks, - config, - dataRef: row, - deletions, - fieldPrefix: '', - fields: block.fields, - numbers, - path: `${blockFieldPath}.${row._order - 1}`, - relationships, - table: row, - texts, - withinArrayOrBlockLocale: locale, - }) - - delete blockResult._order - return blockResult - } - return {} - }) - }) - } else { - // Add locale-specific index to have a proper blockFieldPath for current locale - // because blocks can be in the same array for different locales! - if (withinArrayOrBlockLocale && config.localization) { - for (const locale of config.localization.localeCodes) { - let localeIndex = 0 - - for (let i = 0; i < blocksByPath.length; i++) { - const row = blocksByPath[i] - if (row._locale === locale) { - row._index = localeIndex - localeIndex++ - } - } + return acc } + } else { + acc.push({}) } - result[field.name] = blocksByPath.reduce((acc, row, i) => { - delete row._order - if (row._uuid) { - row.id = row._uuid - delete row._uuid - } - const block = field.blocks.find(({ slug }) => slug === row.blockType) - - if (block) { - if ( - !withinArrayOrBlockLocale || - (withinArrayOrBlockLocale && withinArrayOrBlockLocale === row._locale) - ) { - if (row._locale) { - delete row._locale - } - if (typeof row._index === 'number') { - i = row._index - delete row._index - } - - acc.push( - traverseFields({ - adapter, - blocks, - config, - dataRef: row, - deletions, - fieldPrefix: '', - fields: block.fields, - numbers, - path: `${blockFieldPath}.${i}`, - relationships, - table: row, - texts, - withinArrayOrBlockLocale, - }), - ) - - return acc - } - } else { - acc.push({}) - } - - return acc - }, []) - } + return acc + }, []) } - - return result } - if (field.type === 'relationship' || field.type === 'upload') { - if (typeof field.relationTo === 'string' && !('hasMany' in field && field.hasMany)) { - if ( - field.localized && - config.localization && - config.localization.locales && - Array.isArray(table?._locales) - ) { - table._locales.forEach((localeRow) => { - result[field.name] = { [localeRow._locale]: localeRow[fieldName] } - }) - } else { - valuesToTransform.push({ ref: result, table }) - } - } else { - const relationPathMatch = relationships[`${sanitizedPath}${field.name}`] - - if (!relationPathMatch) { - if ('hasMany' in field && field.hasMany) { - if (field.localized && config.localization && config.localization.locales) { - result[field.name] = { - [config.localization.defaultLocale]: [], - } - } else { - result[field.name] = [] - } - } - - return result - } - - if (field.localized) { - result[field.name] = {} - const relationsByLocale: Record[]> = {} - - relationPathMatch.forEach((row) => { - if (typeof row.locale === 'string') { - if (!relationsByLocale[row.locale]) { - relationsByLocale[row.locale] = [] - } - relationsByLocale[row.locale].push(row) - } - }) - - Object.entries(relationsByLocale).forEach(([locale, relations]) => { - transformRelationship({ - field, - locale, - ref: result, - relations, - }) - }) - } else { - transformRelationship({ - field, - ref: result, - relations: relationPathMatch, - withinArrayOrBlockLocale, - }) - } - return result - } - } - - if (field.type === 'join') { - const { limit = field.defaultLimit ?? 10 } = - joinQuery?.[`${fieldPrefix.replaceAll('_', '.')}${field.name}`] || {} + return result + } - // raw hasMany results from SQLite - if (typeof fieldData === 'string') { - fieldData = JSON.parse(fieldData) + if (field.type === 'relationship' || field.type === 'upload') { + if (typeof field.relationTo === 'string' && !('hasMany' in field && field.hasMany)) { + if ( + field.localized && + config.localization && + config.localization.locales && + Array.isArray(table?._locales) + ) { + table._locales.forEach((localeRow) => { + result[field.name] = { [localeRow._locale]: localeRow[fieldName] } + }) + } else { + valuesToTransform.push({ ref: result, table }) } + } else { + const relationPathMatch = relationships[`${sanitizedPath}${field.name}`] - let fieldResult: - | { docs: unknown[]; hasNextPage: boolean } - | Record - if (Array.isArray(fieldData)) { - if (field.localized) { - fieldResult = fieldData.reduce((joinResult, row) => { - if (typeof row._locale === 'string') { - if (!joinResult[row._locale]) { - joinResult[row._locale] = { - docs: [], - hasNextPage: false, - } - } - joinResult[row._locale].docs.push(row._parentID) + if (!relationPathMatch) { + if ('hasMany' in field && field.hasMany) { + if (field.localized && config.localization && config.localization.locales) { + result[field.name] = { + [config.localization.defaultLocale]: [], } - - return joinResult - }, {}) - Object.keys(fieldResult).forEach((locale) => { - fieldResult[locale].hasNextPage = fieldResult[locale].docs.length > limit - fieldResult[locale].docs = fieldResult[locale].docs.slice(0, limit) - }) - } else { - const hasNextPage = limit !== 0 && fieldData.length > limit - fieldResult = { - docs: (hasNextPage ? fieldData.slice(0, limit) : fieldData).map(({ id }) => ({ - id, - })), - hasNextPage, + } else { + result[field.name] = [] } } - } - - result[field.name] = fieldResult - return result - } - if (field.type === 'text' && field?.hasMany) { - const textPathMatch = texts[`${sanitizedPath}${field.name}`] - if (!textPathMatch) { return result } if (field.localized) { result[field.name] = {} - const textsByLocale: Record[]> = {} + const relationsByLocale: Record[]> = {} - textPathMatch.forEach((row) => { + relationPathMatch.forEach((row) => { if (typeof row.locale === 'string') { - if (!textsByLocale[row.locale]) { - textsByLocale[row.locale] = [] + if (!relationsByLocale[row.locale]) { + relationsByLocale[row.locale] = [] } - textsByLocale[row.locale].push(row) + relationsByLocale[row.locale].push(row) } }) - Object.entries(textsByLocale).forEach(([locale, texts]) => { - transformHasManyText({ + Object.entries(relationsByLocale).forEach(([locale, relations]) => { + transformRelationship({ field, locale, ref: result, - textRows: texts, + relations, }) }) } else { - transformHasManyText({ + transformRelationship({ field, ref: result, - textRows: textPathMatch, + relations: relationPathMatch, withinArrayOrBlockLocale, }) } - return result } + } - if (field.type === 'number' && field.hasMany) { - const numberPathMatch = numbers[`${sanitizedPath}${field.name}`] - if (!numberPathMatch) { - return result - } + if (field.type === 'join') { + const { limit = field.defaultLimit ?? 10 } = + joinQuery?.[`${fieldPrefix.replaceAll('_', '.')}${field.name}`] || {} - if (field.localized) { - result[field.name] = {} - const numbersByLocale: Record[]> = {} + // raw hasMany results from SQLite + if (typeof fieldData === 'string') { + fieldData = JSON.parse(fieldData) + } - numberPathMatch.forEach((row) => { - if (typeof row.locale === 'string') { - if (!numbersByLocale[row.locale]) { - numbersByLocale[row.locale] = [] + let fieldResult: + | { docs: unknown[]; hasNextPage: boolean } + | Record + if (Array.isArray(fieldData)) { + if (field.localized) { + fieldResult = fieldData.reduce((joinResult, row) => { + if (typeof row._locale === 'string') { + if (!joinResult[row._locale]) { + joinResult[row._locale] = { + docs: [], + hasNextPage: false, + } } - numbersByLocale[row.locale].push(row) + joinResult[row._locale].docs.push(row._parentID) } - }) - Object.entries(numbersByLocale).forEach(([locale, numbers]) => { - transformHasManyNumber({ - field, - locale, - numberRows: numbers, - ref: result, - }) + return joinResult + }, {}) + Object.keys(fieldResult).forEach((locale) => { + fieldResult[locale].hasNextPage = fieldResult[locale].docs.length > limit + fieldResult[locale].docs = fieldResult[locale].docs.slice(0, limit) }) } else { - transformHasManyNumber({ - field, - numberRows: numberPathMatch, - ref: result, - withinArrayOrBlockLocale, - }) + const hasNextPage = limit !== 0 && fieldData.length > limit + fieldResult = { + docs: (hasNextPage ? fieldData.slice(0, limit) : fieldData).map(({ id }) => ({ + id, + })), + hasNextPage, + } } + } + result[field.name] = fieldResult + return result + } + + if (field.type === 'text' && field?.hasMany) { + const textPathMatch = texts[`${sanitizedPath}${field.name}`] + if (!textPathMatch) { return result } - if (field.type === 'select' && field.hasMany) { - if (Array.isArray(fieldData)) { - if (field.localized) { - result[field.name] = fieldData.reduce((selectResult, row) => { - if (typeof row.locale === 'string') { - if (!selectResult[row.locale]) { - selectResult[row.locale] = [] - } - selectResult[row.locale].push(row.value) - } + if (field.localized) { + result[field.name] = {} + const textsByLocale: Record[]> = {} - return selectResult - }, {}) - } else { - let selectData = fieldData - if (withinArrayOrBlockLocale) { - selectData = selectData.filter(({ locale }) => locale === withinArrayOrBlockLocale) + textPathMatch.forEach((row) => { + if (typeof row.locale === 'string') { + if (!textsByLocale[row.locale]) { + textsByLocale[row.locale] = [] } - result[field.name] = selectData.map(({ value }) => value) + textsByLocale[row.locale].push(row) } - } + }) + + Object.entries(textsByLocale).forEach(([locale, texts]) => { + transformHasManyText({ + field, + locale, + ref: result, + textRows: texts, + }) + }) + } else { + transformHasManyText({ + field, + ref: result, + textRows: textPathMatch, + withinArrayOrBlockLocale, + }) + } + + return result + } + + if (field.type === 'number' && field.hasMany) { + const numberPathMatch = numbers[`${sanitizedPath}${field.name}`] + if (!numberPathMatch) { return result } - if (field.localized && Array.isArray(table._locales)) { - if (!table._locales.length && adapter.payload.config.localization) { - adapter.payload.config.localization.localeCodes.forEach((_locale) => - (table._locales as unknown[]).push({ _locale }), - ) - } + if (field.localized) { + result[field.name] = {} + const numbersByLocale: Record[]> = {} + + numberPathMatch.forEach((row) => { + if (typeof row.locale === 'string') { + if (!numbersByLocale[row.locale]) { + numbersByLocale[row.locale] = [] + } + numbersByLocale[row.locale].push(row) + } + }) - table._locales.forEach((localeRow) => { - valuesToTransform.push({ - ref: localizedFieldData, - table: { - ...table, - ...localeRow, - }, + Object.entries(numbersByLocale).forEach(([locale, numbers]) => { + transformHasManyNumber({ + field, + locale, + numberRows: numbers, + ref: result, }) }) } else { - valuesToTransform.push({ ref: result, table }) + transformHasManyNumber({ + field, + numberRows: numberPathMatch, + ref: result, + withinArrayOrBlockLocale, + }) } - valuesToTransform.forEach(({ ref, table }) => { - const fieldData = table[`${fieldPrefix || ''}${field.name}`] - const locale = table?._locale - let val = fieldData + return result + } - switch (field.type) { - case 'date': { - if (typeof fieldData === 'string') { - val = new Date(fieldData).toISOString() + if (field.type === 'select' && field.hasMany) { + if (Array.isArray(fieldData)) { + if (field.localized) { + result[field.name] = fieldData.reduce((selectResult, row) => { + if (typeof row.locale === 'string') { + if (!selectResult[row.locale]) { + selectResult[row.locale] = [] + } + selectResult[row.locale].push(row.value) } - break + return selectResult + }, {}) + } else { + let selectData = fieldData + if (withinArrayOrBlockLocale) { + selectData = selectData.filter(({ locale }) => locale === withinArrayOrBlockLocale) } - case 'group': + result[field.name] = selectData.map(({ value }) => value) + } + } + return result + } - case 'tab': { - const groupFieldPrefix = `${fieldPrefix || ''}${field.name}_` - const groupData = {} - const locale = table._locale as string - const refKey = field.localized && locale ? locale : field.name + if (field.localized && Array.isArray(table._locales)) { + if (!table._locales.length && adapter.payload.config.localization) { + adapter.payload.config.localization.localeCodes.forEach((_locale) => + (table._locales as unknown[]).push({ _locale }), + ) + } - if (field.localized && locale) { - delete table._locale - } - ref[refKey] = traverseFields>({ - adapter, - blocks, - config, - dataRef: groupData as Record, - deletions, - fieldPrefix: groupFieldPrefix, - fields: field.fields, - numbers, - path: `${sanitizedPath}${field.name}`, - relationships, - table, - texts, - withinArrayOrBlockLocale: locale || withinArrayOrBlockLocale, - }) + table._locales.forEach((localeRow) => { + valuesToTransform.push({ + ref: localizedFieldData, + table: { + ...table, + ...localeRow, + }, + }) + }) + } else { + valuesToTransform.push({ ref: result, table }) + } - if ('_order' in ref) { - delete ref._order - } + valuesToTransform.forEach(({ ref, table }) => { + const fieldData = table[`${fieldPrefix || ''}${field.name}`] + const locale = table?._locale + let val = fieldData - return + switch (field.type) { + case 'date': { + if (typeof fieldData === 'string') { + val = new Date(fieldData).toISOString() } - case 'number': { - if (typeof fieldData === 'string') { - val = Number.parseFloat(fieldData) - } + break + } - break + case 'group': + case 'tab': { + const groupFieldPrefix = `${fieldPrefix || ''}${field.name}_` + const groupData = {} + const locale = table._locale as string + const refKey = field.localized && locale ? locale : field.name + + if (field.localized && locale) { + delete table._locale } + ref[refKey] = traverseFields>({ + adapter, + blocks, + config, + dataRef: groupData as Record, + deletions, + fieldPrefix: groupFieldPrefix, + fields: field.flattenedFields, + numbers, + path: `${sanitizedPath}${field.name}`, + relationships, + table, + texts, + withinArrayOrBlockLocale: locale || withinArrayOrBlockLocale, + }) - case 'relationship': + if ('_order' in ref) { + delete ref._order + } - case 'upload': { - if ( - val && - typeof field.relationTo === 'string' && - adapter.payload.collections[field.relationTo].customIDType === 'number' - ) { - val = Number(val) - } + return + } - break + case 'number': { + if (typeof fieldData === 'string') { + val = Number.parseFloat(fieldData) } - case 'text': { - if (typeof fieldData === 'string') { - val = String(fieldData) - } - break - } + break + } - default: { - break + case 'relationship': + case 'upload': { + if ( + val && + typeof field.relationTo === 'string' && + adapter.payload.collections[field.relationTo].customIDType === 'number' + ) { + val = Number(val) } + + break } - if (typeof locale === 'string') { - ref[locale] = val - } else { - result[field.name] = val + case 'text': { + if (typeof fieldData === 'string') { + val = String(fieldData) + } + + break } - }) - if (Object.keys(localizedFieldData).length > 0) { - result[field.name] = localizedFieldData + default: { + break + } + } + if (typeof locale === 'string') { + ref[locale] = val + } else { + result[field.name] = val } + }) - return result + if (Object.keys(localizedFieldData).length > 0) { + result[field.name] = localizedFieldData } return result + + return result }, dataRef) if (Array.isArray(table._locales)) { diff --git a/packages/drizzle/src/transform/write/array.ts b/packages/drizzle/src/transform/write/array.ts index fb4f5fd682f..e99e5b3c7bb 100644 --- a/packages/drizzle/src/transform/write/array.ts +++ b/packages/drizzle/src/transform/write/array.ts @@ -1,4 +1,4 @@ -import type { ArrayField } from 'payload' +import type { FlattenedArrayField } from 'payload' import type { DrizzleAdapter } from '../../types.js' import type { ArrayRowToInsert, BlockRowToInsert, RelationshipToDelete } from './types.js' @@ -15,7 +15,7 @@ type Args = { } blocksToDelete: Set data: unknown - field: ArrayField + field: FlattenedArrayField locale?: string numbers: Record[] path: string @@ -96,7 +96,7 @@ export const transformArray = ({ columnPrefix: '', data: arrayRow, fieldPrefix: '', - fields: field.fields, + fields: field.flattenedFields, locales: newRow.locales, numbers, parentTableName: arrayTableName, diff --git a/packages/drizzle/src/transform/write/blocks.ts b/packages/drizzle/src/transform/write/blocks.ts index 0e97cd324c3..74f41625e76 100644 --- a/packages/drizzle/src/transform/write/blocks.ts +++ b/packages/drizzle/src/transform/write/blocks.ts @@ -1,4 +1,4 @@ -import type { BlocksField } from 'payload' +import type { FlattenedBlocksField } from 'payload' import toSnakeCase from 'to-snake-case' @@ -15,7 +15,7 @@ type Args = { } blocksToDelete: Set data: Record[] - field: BlocksField + field: FlattenedBlocksField locale?: string numbers: Record[] path: string @@ -100,7 +100,7 @@ export const transformBlocks = ({ columnPrefix: '', data: blockRow, fieldPrefix: '', - fields: matchedBlock.fields, + fields: matchedBlock.flattenedFields, locales: newRow.locales, numbers, parentTableName: blockTableName, diff --git a/packages/drizzle/src/transform/write/index.ts b/packages/drizzle/src/transform/write/index.ts index 80725b4c916..faa03cf3b08 100644 --- a/packages/drizzle/src/transform/write/index.ts +++ b/packages/drizzle/src/transform/write/index.ts @@ -1,4 +1,4 @@ -import type { Field } from 'payload' +import type { FlattenedField } from 'payload' import type { DrizzleAdapter } from '../../types.js' import type { RowToInsert } from './types.js' @@ -8,7 +8,7 @@ import { traverseFields } from './traverseFields.js' type Args = { adapter: DrizzleAdapter data: Record - fields: Field[] + fields: FlattenedField[] path?: string tableName: string } diff --git a/packages/drizzle/src/transform/write/traverseFields.ts b/packages/drizzle/src/transform/write/traverseFields.ts index 7b4f9529fc9..d65630cb743 100644 --- a/packages/drizzle/src/transform/write/traverseFields.ts +++ b/packages/drizzle/src/transform/write/traverseFields.ts @@ -1,7 +1,7 @@ -import type { Field } from 'payload' +import type { FlattenedField } from 'payload' import { sql } from 'drizzle-orm' -import { fieldAffectsData, fieldIsVirtual } from 'payload/shared' +import { fieldIsVirtual } from 'payload/shared' import toSnakeCase from 'to-snake-case' import type { DrizzleAdapter } from '../../types.js' @@ -40,7 +40,7 @@ type Args = { * Ex: myGroup_myNamedTab_ */ fieldPrefix: string - fields: Field[] + fields: FlattenedField[] forcedLocale?: string locales: { [locale: string]: Record @@ -93,16 +93,14 @@ export const traverseFields = ({ let fieldName = '' let fieldData: unknown - if (fieldAffectsData(field)) { - if (fieldIsVirtual(field)) { - return - } - - columnName = `${columnPrefix || ''}${toSnakeCase(field.name)}` - fieldName = `${fieldPrefix || ''}${field.name}` - fieldData = data[field.name] + if (fieldIsVirtual(field)) { + return } + columnName = `${columnPrefix || ''}${toSnakeCase(field.name)}` + fieldName = `${fieldPrefix || ''}${field.name}` + fieldData = data[field.name] + if (field.type === 'array') { const arrayTableName = adapter.tableNameMap.get(`${parentTableName}_${columnName}`) @@ -209,7 +207,7 @@ export const traverseFields = ({ return } - if (field.type === 'group') { + if (field.type === 'group' || field.type === 'tab') { if (typeof data[field.name] === 'object' && data[field.name] !== null) { if (field.localized) { Object.entries(data[field.name]).forEach(([localeKey, localeData]) => { @@ -226,7 +224,7 @@ export const traverseFields = ({ data: localeData as Record, existingLocales, fieldPrefix: `${fieldName}_`, - fields: field.fields, + fields: field.flattenedFields, forcedLocale: localeKey, locales, numbers, @@ -255,7 +253,7 @@ export const traverseFields = ({ data: groupData, existingLocales, fieldPrefix: `${fieldName}_`, - fields: field.fields, + fields: field.flattenedFields, locales, numbers, parentTableName, @@ -273,125 +271,6 @@ export const traverseFields = ({ return } - if (field.type === 'tabs') { - field.tabs.forEach((tab) => { - if ('name' in tab) { - if (fieldIsVirtual(tab)) { - return - } - - if (typeof data[tab.name] === 'object' && data[tab.name] !== null) { - if (tab.localized) { - Object.entries(data[tab.name]).forEach(([localeKey, localeData]) => { - // preserve array ID if there is - localeData._uuid = data.id || data._uuid - - traverseFields({ - adapter, - arrays, - baseTableName, - blocks, - blocksToDelete, - columnPrefix: `${columnPrefix || ''}${toSnakeCase(tab.name)}_`, - data: localeData as Record, - existingLocales, - fieldPrefix: `${fieldPrefix || ''}${tab.name}_`, - fields: tab.fields, - forcedLocale: localeKey, - locales, - numbers, - parentTableName, - path: `${path || ''}${tab.name}.`, - relationships, - relationshipsToDelete, - row, - selects, - texts, - withinArrayOrBlockLocale: localeKey, - }) - }) - } else { - const tabData = data[tab.name] as Record - // preserve array ID if there is - tabData._uuid = data.id || data._uuid - - traverseFields({ - adapter, - arrays, - baseTableName, - blocks, - blocksToDelete, - columnPrefix: `${columnPrefix || ''}${toSnakeCase(tab.name)}_`, - data: tabData, - existingLocales, - fieldPrefix: `${fieldPrefix || ''}${tab.name}_`, - fields: tab.fields, - locales, - numbers, - parentTableName, - path: `${path || ''}${tab.name}.`, - relationships, - relationshipsToDelete, - row, - selects, - texts, - withinArrayOrBlockLocale, - }) - } - } - } else { - traverseFields({ - adapter, - arrays, - baseTableName, - blocks, - blocksToDelete, - columnPrefix, - data, - existingLocales, - fieldPrefix, - fields: tab.fields, - locales, - numbers, - parentTableName, - path, - relationships, - relationshipsToDelete, - row, - selects, - texts, - withinArrayOrBlockLocale, - }) - } - }) - } - - if (field.type === 'row' || field.type === 'collapsible') { - traverseFields({ - adapter, - arrays, - baseTableName, - blocks, - blocksToDelete, - columnPrefix, - data, - existingLocales, - fieldPrefix, - fields: field.fields, - forcedLocale, - locales, - numbers, - parentTableName, - path, - relationships, - relationshipsToDelete, - row, - selects, - texts, - withinArrayOrBlockLocale, - }) - } - if (field.type === 'relationship' || field.type === 'upload') { const relationshipPath = `${path || ''}${field.name}` @@ -559,61 +438,59 @@ export const traverseFields = ({ return } - if (fieldAffectsData(field)) { - const valuesToTransform: { localeKey?: string; ref: unknown; value: unknown }[] = [] + const valuesToTransform: { localeKey?: string; ref: unknown; value: unknown }[] = [] - if (field.localized) { - if (typeof fieldData === 'object' && fieldData !== null) { - Object.entries(fieldData).forEach(([localeKey, localeData]) => { - if (!locales[localeKey]) { - locales[localeKey] = {} - } + if (field.localized) { + if (typeof fieldData === 'object' && fieldData !== null) { + Object.entries(fieldData).forEach(([localeKey, localeData]) => { + if (!locales[localeKey]) { + locales[localeKey] = {} + } - valuesToTransform.push({ - localeKey, - ref: locales, - value: localeData, - }) + valuesToTransform.push({ + localeKey, + ref: locales, + value: localeData, }) - } - } else { - let ref = row + }) + } + } else { + let ref = row - if (forcedLocale) { - if (!locales[forcedLocale]) { - locales[forcedLocale] = {} - } - ref = locales[forcedLocale] + if (forcedLocale) { + if (!locales[forcedLocale]) { + locales[forcedLocale] = {} } - - valuesToTransform.push({ ref, value: fieldData }) + ref = locales[forcedLocale] } - valuesToTransform.forEach(({ localeKey, ref, value }) => { - if (typeof value !== 'undefined') { - let formattedValue = value - if (value && field.type === 'point' && adapter.name !== 'sqlite') { - formattedValue = sql`ST_GeomFromGeoJSON(${JSON.stringify(value)})` - } + valuesToTransform.push({ ref, value: fieldData }) + } - if (field.type === 'date') { - if (typeof value === 'number' && !Number.isNaN(value)) { - formattedValue = new Date(value).toISOString() - } else if (value instanceof Date) { - formattedValue = value.toISOString() - } else if (fieldName === 'updatedAt') { - // let the db handle this - formattedValue = new Date().toISOString() - } - } + valuesToTransform.forEach(({ localeKey, ref, value }) => { + if (typeof value !== 'undefined') { + let formattedValue = value + if (value && field.type === 'point' && adapter.name !== 'sqlite') { + formattedValue = sql`ST_GeomFromGeoJSON(${JSON.stringify(value)})` + } - if (localeKey) { - ref[localeKey][fieldName] = formattedValue - } else { - ref[fieldName] = formattedValue + if (field.type === 'date') { + if (typeof value === 'number' && !Number.isNaN(value)) { + formattedValue = new Date(value).toISOString() + } else if (value instanceof Date) { + formattedValue = value.toISOString() + } else if (fieldName === 'updatedAt') { + // let the db handle this + formattedValue = new Date().toISOString() } } - }) - } + + if (localeKey) { + ref[localeKey][fieldName] = formattedValue + } else { + ref[fieldName] = formattedValue + } + } + }) }) } diff --git a/packages/drizzle/src/update.ts b/packages/drizzle/src/update.ts index 94505642029..3c2c8dcf7b4 100644 --- a/packages/drizzle/src/update.ts +++ b/packages/drizzle/src/update.ts @@ -20,7 +20,7 @@ export const updateOne: UpdateOne = async function updateOne( const { joins, selectFields, where } = buildQuery({ adapter: this, - fields: collection.fields, + fields: collection.flattenedFields, locale, tableName, where: whereToUse, @@ -45,7 +45,7 @@ export const updateOne: UpdateOne = async function updateOne( adapter: this, data, db, - fields: collection.fields, + fields: collection.flattenedFields, joinQuery, operation: 'update', req, diff --git a/packages/drizzle/src/updateGlobal.ts b/packages/drizzle/src/updateGlobal.ts index 18dfe83fc49..b7bd6ba032b 100644 --- a/packages/drizzle/src/updateGlobal.ts +++ b/packages/drizzle/src/updateGlobal.ts @@ -21,7 +21,7 @@ export async function updateGlobal>( adapter: this, data, db, - fields: globalConfig.fields, + fields: globalConfig.flattenedFields, req, select, tableName, diff --git a/packages/drizzle/src/updateGlobalVersion.ts b/packages/drizzle/src/updateGlobalVersion.ts index 3b61fa57bfc..3c5a223537f 100644 --- a/packages/drizzle/src/updateGlobalVersion.ts +++ b/packages/drizzle/src/updateGlobalVersion.ts @@ -36,7 +36,7 @@ export async function updateGlobalVersion( `_${toSnakeCase(globalConfig.slug)}${this.versionsSuffix}`, ) - const fields = buildVersionGlobalFields(this.payload.config, globalConfig) + const fields = buildVersionGlobalFields(this.payload.config, globalConfig, true) const { where } = buildQuery({ adapter: this, diff --git a/packages/drizzle/src/updateVersion.ts b/packages/drizzle/src/updateVersion.ts index fbba1197b88..09fcaf0dbd5 100644 --- a/packages/drizzle/src/updateVersion.ts +++ b/packages/drizzle/src/updateVersion.ts @@ -33,7 +33,7 @@ export async function updateVersion( `_${toSnakeCase(collectionConfig.slug)}${this.versionsSuffix}`, ) - const fields = buildVersionCollectionFields(this.payload.config, collectionConfig) + const fields = buildVersionCollectionFields(this.payload.config, collectionConfig, true) const { where } = buildQuery({ adapter: this, diff --git a/packages/drizzle/src/upsertRow/types.ts b/packages/drizzle/src/upsertRow/types.ts index 81f0aa701ea..5f69d59bc3d 100644 --- a/packages/drizzle/src/upsertRow/types.ts +++ b/packages/drizzle/src/upsertRow/types.ts @@ -1,5 +1,5 @@ import type { SQL } from 'drizzle-orm' -import type { Field, JoinQuery, PayloadRequest, SelectType } from 'payload' +import type { FlattenedField, JoinQuery, PayloadRequest, SelectType } from 'payload' import type { DrizzleAdapter, DrizzleTransaction, GenericColumn } from '../types.js' @@ -7,7 +7,7 @@ type BaseArgs = { adapter: DrizzleAdapter data: Record db: DrizzleAdapter['drizzle'] | DrizzleTransaction - fields: Field[] + fields: FlattenedField[] /** * When true, skips reading the data back from the database and returns the input data * @default false diff --git a/packages/payload/src/collections/config/client.ts b/packages/payload/src/collections/config/client.ts index 4404a713308..666c0a8def2 100644 --- a/packages/payload/src/collections/config/client.ts +++ b/packages/payload/src/collections/config/client.ts @@ -16,7 +16,7 @@ import { deepCopyObjectSimple } from '../../utilities/deepCopyObject.js' export type ServerOnlyCollectionProperties = keyof Pick< SanitizedCollectionConfig, - 'access' | 'custom' | 'endpoints' | 'hooks' | 'joins' + 'access' | 'custom' | 'endpoints' | 'flattenedFields' | 'hooks' | 'joins' > export type ServerOnlyCollectionAdminProperties = keyof Pick< @@ -60,6 +60,7 @@ const serverOnlyCollectionProperties: Partial[] 'endpoints', 'custom', 'joins', + 'flattenedFields', // `upload` // `admin` // are all handled separately diff --git a/packages/payload/src/collections/config/sanitize.ts b/packages/payload/src/collections/config/sanitize.ts index c26fab9170e..2ad6fe00dce 100644 --- a/packages/payload/src/collections/config/sanitize.ts +++ b/packages/payload/src/collections/config/sanitize.ts @@ -9,6 +9,7 @@ import { fieldAffectsData } from '../../fields/config/types.js' import mergeBaseFields from '../../fields/mergeBaseFields.js' import { getBaseUploadFields } from '../../uploads/getBaseFields.js' import { deepMergeWithReactComponents } from '../../utilities/deepMerge.js' +import { flattenAllFields } from '../../utilities/flattenAllFields.js' import { formatLabels } from '../../utilities/formatLabels.js' import baseVersionFields from '../../versions/baseFields.js' import { versionDefaults } from '../../versions/defaults.js' @@ -207,5 +208,7 @@ export const sanitizeCollection = async ( sanitizedConfig.joins = joins + sanitizedConfig.flattenedFields = flattenAllFields({ fields: sanitizedConfig.fields }) + return sanitizedConfig } diff --git a/packages/payload/src/collections/config/types.ts b/packages/payload/src/collections/config/types.ts index 957c8b69842..91febfcc4f0 100644 --- a/packages/payload/src/collections/config/types.ts +++ b/packages/payload/src/collections/config/types.ts @@ -31,7 +31,13 @@ import type { StaticLabel, } from '../../config/types.js' import type { DBIdentifierName } from '../../database/types.js' -import type { Field, JoinField, RelationshipField, UploadField } from '../../fields/config/types.js' +import type { + Field, + FlattenedField, + JoinField, + RelationshipField, + UploadField, +} from '../../fields/config/types.js' import type { CollectionSlug, JsonObject, @@ -525,11 +531,18 @@ export interface SanitizedCollectionConfig auth: Auth endpoints: Endpoint[] | false fields: Field[] - slug: CollectionSlug + + /** + * Fields in the database schema structure + * Rows / collapsible / tabs w/o name `fields` merged to top, UIs are excluded + */ + flattenedFields: FlattenedField[] + /** * Object of collections to join 'Join Fields object keyed by collection */ joins: SanitizedJoins + slug: CollectionSlug upload: SanitizedUploadConfig versions: SanitizedCollectionVersions } diff --git a/packages/payload/src/collections/operations/countVersions.ts b/packages/payload/src/collections/operations/countVersions.ts index ad0a0384dcb..8c7ffa639a8 100644 --- a/packages/payload/src/collections/operations/countVersions.ts +++ b/packages/payload/src/collections/operations/countVersions.ts @@ -73,7 +73,7 @@ export const countVersionsOperation = async ( const fullWhere = combineQueries(where, accessResult) - const versionFields = buildVersionCollectionFields(payload.config, collectionConfig) + const versionFields = buildVersionCollectionFields(payload.config, collectionConfig, true) await validateQueryPaths({ collectionConfig, diff --git a/packages/payload/src/collections/operations/find.ts b/packages/payload/src/collections/operations/find.ts index c183df9aaa9..334dbaf80cb 100644 --- a/packages/payload/src/collections/operations/find.ts +++ b/packages/payload/src/collections/operations/find.ts @@ -147,7 +147,7 @@ export const findOperation = async < collectionConfig: collection.config, overrideAccess, req, - versionFields: buildVersionCollectionFields(payload.config, collection.config), + versionFields: buildVersionCollectionFields(payload.config, collection.config, true), where: fullWhere, }) diff --git a/packages/payload/src/collections/operations/findVersions.ts b/packages/payload/src/collections/operations/findVersions.ts index 0993131ee24..0a0849b8dee 100644 --- a/packages/payload/src/collections/operations/findVersions.ts +++ b/packages/payload/src/collections/operations/findVersions.ts @@ -56,7 +56,7 @@ export const findVersionsOperation = async accessResults = await executeAccess({ req }, collectionConfig.access.readVersions) } - const versionFields = buildVersionCollectionFields(payload.config, collectionConfig) + const versionFields = buildVersionCollectionFields(payload.config, collectionConfig, true) await validateQueryPaths({ collectionConfig, diff --git a/packages/payload/src/collections/operations/update.ts b/packages/payload/src/collections/operations/update.ts index 9447f09c7fd..3694501631b 100644 --- a/packages/payload/src/collections/operations/update.ts +++ b/packages/payload/src/collections/operations/update.ts @@ -141,7 +141,7 @@ export const updateOperation = async < collectionConfig: collection.config, overrideAccess, req, - versionFields: buildVersionCollectionFields(payload.config, collection.config), + versionFields: buildVersionCollectionFields(payload.config, collection.config, true), where: versionsWhere, }) diff --git a/packages/payload/src/database/getLocalizedPaths.ts b/packages/payload/src/database/getLocalizedPaths.ts index b8898cfaf5f..4df08731e96 100644 --- a/packages/payload/src/database/getLocalizedPaths.ts +++ b/packages/payload/src/database/getLocalizedPaths.ts @@ -1,11 +1,8 @@ -import type { Field } from '../fields/config/types.js' +import type { Field, FlattenedField } from '../fields/config/types.js' import type { Payload } from '../index.js' import type { PathToQuery } from './queryValidation/types.js' -import { fieldAffectsData } from '../fields/config/types.js' -import flattenFields from '../utilities/flattenTopLevelFields.js' - -export async function getLocalizedPaths({ +export function getLocalizedPaths({ collectionSlug, fields, globalSlug, @@ -15,13 +12,13 @@ export async function getLocalizedPaths({ payload, }: { collectionSlug?: string - fields: Field[] + fields: FlattenedField[] globalSlug?: string incomingPath: string locale?: string overrideAccess?: boolean payload: Payload -}): Promise { +}): PathToQuery[] { const pathSegments = incomingPath.split('.') const localizationConfig = payload.config.localization @@ -30,7 +27,7 @@ export async function getLocalizedPaths({ collectionSlug, complete: false, field: undefined, - fields: flattenFields(fields, false), + fields, globalSlug, invalid: false, path: '', @@ -46,9 +43,15 @@ export async function getLocalizedPaths({ const { path } = lastIncompletePath let currentPath = path ? `${path}.${segment}` : segment - const matchedField = lastIncompletePath.fields.find( - (field) => fieldAffectsData(field) && field.name === segment, - ) + let fieldsToSearch: FlattenedField[] + + if (lastIncompletePath?.field && 'flattenedFields' in lastIncompletePath.field) { + fieldsToSearch = lastIncompletePath.field.flattenedFields + } else { + fieldsToSearch = lastIncompletePath.fields + } + + const matchedField = fieldsToSearch.find((field) => field.name === segment) lastIncompletePath.field = matchedField if (currentPath === 'globalType' && globalSlug) { @@ -131,9 +134,9 @@ export async function getLocalizedPaths({ if (nestedPathToQuery) { const relatedCollection = payload.collections[matchedField.relationTo].config - const remainingPaths = await getLocalizedPaths({ + const remainingPaths = getLocalizedPaths({ collectionSlug: relatedCollection.slug, - fields: relatedCollection.fields, + fields: relatedCollection.flattenedFields, globalSlug, incomingPath: nestedPathToQuery, locale, @@ -150,10 +153,6 @@ export async function getLocalizedPaths({ } default: { - if ('fields' in lastIncompletePath.field) { - lastIncompletePath.fields = flattenFields(lastIncompletePath.field.fields, false) - } - if (i + 1 === pathSegments.length) { lastIncompletePath.complete = true } diff --git a/packages/payload/src/database/queryValidation/types.ts b/packages/payload/src/database/queryValidation/types.ts index 4e77416c4fc..ef2c2d6074d 100644 --- a/packages/payload/src/database/queryValidation/types.ts +++ b/packages/payload/src/database/queryValidation/types.ts @@ -1,5 +1,5 @@ import type { CollectionPermission, GlobalPermission } from '../../auth/index.js' -import type { Field, FieldAffectingData, TabAsField, UIField } from '../../fields/config/types.js' +import type { FlattenedField } from '../../fields/config/types.js' export type EntityPolicies = { collections?: { @@ -13,8 +13,8 @@ export type EntityPolicies = { export type PathToQuery = { collectionSlug?: string complete: boolean - field: Field | TabAsField - fields?: (FieldAffectingData | TabAsField | UIField)[] + field: FlattenedField + fields?: FlattenedField[] globalSlug?: string invalid?: boolean path: string diff --git a/packages/payload/src/database/queryValidation/validateQueryPaths.ts b/packages/payload/src/database/queryValidation/validateQueryPaths.ts index 1639fb4256c..af952feb00a 100644 --- a/packages/payload/src/database/queryValidation/validateQueryPaths.ts +++ b/packages/payload/src/database/queryValidation/validateQueryPaths.ts @@ -1,13 +1,11 @@ import type { SanitizedCollectionConfig } from '../../collections/config/types.js' -import type { Field, FieldAffectingData } from '../../fields/config/types.js' +import type { FlattenedField } from '../../fields/config/types.js' import type { SanitizedGlobalConfig } from '../../globals/config/types.js' import type { Operator, PayloadRequest, Where, WhereField } from '../../types/index.js' import type { EntityPolicies } from './types.js' import { QueryError } from '../../errors/QueryError.js' import { validOperators } from '../../types/constants.js' -import { deepCopyObject } from '../../utilities/deepCopyObject.js' -import flattenFields from '../../utilities/flattenTopLevelFields.js' import { validateSearchParam } from './validateSearchParams.js' type Args = { @@ -15,7 +13,7 @@ type Args = { overrideAccess: boolean policies?: EntityPolicies req: PayloadRequest - versionFields?: Field[] + versionFields?: FlattenedField[] where: Where } & ( | { @@ -54,9 +52,8 @@ export async function validateQueryPaths({ versionFields, where, }: Args): Promise { - const fields = flattenFields( - versionFields || (globalConfig || collectionConfig).fields, - ) as FieldAffectingData[] + const fields = versionFields || (globalConfig || collectionConfig).flattenedFields + if (typeof where === 'object') { const whereFields = flattenWhere(where) // We need to determine if the whereKey is an AND, OR, or a schema path @@ -67,10 +64,10 @@ export async function validateQueryPaths({ if (validOperators.includes(operator as Operator)) { promises.push( validateSearchParam({ - collectionConfig: deepCopyObject(collectionConfig), + collectionConfig, errors, - fields: fields as Field[], - globalConfig: deepCopyObject(globalConfig), + fields, + globalConfig, operator, overrideAccess, path, diff --git a/packages/payload/src/database/queryValidation/validateSearchParams.ts b/packages/payload/src/database/queryValidation/validateSearchParams.ts index 5df42fea5ff..84fa0845a5b 100644 --- a/packages/payload/src/database/queryValidation/validateSearchParams.ts +++ b/packages/payload/src/database/queryValidation/validateSearchParams.ts @@ -1,5 +1,5 @@ import type { SanitizedCollectionConfig } from '../../collections/config/types.js' -import type { Field } from '../../fields/config/types.js' +import type { FlattenedField } from '../../fields/config/types.js' import type { SanitizedGlobalConfig } from '../../globals/config/types.js' import type { PayloadRequest } from '../../types/index.js' import type { EntityPolicies, PathToQuery } from './types.js' @@ -13,7 +13,7 @@ import { validateQueryPaths } from './validateQueryPaths.js' type Args = { collectionConfig?: SanitizedCollectionConfig errors: { path: string }[] - fields: Field[] + fields: FlattenedField[] globalConfig?: SanitizedGlobalConfig operator: string overrideAccess: boolean @@ -21,7 +21,7 @@ type Args = { policies: EntityPolicies req: PayloadRequest val: unknown - versionFields?: Field[] + versionFields?: FlattenedField[] } /** @@ -51,8 +51,6 @@ export async function validateSearchParam({ const { slug } = collectionConfig || globalConfig if (globalConfig && !policies.globals[slug]) { - globalConfig.fields = fields - policies.globals[slug] = await getEntityPolicies({ type: 'global', entity: globalConfig, @@ -62,7 +60,7 @@ export async function validateSearchParam({ } if (sanitizedPath !== 'id') { - paths = await getLocalizedPaths({ + paths = getLocalizedPaths({ collectionSlug: collectionConfig?.slug, fields, globalSlug: globalConfig?.slug, diff --git a/packages/payload/src/fields/config/types.ts b/packages/payload/src/fields/config/types.ts index 963d3294519..2f6b36ac53c 100644 --- a/packages/payload/src/fields/config/types.ts +++ b/packages/payload/src/fields/config/types.ts @@ -3,7 +3,7 @@ import type { EditorProps } from '@monaco-editor/react' import type { JSONSchema4 } from 'json-schema' import type { CSSProperties } from 'react' -import type { DeepUndefinable } from 'ts-essentials' +import type { DeepUndefinable, MarkRequired } from 'ts-essentials' import type { JoinFieldClientProps, @@ -1396,6 +1396,46 @@ export type JoinFieldClient = { } & FieldBaseClient & Pick +export type FlattenedBlock = { + flattenedFields: FlattenedField[] +} & Block + +export type FlattenedBlocksField = { + blocks: FlattenedBlock[] +} & BlocksField + +export type FlattenedGroupField = { + flattenedFields: FlattenedField[] +} & GroupField + +export type FlattenedArrayField = { + flattenedFields: FlattenedField[] +} & ArrayField + +export type FlattenedTabAsField = { + flattenedFields: FlattenedField[] +} & MarkRequired + +export type FlattenedField = + | CheckboxField + | CodeField + | DateField + | EmailField + | FlattenedArrayField + | FlattenedBlocksField + | FlattenedGroupField + | FlattenedTabAsField + | JoinField + | JSONField + | NumberField + | PointField + | RadioField + | RelationshipField + | RichTextField + | SelectField + | TextareaField + | TextField + | UploadField export type Field = | ArrayField | BlocksField diff --git a/packages/payload/src/globals/config/client.ts b/packages/payload/src/globals/config/client.ts index bbc52649cde..7a59eecdf16 100644 --- a/packages/payload/src/globals/config/client.ts +++ b/packages/payload/src/globals/config/client.ts @@ -14,7 +14,7 @@ import { deepCopyObjectSimple } from '../../utilities/deepCopyObject.js' export type ServerOnlyGlobalProperties = keyof Pick< SanitizedGlobalConfig, - 'access' | 'admin' | 'custom' | 'endpoints' | 'fields' | 'hooks' + 'access' | 'admin' | 'custom' | 'endpoints' | 'fields' | 'flattenedFields' | 'hooks' > export type ServerOnlyGlobalAdminProperties = keyof Pick @@ -36,6 +36,7 @@ const serverOnlyProperties: Partial[] = [ 'access', 'endpoints', 'custom', + 'flattenedFields', // `admin` is handled separately ] diff --git a/packages/payload/src/globals/config/sanitize.ts b/packages/payload/src/globals/config/sanitize.ts index f169bd76422..c672c6a3d70 100644 --- a/packages/payload/src/globals/config/sanitize.ts +++ b/packages/payload/src/globals/config/sanitize.ts @@ -5,6 +5,7 @@ import defaultAccess from '../../auth/defaultAccess.js' import { sanitizeFields } from '../../fields/config/sanitize.js' import { fieldAffectsData } from '../../fields/config/types.js' import mergeBaseFields from '../../fields/mergeBaseFields.js' +import { flattenAllFields } from '../../utilities/flattenAllFields.js' import { toWords } from '../../utilities/formatLabels.js' import baseVersionFields from '../../versions/baseFields.js' import { versionDefaults } from '../../versions/defaults.js' @@ -143,6 +144,8 @@ export const sanitizeGlobals = async ( }) } + ;(global as SanitizedGlobalConfig).flattenedFields = flattenAllFields({ fields: global.fields }) + globals[i] = global } diff --git a/packages/payload/src/globals/config/types.ts b/packages/payload/src/globals/config/types.ts index ac2fb3c438f..d685c0ee0b3 100644 --- a/packages/payload/src/globals/config/types.ts +++ b/packages/payload/src/globals/config/types.ts @@ -18,7 +18,7 @@ import type { MetaConfig, } from '../../config/types.js' import type { DBIdentifierName } from '../../database/types.js' -import type { Field } from '../../fields/config/types.js' +import type { Field, FlattenedField } from '../../fields/config/types.js' import type { GlobalSlug, RequestContext, TypedGlobal, TypedGlobalSelect } from '../../index.js' import type { PayloadRequest, Where } from '../../types/index.js' import type { IncomingGlobalVersions, SanitizedGlobalVersions } from '../../versions/types.js' @@ -191,7 +191,14 @@ export type GlobalConfig = { export interface SanitizedGlobalConfig extends Omit, 'endpoints' | 'fields' | 'slug' | 'versions'> { endpoints: Endpoint[] | false + fields: Field[] + + /** + * Fields in the database schema structure + * Rows / collapsible / tabs w/o name `fields` merged to top, UIs are excluded + */ + flattenedFields: FlattenedField[] slug: GlobalSlug versions: SanitizedGlobalVersions } diff --git a/packages/payload/src/globals/operations/countGlobalVersions.ts b/packages/payload/src/globals/operations/countGlobalVersions.ts index c7a07838267..fbba748165c 100644 --- a/packages/payload/src/globals/operations/countGlobalVersions.ts +++ b/packages/payload/src/globals/operations/countGlobalVersions.ts @@ -51,7 +51,7 @@ export const countGlobalVersionsOperation = async ( const fullWhere = combineQueries(where, accessResult) - const versionFields = buildVersionGlobalFields(payload.config, global) + const versionFields = buildVersionGlobalFields(payload.config, global, true) await validateQueryPaths({ globalConfig: global, diff --git a/packages/payload/src/globals/operations/findVersions.ts b/packages/payload/src/globals/operations/findVersions.ts index f36f1c95b04..e26f2766971 100644 --- a/packages/payload/src/globals/operations/findVersions.ts +++ b/packages/payload/src/globals/operations/findVersions.ts @@ -45,7 +45,7 @@ export const findVersionsOperation = async >( where, } = args - const versionFields = buildVersionGlobalFields(payload.config, globalConfig) + const versionFields = buildVersionGlobalFields(payload.config, globalConfig, true) try { // ///////////////////////////////////// diff --git a/packages/payload/src/index.ts b/packages/payload/src/index.ts index 94a1bcdb0c3..71c82ad4573 100644 --- a/packages/payload/src/index.ts +++ b/packages/payload/src/index.ts @@ -1144,6 +1144,12 @@ export type { FieldWithSubFieldsClient, FilterOptions, FilterOptionsProps, + FlattenedArrayField, + FlattenedBlock, + FlattenedBlocksField, + FlattenedField, + FlattenedGroupField, + FlattenedTabAsField, GroupField, GroupFieldClient, HookName, @@ -1326,6 +1332,7 @@ export { pathExistsAndIsAccessible, pathExistsAndIsAccessibleSync, } from './utilities/findUp.js' +export { flattenAllFields } from './utilities/flattenAllFields.js' export { default as flattenTopLevelFields } from './utilities/flattenTopLevelFields.js' export { formatErrors } from './utilities/formatErrors.js' export { formatLabels, formatNames, toWords } from './utilities/formatLabels.js' diff --git a/packages/payload/src/queues/config/generateJobsJSONSchemas.ts b/packages/payload/src/queues/config/generateJobsJSONSchemas.ts index 2a52e5c461e..e04cc4bd850 100644 --- a/packages/payload/src/queues/config/generateJobsJSONSchemas.ts +++ b/packages/payload/src/queues/config/generateJobsJSONSchemas.ts @@ -4,6 +4,7 @@ import type { SanitizedConfig } from '../../config/types.js' import type { JobsConfig } from './types/index.js' import { fieldsToJSONSchema } from '../../utilities/configToJSONSchema.js' +import { flattenAllFields } from '../../utilities/flattenAllFields.js' export function generateJobsJSONSchemas( config: SanitizedConfig, @@ -39,7 +40,7 @@ export function generateJobsJSONSchemas( if (task?.inputSchema?.length) { const inputJsonSchema = fieldsToJSONSchema( collectionIDFieldTypes, - task.inputSchema, + flattenAllFields({ fields: task.inputSchema }), interfaceNameDefinitions, config, ) @@ -57,7 +58,7 @@ export function generateJobsJSONSchemas( if (task?.outputSchema?.length) { const outputJsonSchema = fieldsToJSONSchema( collectionIDFieldTypes, - task.outputSchema, + flattenAllFields({ fields: task.outputSchema }), interfaceNameDefinitions, config, ) @@ -123,7 +124,7 @@ export function generateJobsJSONSchemas( if (workflow?.inputSchema?.length) { const inputJsonSchema = fieldsToJSONSchema( collectionIDFieldTypes, - workflow.inputSchema, + flattenAllFields({ fields: workflow.inputSchema }), interfaceNameDefinitions, config, ) diff --git a/packages/payload/src/utilities/configToJSONSchema.ts b/packages/payload/src/utilities/configToJSONSchema.ts index 3216f391b64..2501f2ea8ce 100644 --- a/packages/payload/src/utilities/configToJSONSchema.ts +++ b/packages/payload/src/utilities/configToJSONSchema.ts @@ -6,17 +6,17 @@ const { singular } = pluralize import type { Auth } from '../auth/types.js' import type { SanitizedCollectionConfig } from '../collections/config/types.js' import type { SanitizedConfig } from '../config/types.js' -import type { Field, FieldAffectingData, Option } from '../fields/config/types.js' +import type { FieldAffectingData, FlattenedField, Option } from '../fields/config/types.js' import type { SanitizedGlobalConfig } from '../globals/config/types.js' import { MissingEditorProp } from '../errors/MissingEditorProp.js' -import { fieldAffectsData, tabHasName } from '../fields/config/types.js' +import { fieldAffectsData } from '../fields/config/types.js' import { generateJobsJSONSchemas } from '../queues/config/generateJobsJSONSchemas.js' import { deepCopyObject } from './deepCopyObject.js' import { toWords } from './formatLabels.js' import { getCollectionIDFieldTypes } from './getCollectionIDFieldTypes.js' -const fieldIsRequired = (field: Field) => { +const fieldIsRequired = (field: FlattenedField) => { const isConditional = Boolean(field?.admin && field?.admin?.condition) if (isConditional) { return false @@ -29,17 +29,7 @@ const fieldIsRequired = (field: Field) => { // if any subfields are required, this field is required if ('fields' in field && field.type !== 'array') { - return field.fields.some((subField) => fieldIsRequired(subField)) - } - - // if any tab subfields have required fields, this field is required - if (field.type === 'tabs') { - return field.tabs.some((tab) => { - if ('name' in tab) { - return tab.fields.some((subField) => fieldIsRequired(subField)) - } - return false - }) + return field.flattenedFields.some((subField) => fieldIsRequired(subField)) } return false @@ -214,7 +204,7 @@ export function fieldsToJSONSchema( * if they have custom ID fields. */ collectionIDFieldTypes: { [key: string]: 'number' | 'string' }, - fields: Field[], + fields: FlattenedField[], /** * Allows you to define new top-level interfaces that can be re-used in the output schema. */ @@ -247,7 +237,7 @@ export function fieldsToJSONSchema( additionalProperties: false, ...fieldsToJSONSchema( collectionIDFieldTypes, - field.fields, + field.flattenedFields, interfaceNameDefinitions, config, ), @@ -276,7 +266,7 @@ export function fieldsToJSONSchema( oneOf: field.blocks.map((block) => { const blockFieldSchemas = fieldsToJSONSchema( collectionIDFieldTypes, - block.fields, + block.flattenedFields, interfaceNameDefinitions, config, ) @@ -320,30 +310,14 @@ export function fieldsToJSONSchema( break } - case 'collapsible': - case 'row': { - const childSchema = fieldsToJSONSchema( - collectionIDFieldTypes, - field.fields, - interfaceNameDefinitions, - config, - ) - Object.entries(childSchema.properties).forEach(([propName, propSchema]) => { - fieldSchemas.set(propName, propSchema) - }) - childSchema.required.forEach((propName) => { - requiredFieldNames.add(propName) - }) - break - } - - case 'group': { + case 'group': + case 'tab': { fieldSchema = { type: 'object', additionalProperties: false, ...fieldsToJSONSchema( collectionIDFieldTypes, - field.fields, + field.flattenedFields, interfaceNameDefinitions, config, ), @@ -569,40 +543,6 @@ export function fieldsToJSONSchema( break } - case 'tabs': { - field.tabs.forEach((tab) => { - const childSchema = fieldsToJSONSchema( - collectionIDFieldTypes, - tab.fields, - interfaceNameDefinitions, - config, - ) - if (tabHasName(tab)) { - // could have interface - fieldSchemas.set(tab.name, { - type: 'object', - additionalProperties: false, - ...childSchema, - }) - - // If the named tab has any required fields then we mark this as required otherwise it should be optional - const hasRequiredFields = tab.fields.some((subField) => fieldIsRequired(subField)) - - if (hasRequiredFields) { - requiredFieldNames.add(tab.name) - } - } else { - Object.entries(childSchema.properties).forEach(([propName, propSchema]) => { - fieldSchemas.set(propName, propSchema) - }) - childSchema.required.forEach((propName) => { - requiredFieldNames.add(propName) - }) - } - }) - break - } - case 'text': if (field.hasMany === true) { fieldSchema = { @@ -647,26 +587,27 @@ export function entityToJSONSchema( if (!collectionIDFieldTypes) { collectionIDFieldTypes = getCollectionIDFieldTypes({ config, defaultIDType }) } + const entity: SanitizedCollectionConfig | SanitizedGlobalConfig = deepCopyObject(incomingEntity) const title = entity.typescript?.interface ? entity.typescript.interface : singular(toWords(entity.slug, true)) const idField: FieldAffectingData = { name: 'id', type: defaultIDType as 'text', required: true } - const customIdField = entity.fields.find( - (field) => fieldAffectsData(field) && field.name === 'id', + const customIdField = entity.flattenedFields.find( + (field) => field.name === 'id', ) as FieldAffectingData if (customIdField && customIdField.type !== 'group' && customIdField.type !== 'tab') { customIdField.required = true } else { - entity.fields.unshift(idField) + entity.flattenedFields.unshift(idField) } // mark timestamp fields required if ('timestamps' in entity && entity.timestamps !== false) { - entity.fields = entity.fields.map((field) => { - if (fieldAffectsData(field) && (field.name === 'createdAt' || field.name === 'updatedAt')) { + entity.flattenedFields = entity.flattenedFields.map((field) => { + if (field.name === 'createdAt' || field.name === 'updatedAt') { return { ...field, required: true, @@ -677,7 +618,7 @@ export function entityToJSONSchema( } if ('auth' in entity && entity.auth && !entity.auth?.disableLocalStrategy) { - entity.fields.push({ + entity.flattenedFields.push({ name: 'password', type: 'text', }) @@ -687,11 +628,16 @@ export function entityToJSONSchema( type: 'object', additionalProperties: false, title, - ...fieldsToJSONSchema(collectionIDFieldTypes, entity.fields, interfaceNameDefinitions, config), + ...fieldsToJSONSchema( + collectionIDFieldTypes, + entity.flattenedFields, + interfaceNameDefinitions, + config, + ), } } -export function fieldsToSelectJSONSchema({ fields }: { fields: Field[] }): JSONSchema4 { +export function fieldsToSelectJSONSchema({ fields }: { fields: FlattenedField[] }): JSONSchema4 { const schema: JSONSchema4 = { type: 'object', additionalProperties: false, @@ -702,12 +648,13 @@ export function fieldsToSelectJSONSchema({ fields }: { fields: Field[] }): JSONS switch (field.type) { case 'array': case 'group': + case 'tab': schema.properties[field.name] = { oneOf: [ { type: 'boolean', }, - fieldsToSelectJSONSchema({ fields: field.fields }), + fieldsToSelectJSONSchema({ fields: field.flattenedFields }), ], } break @@ -725,7 +672,7 @@ export function fieldsToSelectJSONSchema({ fields }: { fields: Field[] }): JSONS { type: 'boolean', }, - fieldsToSelectJSONSchema({ fields: block.fields }), + fieldsToSelectJSONSchema({ fields: block.flattenedFields }), ], } } @@ -741,35 +688,6 @@ export function fieldsToSelectJSONSchema({ fields }: { fields: Field[] }): JSONS break } - case 'collapsible': - case 'row': - schema.properties = { - ...schema.properties, - ...fieldsToSelectJSONSchema({ fields: field.fields }).properties, - } - - break - - case 'tabs': - for (const tab of field.tabs) { - if (tabHasName(tab)) { - schema.properties[tab.name] = { - oneOf: [ - { - type: 'boolean', - }, - fieldsToSelectJSONSchema({ fields: tab.fields }), - ], - } - continue - } - - schema.properties = { - ...schema.properties, - ...fieldsToSelectJSONSchema({ fields: tab.fields }).properties, - } - } - break default: schema.properties[field.name] = { @@ -976,7 +894,7 @@ export function configToJSONSchema( defaultIDType, collectionIDFieldTypes, ) - const select = fieldsToSelectJSONSchema({ fields: entity.fields }) + const select = fieldsToSelectJSONSchema({ fields: entity.flattenedFields }) if (type === 'global') { select.properties.globalType = { diff --git a/packages/payload/src/utilities/flattenAllFields.ts b/packages/payload/src/utilities/flattenAllFields.ts new file mode 100644 index 00000000000..8aa7c077376 --- /dev/null +++ b/packages/payload/src/utilities/flattenAllFields.ts @@ -0,0 +1,65 @@ +import type { Field, FlattenedField } from '../fields/config/types.js' + +import { tabHasName } from '../fields/config/types.js' + +export const flattenAllFields = ({ fields }: { fields: Field[] }): FlattenedField[] => { + const result: FlattenedField[] = [] + + for (const field of fields) { + switch (field.type) { + case 'array': + case 'group': { + result.push({ ...field, flattenedFields: flattenAllFields({ fields: field.fields }) }) + break + } + + case 'blocks': { + const blocks = [] + for (const block of field.blocks) { + blocks.push({ + ...block, + flattenedFields: flattenAllFields({ fields: block.fields }), + }) + } + result.push({ + ...field, + blocks, + }) + break + } + + case 'collapsible': + case 'row': { + for (const nestedField of flattenAllFields({ fields: field.fields })) { + result.push(nestedField) + } + break + } + + case 'tabs': { + for (const tab of field.tabs) { + if (!tabHasName(tab)) { + for (const nestedField of flattenAllFields({ fields: tab.fields })) { + result.push(nestedField) + } + } else { + result.push({ + ...tab, + type: 'tab', + flattenedFields: flattenAllFields({ fields: tab.fields }), + }) + } + } + break + } + + default: { + if (field.type !== 'ui') { + result.push(field) + } + } + } + } + + return result +} diff --git a/packages/payload/src/utilities/getEntityPolicies.ts b/packages/payload/src/utilities/getEntityPolicies.ts index 0a91a67bf9c..1f77a970272 100644 --- a/packages/payload/src/utilities/getEntityPolicies.ts +++ b/packages/payload/src/utilities/getEntityPolicies.ts @@ -6,7 +6,7 @@ import type { SanitizedGlobalConfig } from '../globals/config/types.js' import type { AllOperations, Document, PayloadRequest, Where } from '../types/index.js' import { combineQueries } from '../database/combineQueries.js' -import { fieldAffectsData, tabHasName } from '../fields/config/types.js' +import { tabHasName } from '../fields/config/types.js' type Args = { entity: SanitizedCollectionConfig | SanitizedGlobalConfig diff --git a/packages/payload/src/versions/baseFields.ts b/packages/payload/src/versions/baseFields.ts index 458bf3f32aa..3cc55e1fae7 100644 --- a/packages/payload/src/versions/baseFields.ts +++ b/packages/payload/src/versions/baseFields.ts @@ -1,4 +1,4 @@ -import type { Field } from '../fields/config/types.js' +import type { CheckboxField, Field } from '../fields/config/types.js' export const statuses = [ { @@ -33,7 +33,7 @@ const baseVersionFields: Field[] = [ // "snapshot" to retain all existing draft data. // This field will be used to exclude any snapshot versions // from the admin Versions list -export const versionSnapshotField: Field = { +export const versionSnapshotField: CheckboxField = { name: 'snapshot', type: 'checkbox', admin: { diff --git a/packages/payload/src/versions/buildCollectionFields.ts b/packages/payload/src/versions/buildCollectionFields.ts index 89eb41aa854..14872fa130a 100644 --- a/packages/payload/src/versions/buildCollectionFields.ts +++ b/packages/payload/src/versions/buildCollectionFields.ts @@ -1,14 +1,15 @@ import type { SanitizedCollectionConfig } from '../collections/config/types.js' import type { SanitizedConfig } from '../config/types.js' -import type { Field } from '../fields/config/types.js' +import type { Field, FlattenedField } from '../fields/config/types.js' import { versionSnapshotField } from './baseFields.js' -export const buildVersionCollectionFields = ( +export const buildVersionCollectionFields = ( config: SanitizedConfig, collection: SanitizedCollectionConfig, -): Field[] => { - const fields: Field[] = [ + flatten?: T, +): true extends T ? FlattenedField[] : Field[] => { + const fields: FlattenedField[] = [ { name: 'parent', type: 'relationship', @@ -19,6 +20,9 @@ export const buildVersionCollectionFields = ( name: 'version', type: 'group', fields: collection.fields.filter((field) => !('name' in field) || field.name !== 'id'), + ...(flatten && { + flattenedFields: collection.flattenedFields.filter((each) => each.name !== 'id'), + }), }, { name: 'createdAt', @@ -78,5 +82,5 @@ export const buildVersionCollectionFields = ( } } - return fields + return fields as true extends T ? FlattenedField[] : Field[] } diff --git a/packages/payload/src/versions/buildGlobalFields.ts b/packages/payload/src/versions/buildGlobalFields.ts index ed21c3d40ac..4a78c09033c 100644 --- a/packages/payload/src/versions/buildGlobalFields.ts +++ b/packages/payload/src/versions/buildGlobalFields.ts @@ -1,18 +1,22 @@ import type { SanitizedConfig } from '../config/types.js' -import type { Field } from '../fields/config/types.js' +import type { Field, FlattenedField } from '../fields/config/types.js' import type { SanitizedGlobalConfig } from '../globals/config/types.js' import { versionSnapshotField } from './baseFields.js' -export const buildVersionGlobalFields = ( +export const buildVersionGlobalFields = ( config: SanitizedConfig, global: SanitizedGlobalConfig, -): Field[] => { - const fields: Field[] = [ + flatten?: T, +): true extends T ? FlattenedField[] : Field[] => { + const fields: FlattenedField[] = [ { name: 'version', type: 'group', fields: global.fields, + ...(flatten && { + flattenedFields: global.flattenedFields, + }), }, { name: 'createdAt', @@ -72,5 +76,5 @@ export const buildVersionGlobalFields = ( } } - return fields + return fields as true extends T ? FlattenedField[] : Field[] } diff --git a/packages/richtext-lexical/src/features/blocks/server/index.ts b/packages/richtext-lexical/src/features/blocks/server/index.ts index d7005b1f060..e8f98c0e97d 100644 --- a/packages/richtext-lexical/src/features/blocks/server/index.ts +++ b/packages/richtext-lexical/src/features/blocks/server/index.ts @@ -1,6 +1,6 @@ -import type { Block, BlocksField, Config, FieldSchemaMap } from 'payload' +import type { Block, BlocksField, Config, FieldSchemaMap, FlattenedBlocksField } from 'payload' -import { fieldsToJSONSchema, sanitizeFields } from 'payload' +import { fieldsToJSONSchema, flattenAllFields, sanitizeFields } from 'payload' import { createServerFeature } from '../../../utilities/createServerFeature.js' import { createNode } from '../../typeUtilities.js' @@ -56,20 +56,26 @@ export const BlocksFeature = createServerFeature ({ + ...block, + flattenedFields: flattenAllFields({ fields: block.fields }), + })), }) } if (props?.inlineBlocks?.length) { fields.push({ name: field?.name + '_lexical_inline_blocks', type: 'blocks', - blocks: props.inlineBlocks, + blocks: props.inlineBlocks.map((block) => ({ + ...block, + flattenedFields: flattenAllFields({ fields: block.fields }), + })), }) } diff --git a/test/admin/payload-types.ts b/test/admin/payload-types.ts index 577f229749f..bb15b9395a7 100644 --- a/test/admin/payload-types.ts +++ b/test/admin/payload-types.ts @@ -82,9 +82,9 @@ export interface Config { user: User & { collection: 'users'; }; - jobs?: { + jobs: { tasks: unknown; - workflows?: unknown; + workflows: unknown; }; } export interface UserAuthOperations { @@ -280,8 +280,8 @@ export interface Geo { * via the `definition` "customIdTab". */ export interface CustomIdTab { - id: string | null; title?: string | null; + id: string; description?: string | null; number?: number | null; updatedAt: string; @@ -292,8 +292,8 @@ export interface CustomIdTab { * via the `definition` "customIdRow". */ export interface CustomIdRow { - id: string | null; title?: string | null; + id: string; description?: string | null; number?: number | null; updatedAt: string; @@ -463,7 +463,6 @@ export interface PostsSelect { description?: T; number?: T; richText?: T; - demoUIField?: T; group?: | T | { diff --git a/test/fields/collections/Tabs/index.ts b/test/fields/collections/Tabs/index.ts index 1d1da002e8b..8a7121c8bb7 100644 --- a/test/fields/collections/Tabs/index.ts +++ b/test/fields/collections/Tabs/index.ts @@ -113,6 +113,7 @@ const TabsFields: CollectionConfig = { { name: 'tab', label: 'Tab with Name', + interfaceName: 'TabWithName', description: 'This tab has a name, which should namespace the contained fields.', fields: [ { diff --git a/test/fields/payload-types.ts b/test/fields/payload-types.ts index 8f02f35f117..b95b57478d1 100644 --- a/test/fields/payload-types.ts +++ b/test/fields/payload-types.ts @@ -1604,20 +1604,7 @@ export interface TabsField { | number | boolean | null; - tab: { - array: { - text: string; - id?: string | null; - }[]; - text?: string | null; - defaultValue?: string | null; - arrayInRow?: - | { - textInArrayInRow?: string | null; - id?: string | null; - }[] - | null; - }; + tab: TabWithName; namedTabWithDefaultValue?: { defaultValue?: string | null; }; @@ -1655,6 +1642,24 @@ export interface TabsField { updatedAt: string; createdAt: string; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "TabWithName". + */ +export interface TabWithName { + array: { + text: string; + id?: string | null; + }[]; + text?: string | null; + defaultValue?: string | null; + arrayInRow?: + | { + textInArrayInRow?: string | null; + id?: string | null; + }[] + | null; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "uploads". @@ -2092,7 +2097,6 @@ export interface ArrayFieldsSelect { | { text?: T; anotherText?: T; - uiField?: T; localizedText?: T; subArray?: | T @@ -2163,10 +2167,8 @@ export interface ArrayFieldsSelect { externallyUpdatedArray?: | T | { - customField?: T; id?: T; }; - ui?: T; updatedAt?: T; createdAt?: T; } @@ -2552,7 +2554,6 @@ export interface BlockFieldsSelect { blockName?: T; }; }; - ui?: T; relationshipBlocks?: | T | { @@ -3095,7 +3096,6 @@ export interface TabsFields2Select { */ export interface TabsFieldsSelect { sidebarField?: T; - demoUIField?: T; array?: | T | { @@ -3374,7 +3374,6 @@ export interface UploadsRestrictedSelect { */ export interface UiFieldsSelect { text?: T; - uiCustomClient?: T; updatedAt?: T; createdAt?: T; }