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
This commit is contained in:
Sasha
2025-01-31 18:26:04 +02:00
committed by GitHub
parent e1dcb9594c
commit 68a7de2610
8 changed files with 170 additions and 2 deletions

View File

@@ -88,6 +88,10 @@ export const traverseFields = ({
texts, texts,
withinArrayOrBlockLocale, withinArrayOrBlockLocale,
}: Args) => { }: Args) => {
if (row._uuid) {
data._uuid = row._uuid
}
fields.forEach((field) => { fields.forEach((field) => {
let columnName = '' let columnName = ''
let fieldName = '' let fieldName = ''

View File

@@ -266,6 +266,9 @@ export const upsertRow = async <T extends Record<string, unknown> | 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<string, number | string> = {}
for (const [blockName, blockRows] of Object.entries(blocksToInsert)) { for (const [blockName, blockRows] of Object.entries(blocksToInsert)) {
const blockTableName = adapter.tableNameMap.get(`${tableName}_blocks_${blockName}`) const blockTableName = adapter.tableNameMap.get(`${tableName}_blocks_${blockName}`)
insertedBlockRows[blockName] = await adapter.insert({ insertedBlockRows[blockName] = await adapter.insert({
@@ -276,6 +279,12 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
insertedBlockRows[blockName].forEach((row, i) => { insertedBlockRows[blockName].forEach((row, i) => {
blockRows[i].row = row 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[] = [] const blockLocaleIndexMap: number[] = []
@@ -308,6 +317,7 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
arrays: blockRows.map(({ arrays }) => arrays), arrays: blockRows.map(({ arrays }) => arrays),
db, db,
parentRows: insertedBlockRows[blockName], parentRows: insertedBlockRows[blockName],
uuidMap: arraysBlocksUUIDMap,
}) })
} }
@@ -331,6 +341,7 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
arrays: [rowToInsert.arrays], arrays: [rowToInsert.arrays],
db, db,
parentRows: [insertedRow], parentRows: [insertedRow],
uuidMap: arraysBlocksUUIDMap,
}) })
// ////////////////////////////////// // //////////////////////////////////
@@ -347,6 +358,14 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
}) })
} }
if (Object.keys(arraysBlocksUUIDMap).length > 0) {
tableRows.forEach((row: any) => {
if (row.parent in arraysBlocksUUIDMap) {
row.parent = arraysBlocksUUIDMap[row.parent]
}
})
}
if (tableRows.length) { if (tableRows.length) {
await adapter.insert({ await adapter.insert({
db, db,

View File

@@ -8,6 +8,7 @@ type Args = {
}[] }[]
db: DrizzleAdapter['drizzle'] | DrizzleTransaction db: DrizzleAdapter['drizzle'] | DrizzleTransaction
parentRows: Record<string, unknown>[] parentRows: Record<string, unknown>[]
uuidMap?: Record<string, number | string>
} }
type RowsByTable = { type RowsByTable = {
@@ -20,7 +21,13 @@ type RowsByTable = {
} }
} }
export const insertArrays = async ({ adapter, arrays, db, parentRows }: Args): Promise<void> => { export const insertArrays = async ({
adapter,
arrays,
db,
parentRows,
uuidMap = {},
}: Args): Promise<void> => {
// Maintain a map of flattened rows by table // Maintain a map of flattened rows by table
const rowsByTable: RowsByTable = {} const rowsByTable: RowsByTable = {}
@@ -74,6 +81,15 @@ export const insertArrays = async ({ adapter, arrays, db, parentRows }: Args): P
tableName, tableName,
values: row.rows, 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 // Insert locale rows

View File

@@ -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

View File

@@ -33,6 +33,7 @@ import RelationshipFields from './collections/Relationship/index.js'
import RichTextFields from './collections/RichText/index.js' import RichTextFields from './collections/RichText/index.js'
import RowFields from './collections/Row/index.js' import RowFields from './collections/Row/index.js'
import SelectFields from './collections/Select/index.js' import SelectFields from './collections/Select/index.js'
import SelectVersionsFields from './collections/SelectVersions/index.js'
import TabsFields from './collections/Tabs/index.js' import TabsFields from './collections/Tabs/index.js'
import { TabsFields2 } from './collections/Tabs2/index.js' import { TabsFields2 } from './collections/Tabs2/index.js'
import TextFields from './collections/Text/index.js' import TextFields from './collections/Text/index.js'
@@ -67,7 +68,7 @@ export const collectionSlugs: CollectionConfig[] = [
], ],
}, },
LexicalInBlock, LexicalInBlock,
SelectVersionsFields,
ArrayFields, ArrayFields,
BlockFields, BlockFields,
CheckboxFields, CheckboxFields,

View File

@@ -678,6 +678,30 @@ describe('Fields', () => {
expect(upd.array[0].group.selectHasMany).toStrictEqual(['six']) 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', () => { describe('number', () => {

View File

@@ -34,6 +34,7 @@ export interface Config {
lexicalObjectReferenceBug: LexicalObjectReferenceBug; lexicalObjectReferenceBug: LexicalObjectReferenceBug;
users: User; users: User;
LexicalInBlock: LexicalInBlock; LexicalInBlock: LexicalInBlock;
'select-versions-fields': SelectVersionsField;
'array-fields': ArrayField; 'array-fields': ArrayField;
'block-fields': BlockField; 'block-fields': BlockField;
'checkbox-fields': CheckboxField; 'checkbox-fields': CheckboxField;
@@ -79,6 +80,7 @@ export interface Config {
lexicalObjectReferenceBug: LexicalObjectReferenceBugSelect<false> | LexicalObjectReferenceBugSelect<true>; lexicalObjectReferenceBug: LexicalObjectReferenceBugSelect<false> | LexicalObjectReferenceBugSelect<true>;
users: UsersSelect<false> | UsersSelect<true>; users: UsersSelect<false> | UsersSelect<true>;
LexicalInBlock: LexicalInBlockSelect<false> | LexicalInBlockSelect<true>; LexicalInBlock: LexicalInBlockSelect<false> | LexicalInBlockSelect<true>;
'select-versions-fields': SelectVersionsFieldsSelect<false> | SelectVersionsFieldsSelect<true>;
'array-fields': ArrayFieldsSelect<false> | ArrayFieldsSelect<true>; 'array-fields': ArrayFieldsSelect<false> | ArrayFieldsSelect<true>;
'block-fields': BlockFieldsSelect<false> | BlockFieldsSelect<true>; 'block-fields': BlockFieldsSelect<false> | BlockFieldsSelect<true>;
'checkbox-fields': CheckboxFieldsSelect<false> | CheckboxFieldsSelect<true>; 'checkbox-fields': CheckboxFieldsSelect<false> | CheckboxFieldsSelect<true>;
@@ -452,6 +454,30 @@ export interface LexicalInBlock {
updatedAt: string; updatedAt: string;
createdAt: 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 * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "array-fields". * via the `definition` "array-fields".
@@ -1804,6 +1830,10 @@ export interface PayloadLockedDocument {
relationTo: 'LexicalInBlock'; relationTo: 'LexicalInBlock';
value: string | LexicalInBlock; value: string | LexicalInBlock;
} | null) } | null)
| ({
relationTo: 'select-versions-fields';
value: string | SelectVersionsField;
} | null)
| ({ | ({
relationTo: 'array-fields'; relationTo: 'array-fields';
value: string | ArrayField; value: string | ArrayField;
@@ -2074,6 +2104,32 @@ export interface LexicalInBlockSelect<T extends boolean = true> {
updatedAt?: T; updatedAt?: T;
createdAt?: T; createdAt?: T;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "select-versions-fields_select".
*/
export interface SelectVersionsFieldsSelect<T extends boolean = true> {
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 * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "array-fields_select". * via the `definition` "array-fields_select".

View File

@@ -24,6 +24,7 @@ export const relationshipFieldsSlug = 'relationship-fields'
export const richTextFieldsSlug = 'rich-text-fields' export const richTextFieldsSlug = 'rich-text-fields'
export const rowFieldsSlug = 'row-fields' export const rowFieldsSlug = 'row-fields'
export const selectFieldsSlug = 'select-fields' export const selectFieldsSlug = 'select-fields'
export const selectVersionsFieldsSlug = 'select-versions-fields'
export const tabsFieldsSlug = 'tabs-fields' export const tabsFieldsSlug = 'tabs-fields'
export const tabsFields2Slug = 'tabs-fields-2' export const tabsFields2Slug = 'tabs-fields-2'
export const textFieldsSlug = 'text-fields' export const textFieldsSlug = 'text-fields'