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:
@@ -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 = ''
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
47
test/fields/collections/SelectVersions/index.ts
Normal file
47
test/fields/collections/SelectVersions/index.ts
Normal 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
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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', () => {
|
||||||
|
|||||||
@@ -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".
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
Reference in New Issue
Block a user