From 68a7de2610a2b9226575be82b5fa507c29e12adc Mon Sep 17 00:00:00 2001 From: Sasha <64744993+r1tsuu@users.noreply.github.com> Date: Fri, 31 Jan 2025 18:26:04 +0200 Subject: [PATCH] fix(db-postgres): select hasMany inside arrays and blocks with versions (#10829) Fixes https://github.com/payloadcms/payload/issues/10780 Previously, with enabled versions, nested select `hasMany: true` fields weren't working with SQL database adapters. This was due to wrongly passed `parent` to select rows data because we store arrays and blocks in versions a bit differently, using both, `id` and `_uuid` (which contains the normal Object ID) columns. And unlike with non versions `_uuid` column isn't actually applicable here as it's not unique, thus we need to save blocks/arrays first and then map their ObjectIDs to generated by the database IDs and use them for select fields `parent` data --- .../src/transform/write/traverseFields.ts | 4 ++ packages/drizzle/src/upsertRow/index.ts | 19 +++++++ .../drizzle/src/upsertRow/insertArrays.ts | 18 +++++- .../collections/SelectVersions/index.ts | 47 ++++++++++++++++ test/fields/config.ts | 3 +- test/fields/int.spec.ts | 24 ++++++++ test/fields/payload-types.ts | 56 +++++++++++++++++++ test/fields/slugs.ts | 1 + 8 files changed, 170 insertions(+), 2 deletions(-) create mode 100644 test/fields/collections/SelectVersions/index.ts diff --git a/packages/drizzle/src/transform/write/traverseFields.ts b/packages/drizzle/src/transform/write/traverseFields.ts index d65630cb74..03d040b067 100644 --- a/packages/drizzle/src/transform/write/traverseFields.ts +++ b/packages/drizzle/src/transform/write/traverseFields.ts @@ -88,6 +88,10 @@ export const traverseFields = ({ texts, withinArrayOrBlockLocale, }: Args) => { + if (row._uuid) { + data._uuid = row._uuid + } + fields.forEach((field) => { let columnName = '' let fieldName = '' diff --git a/packages/drizzle/src/upsertRow/index.ts b/packages/drizzle/src/upsertRow/index.ts index dfa0d6c3d5..b2438eca29 100644 --- a/packages/drizzle/src/upsertRow/index.ts +++ b/packages/drizzle/src/upsertRow/index.ts @@ -266,6 +266,9 @@ export const upsertRow = async | TypeWithID>( } } + // When versions are enabled, this is used to track mapping between blocks/arrays ObjectID to their numeric generated representation, then we use it for nested to arrays/blocks select hasMany in versions. + const arraysBlocksUUIDMap: Record = {} + for (const [blockName, blockRows] of Object.entries(blocksToInsert)) { const blockTableName = adapter.tableNameMap.get(`${tableName}_blocks_${blockName}`) insertedBlockRows[blockName] = await adapter.insert({ @@ -276,6 +279,12 @@ export const upsertRow = async | TypeWithID>( insertedBlockRows[blockName].forEach((row, i) => { blockRows[i].row = row + if ( + typeof row._uuid === 'string' && + (typeof row.id === 'string' || typeof row.id === 'number') + ) { + arraysBlocksUUIDMap[row._uuid] = row.id + } }) const blockLocaleIndexMap: number[] = [] @@ -308,6 +317,7 @@ export const upsertRow = async | TypeWithID>( arrays: blockRows.map(({ arrays }) => arrays), db, parentRows: insertedBlockRows[blockName], + uuidMap: arraysBlocksUUIDMap, }) } @@ -331,6 +341,7 @@ export const upsertRow = async | TypeWithID>( arrays: [rowToInsert.arrays], db, parentRows: [insertedRow], + uuidMap: arraysBlocksUUIDMap, }) // ////////////////////////////////// @@ -347,6 +358,14 @@ export const upsertRow = async | TypeWithID>( }) } + if (Object.keys(arraysBlocksUUIDMap).length > 0) { + tableRows.forEach((row: any) => { + if (row.parent in arraysBlocksUUIDMap) { + row.parent = arraysBlocksUUIDMap[row.parent] + } + }) + } + if (tableRows.length) { await adapter.insert({ db, diff --git a/packages/drizzle/src/upsertRow/insertArrays.ts b/packages/drizzle/src/upsertRow/insertArrays.ts index 6f9c72cecc..301bd15c3a 100644 --- a/packages/drizzle/src/upsertRow/insertArrays.ts +++ b/packages/drizzle/src/upsertRow/insertArrays.ts @@ -8,6 +8,7 @@ type Args = { }[] db: DrizzleAdapter['drizzle'] | DrizzleTransaction parentRows: Record[] + uuidMap?: Record } type RowsByTable = { @@ -20,7 +21,13 @@ type RowsByTable = { } } -export const insertArrays = async ({ adapter, arrays, db, parentRows }: Args): Promise => { +export const insertArrays = async ({ + adapter, + arrays, + db, + parentRows, + uuidMap = {}, +}: Args): Promise => { // Maintain a map of flattened rows by table const rowsByTable: RowsByTable = {} @@ -74,6 +81,15 @@ export const insertArrays = async ({ adapter, arrays, db, parentRows }: Args): P tableName, values: row.rows, }) + + insertedRows.forEach((row) => { + if ( + typeof row._uuid === 'string' && + (typeof row.id === 'string' || typeof row.id === 'number') + ) { + uuidMap[row._uuid] = row.id + } + }) } // Insert locale rows diff --git a/test/fields/collections/SelectVersions/index.ts b/test/fields/collections/SelectVersions/index.ts new file mode 100644 index 0000000000..ceb0fa0646 --- /dev/null +++ b/test/fields/collections/SelectVersions/index.ts @@ -0,0 +1,47 @@ +import type { CollectionConfig } from 'payload' + +import { selectVersionsFieldsSlug } from '../../slugs.js' + +const SelectVersionsFields: CollectionConfig = { + slug: selectVersionsFieldsSlug, + versions: true, + fields: [ + { + type: 'select', + hasMany: true, + options: ['a', 'b', 'c'], + name: 'hasMany', + }, + { + type: 'array', + name: 'array', + fields: [ + { + type: 'select', + hasMany: true, + options: ['a', 'b', 'c'], + name: 'hasManyArr', + }, + ], + }, + { + type: 'blocks', + name: 'blocks', + blocks: [ + { + slug: 'block', + fields: [ + { + type: 'select', + hasMany: true, + options: ['a', 'b', 'c'], + name: 'hasManyBlocks', + }, + ], + }, + ], + }, + ], +} + +export default SelectVersionsFields diff --git a/test/fields/config.ts b/test/fields/config.ts index cf1d6bf3f3..6e58c7a0b7 100644 --- a/test/fields/config.ts +++ b/test/fields/config.ts @@ -33,6 +33,7 @@ import RelationshipFields from './collections/Relationship/index.js' import RichTextFields from './collections/RichText/index.js' import RowFields from './collections/Row/index.js' import SelectFields from './collections/Select/index.js' +import SelectVersionsFields from './collections/SelectVersions/index.js' import TabsFields from './collections/Tabs/index.js' import { TabsFields2 } from './collections/Tabs2/index.js' import TextFields from './collections/Text/index.js' @@ -67,7 +68,7 @@ export const collectionSlugs: CollectionConfig[] = [ ], }, LexicalInBlock, - + SelectVersionsFields, ArrayFields, BlockFields, CheckboxFields, diff --git a/test/fields/int.spec.ts b/test/fields/int.spec.ts index 3d8b6a1a9a..5409945ab3 100644 --- a/test/fields/int.spec.ts +++ b/test/fields/int.spec.ts @@ -678,6 +678,30 @@ describe('Fields', () => { expect(upd.array[0].group.selectHasMany).toStrictEqual(['six']) }) + + it('should work with versions', async () => { + const base = await payload.create({ + collection: 'select-versions-fields', + data: { hasMany: ['a', 'b'] }, + }) + + expect(base.hasMany).toStrictEqual(['a', 'b']) + + const array = await payload.create({ + collection: 'select-versions-fields', + data: { array: [{ hasManyArr: ['a', 'b'] }] }, + draft: true, + }) + + expect(array.array[0]?.hasManyArr).toStrictEqual(['a', 'b']) + + const block = await payload.create({ + collection: 'select-versions-fields', + data: { blocks: [{ blockType: 'block', hasManyBlocks: ['a', 'b'] }] }, + }) + + expect(block.blocks[0]?.hasManyBlocks).toStrictEqual(['a', 'b']) + }) }) describe('number', () => { diff --git a/test/fields/payload-types.ts b/test/fields/payload-types.ts index 02f06bccea..913a00d856 100644 --- a/test/fields/payload-types.ts +++ b/test/fields/payload-types.ts @@ -34,6 +34,7 @@ export interface Config { lexicalObjectReferenceBug: LexicalObjectReferenceBug; users: User; LexicalInBlock: LexicalInBlock; + 'select-versions-fields': SelectVersionsField; 'array-fields': ArrayField; 'block-fields': BlockField; 'checkbox-fields': CheckboxField; @@ -79,6 +80,7 @@ export interface Config { lexicalObjectReferenceBug: LexicalObjectReferenceBugSelect | LexicalObjectReferenceBugSelect; users: UsersSelect | UsersSelect; LexicalInBlock: LexicalInBlockSelect | LexicalInBlockSelect; + 'select-versions-fields': SelectVersionsFieldsSelect | SelectVersionsFieldsSelect; 'array-fields': ArrayFieldsSelect | ArrayFieldsSelect; 'block-fields': BlockFieldsSelect | BlockFieldsSelect; 'checkbox-fields': CheckboxFieldsSelect | CheckboxFieldsSelect; @@ -452,6 +454,30 @@ export interface LexicalInBlock { updatedAt: string; createdAt: string; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "select-versions-fields". + */ +export interface SelectVersionsField { + id: string; + hasMany?: ('a' | 'b' | 'c')[] | null; + array?: + | { + hasManyArr?: ('a' | 'b' | 'c')[] | null; + id?: string | null; + }[] + | null; + blocks?: + | { + hasManyArr?: ('a' | 'b' | 'c')[] | null; + id?: string | null; + blockName?: string | null; + blockType: 'block'; + }[] + | null; + updatedAt: string; + createdAt: string; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "array-fields". @@ -1804,6 +1830,10 @@ export interface PayloadLockedDocument { relationTo: 'LexicalInBlock'; value: string | LexicalInBlock; } | null) + | ({ + relationTo: 'select-versions-fields'; + value: string | SelectVersionsField; + } | null) | ({ relationTo: 'array-fields'; value: string | ArrayField; @@ -2074,6 +2104,32 @@ export interface LexicalInBlockSelect { updatedAt?: T; createdAt?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "select-versions-fields_select". + */ +export interface SelectVersionsFieldsSelect { + hasMany?: T; + array?: + | T + | { + hasManyArr?: T; + id?: T; + }; + blocks?: + | T + | { + block?: + | T + | { + hasManyArr?: T; + id?: T; + blockName?: T; + }; + }; + updatedAt?: T; + createdAt?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "array-fields_select". diff --git a/test/fields/slugs.ts b/test/fields/slugs.ts index 6afe5355e8..cfb3ec03ff 100644 --- a/test/fields/slugs.ts +++ b/test/fields/slugs.ts @@ -24,6 +24,7 @@ export const relationshipFieldsSlug = 'relationship-fields' export const richTextFieldsSlug = 'rich-text-fields' export const rowFieldsSlug = 'row-fields' export const selectFieldsSlug = 'select-fields' +export const selectVersionsFieldsSlug = 'select-versions-fields' export const tabsFieldsSlug = 'tabs-fields' export const tabsFields2Slug = 'tabs-fields-2' export const textFieldsSlug = 'text-fields'