Compare commits

...

1 Commits

Author SHA1 Message Date
Sasha
85b7a4d7be postgres identical blocks 2025-05-15 13:52:49 +03:00
7 changed files with 308 additions and 176 deletions

View File

@@ -31,6 +31,7 @@ type Args = {
* ie. indexes, multiple columns, etc
*/
baseIndexes?: Record<string, RawIndex>
blockTableNamesMap: Record<string, number>
buildNumbers?: boolean
buildRelationships?: boolean
disableNotNull: boolean
@@ -67,6 +68,7 @@ export const buildTable = ({
baseColumns = {},
baseForeignKeys = {},
baseIndexes = {},
blockTableNamesMap,
disableNotNull,
disableRelsTableUnique = false,
disableUnique = false,
@@ -115,6 +117,7 @@ export const buildTable = ({
hasManyTextField,
} = traverseFields({
adapter,
blockTableNamesMap,
columns,
disableNotNull,
disableRelsTableUnique,

View File

@@ -52,6 +52,7 @@ export const buildRawSchema = ({
buildTable({
adapter,
blockTableNamesMap: {},
disableNotNull: !!collection?.versions?.drafts,
disableUnique: false,
fields: collection.flattenedFields,
@@ -69,6 +70,7 @@ export const buildRawSchema = ({
buildTable({
adapter,
blockTableNamesMap: {},
disableNotNull: !!collection.versions?.drafts,
disableUnique: true,
fields: versionFields,
@@ -88,6 +90,7 @@ export const buildRawSchema = ({
buildTable({
adapter,
blockTableNamesMap: {},
disableNotNull: !!global?.versions?.drafts,
disableUnique: false,
fields: global.flattenedFields,
@@ -109,6 +112,7 @@ export const buildRawSchema = ({
buildTable({
adapter,
blockTableNamesMap: {},
disableNotNull: !!global.versions?.drafts,
disableUnique: true,
fields: versionFields,

View File

@@ -18,13 +18,17 @@ import type {
import { createTableName } from '../createTableName.js'
import { buildIndexName } from '../utilities/buildIndexName.js'
import { hasLocalesTable } from '../utilities/hasLocalesTable.js'
import { validateExistingBlockIsIdentical } from '../utilities/validateExistingBlockIsIdentical.js'
import {
setInternalBlockIndex,
validateExistingBlockIsIdentical,
} from '../utilities/validateExistingBlockIsIdentical.js'
import { buildTable } from './build.js'
import { idToUUID } from './idToUUID.js'
import { withDefault } from './withDefault.js'
type Args = {
adapter: DrizzleAdapter
blockTableNamesMap: Record<string, number>
columnPrefix?: string
columns: Record<string, RawColumn>
disableNotNull: boolean
@@ -64,6 +68,7 @@ type Result = {
export const traverseFields = ({
adapter,
blockTableNamesMap,
columnPrefix,
columns,
disableNotNull,
@@ -239,6 +244,7 @@ export const traverseFields = ({
baseColumns,
baseForeignKeys,
baseIndexes,
blockTableNamesMap,
disableNotNull: disableNotNullFromHere,
disableRelsTableUnique: true,
disableUnique,
@@ -344,7 +350,7 @@ export const traverseFields = ({
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
field.blocks.forEach((block) => {
const blockTableName = createTableName({
let blockTableName = createTableName({
adapter,
config: block,
parentTableName: rootTableName,
@@ -352,186 +358,195 @@ export const traverseFields = ({
throwValidationError,
versionsCustomName: versions,
})
if (!adapter.rawTables[blockTableName]) {
const baseColumns: Record<string, RawColumn> = {
_order: {
name: '_order',
type: 'integer',
notNull: true,
},
_parentID: {
name: '_parent_id',
type: rootTableIDColType,
notNull: true,
},
_path: {
name: '_path',
type: 'text',
notNull: true,
},
}
const baseIndexes: Record<string, RawIndex> = {
_orderIdx: {
name: `${blockTableName}_order_idx`,
on: '_order',
},
_parentIDIdx: {
name: `${blockTableName}_parent_id_idx`,
on: ['_parentID'],
},
_pathIdx: {
name: `${blockTableName}_path_idx`,
on: '_path',
},
}
const baseForeignKeys: Record<string, RawForeignKey> = {
_parentIdFk: {
name: `${blockTableName}_parent_id_fk`,
columns: ['_parentID'],
foreignColumns: [
{
name: 'id',
table: rootTableName,
},
],
onDelete: 'cascade',
},
}
const isLocalized =
Boolean(field.localized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock ||
forceLocalized
if (isLocalized) {
baseColumns._locale = {
name: '_locale',
type: 'enum',
locale: true,
notNull: true,
}
baseIndexes._localeIdx = {
name: `${blockTableName}_locale_idx`,
on: '_locale',
}
}
const {
hasLocalizedManyNumberField: subHasLocalizedManyNumberField,
hasLocalizedManyTextField: subHasLocalizedManyTextField,
hasLocalizedRelationshipField: subHasLocalizedRelationshipField,
hasManyNumberField: subHasManyNumberField,
hasManyTextField: subHasManyTextField,
relationsToBuild: subRelationsToBuild,
} = buildTable({
adapter,
baseColumns,
baseForeignKeys,
baseIndexes,
disableNotNull: disableNotNullFromHere,
disableRelsTableUnique: true,
disableUnique,
fields: disableUnique ? idToUUID(block.flattenedFields) : block.flattenedFields,
rootRelationships: relationships,
rootRelationsToBuild,
rootTableIDColType,
rootTableName,
rootUniqueRelationships: uniqueRelationships,
setColumnID,
tableName: blockTableName,
versions,
withinLocalizedArrayOrBlock: isLocalized,
})
if (subHasLocalizedManyNumberField) {
hasLocalizedManyNumberField = subHasLocalizedManyNumberField
}
if (subHasLocalizedRelationshipField) {
hasLocalizedRelationshipField = subHasLocalizedRelationshipField
}
if (subHasLocalizedManyTextField) {
hasLocalizedManyTextField = subHasLocalizedManyTextField
}
if (subHasManyTextField) {
if (!hasManyTextField || subHasManyTextField === 'index') {
hasManyTextField = subHasManyTextField
}
}
if (subHasManyNumberField) {
if (!hasManyNumberField || subHasManyNumberField === 'index') {
hasManyNumberField = subHasManyNumberField
}
}
const blockRelations: Record<string, RawRelation> = {
_parentID: {
type: 'one',
fields: [
{
name: '_parentID',
table: blockTableName,
},
],
references: ['id'],
relationName: `_blocks_${block.slug}`,
to: rootTableName,
},
}
if (hasLocalesTable(block.fields)) {
blockRelations._locales = {
type: 'many',
relationName: '_locales',
to: `${blockTableName}${adapter.localesSuffix}`,
}
}
subRelationsToBuild.forEach(({ type, localized, target }, key) => {
if (type === 'one') {
const blockWithLocalized = localized
? `${blockTableName}${adapter.localesSuffix}`
: blockTableName
blockRelations[key] = {
type: 'one',
fields: [
{
name: key,
table: blockWithLocalized,
},
],
references: ['id'],
relationName: key,
to: target,
}
}
if (type === 'many') {
blockRelations[key] = {
type: 'many',
relationName: key,
to: target,
}
}
})
adapter.rawRelations[blockTableName] = blockRelations
} else if (process.env.NODE_ENV !== 'production' && !versions) {
validateExistingBlockIsIdentical({
if (typeof blockTableNamesMap[blockTableName] === 'undefined') {
blockTableNamesMap[blockTableName] = 1
} else if (
!validateExistingBlockIsIdentical({
block,
localized: field.localized,
rootTableName,
table: adapter.rawTables[blockTableName],
tableLocales: adapter.rawTables[`${blockTableName}${adapter.localesSuffix}`],
})
) {
blockTableNamesMap[blockTableName]++
blockTableName = `${blockTableName}_${blockTableNamesMap[blockTableName]}`
setInternalBlockIndex(block, blockTableNamesMap[blockTableName])
}
const baseColumns: Record<string, RawColumn> = {
_order: {
name: '_order',
type: 'integer',
notNull: true,
},
_parentID: {
name: '_parent_id',
type: rootTableIDColType,
notNull: true,
},
_path: {
name: '_path',
type: 'text',
notNull: true,
},
}
const baseIndexes: Record<string, RawIndex> = {
_orderIdx: {
name: `${blockTableName}_order_idx`,
on: '_order',
},
_parentIDIdx: {
name: `${blockTableName}_parent_id_idx`,
on: ['_parentID'],
},
_pathIdx: {
name: `${blockTableName}_path_idx`,
on: '_path',
},
}
const baseForeignKeys: Record<string, RawForeignKey> = {
_parentIdFk: {
name: `${blockTableName}_parent_id_fk`,
columns: ['_parentID'],
foreignColumns: [
{
name: 'id',
table: rootTableName,
},
],
onDelete: 'cascade',
},
}
const isLocalized =
Boolean(field.localized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock ||
forceLocalized
if (isLocalized) {
baseColumns._locale = {
name: '_locale',
type: 'enum',
locale: true,
notNull: true,
}
baseIndexes._localeIdx = {
name: `${blockTableName}_locale_idx`,
on: '_locale',
}
}
const {
hasLocalizedManyNumberField: subHasLocalizedManyNumberField,
hasLocalizedManyTextField: subHasLocalizedManyTextField,
hasLocalizedRelationshipField: subHasLocalizedRelationshipField,
hasManyNumberField: subHasManyNumberField,
hasManyTextField: subHasManyTextField,
relationsToBuild: subRelationsToBuild,
} = buildTable({
adapter,
baseColumns,
baseForeignKeys,
baseIndexes,
blockTableNamesMap,
disableNotNull: disableNotNullFromHere,
disableRelsTableUnique: true,
disableUnique,
fields: disableUnique ? idToUUID(block.flattenedFields) : block.flattenedFields,
rootRelationships: relationships,
rootRelationsToBuild,
rootTableIDColType,
rootTableName,
rootUniqueRelationships: uniqueRelationships,
setColumnID,
tableName: blockTableName,
versions,
withinLocalizedArrayOrBlock: isLocalized,
})
if (subHasLocalizedManyNumberField) {
hasLocalizedManyNumberField = subHasLocalizedManyNumberField
}
if (subHasLocalizedRelationshipField) {
hasLocalizedRelationshipField = subHasLocalizedRelationshipField
}
if (subHasLocalizedManyTextField) {
hasLocalizedManyTextField = subHasLocalizedManyTextField
}
if (subHasManyTextField) {
if (!hasManyTextField || subHasManyTextField === 'index') {
hasManyTextField = subHasManyTextField
}
}
if (subHasManyNumberField) {
if (!hasManyNumberField || subHasManyNumberField === 'index') {
hasManyNumberField = subHasManyNumberField
}
}
const blockRelations: Record<string, RawRelation> = {
_parentID: {
type: 'one',
fields: [
{
name: '_parentID',
table: blockTableName,
},
],
references: ['id'],
relationName: `_blocks_${block.slug}`,
to: rootTableName,
},
}
if (hasLocalesTable(block.fields)) {
blockRelations._locales = {
type: 'many',
relationName: '_locales',
to: `${blockTableName}${adapter.localesSuffix}`,
}
}
subRelationsToBuild.forEach(({ type, localized, target }, key) => {
if (type === 'one') {
const blockWithLocalized = localized
? `${blockTableName}${adapter.localesSuffix}`
: blockTableName
blockRelations[key] = {
type: 'one',
fields: [
{
name: key,
table: blockWithLocalized,
},
],
references: ['id'],
relationName: key,
to: target,
}
}
if (type === 'many') {
blockRelations[key] = {
type: 'many',
relationName: key,
to: target,
}
}
})
adapter.rawRelations[blockTableName] = blockRelations
// blocks relationships are defined from the collection or globals table down to the block, bypassing any subBlocks
rootRelationsToBuild.set(`_blocks_${block.slug}`, {
type: 'many',
@@ -597,6 +612,7 @@ export const traverseFields = ({
hasManyTextField: groupHasManyTextField,
} = traverseFields({
adapter,
blockTableNamesMap,
columnPrefix: `${columnName}_`,
columns,
disableNotNull: disableNotNullFromHere,
@@ -812,6 +828,7 @@ export const traverseFields = ({
baseColumns,
baseForeignKeys,
baseIndexes,
blockTableNamesMap,
disableNotNull,
disableUnique,
fields: [],

View File

@@ -1,4 +1,4 @@
import type { Block, Field } from 'payload'
import type { Block, Field, FlattenedBlock } from 'payload'
import { InvalidConfiguration } from 'payload'
import { fieldAffectsData, fieldHasSubFields, tabHasName } from 'payload/shared'
@@ -67,7 +67,7 @@ export const validateExistingBlockIsIdentical = ({
rootTableName,
table,
tableLocales,
}: Args): void => {
}: Args): boolean => {
const fieldNames = getFlattenedFieldNames(block.fields)
const missingField =
@@ -84,6 +84,7 @@ export const validateExistingBlockIsIdentical = ({
})
if (missingField) {
return false
throw new InvalidConfiguration(
`The table ${rootTableName} has multiple blocks with slug ${
block.slug
@@ -94,8 +95,14 @@ export const validateExistingBlockIsIdentical = ({
}
if (Boolean(localized) !== Boolean(table.columns._locale)) {
return false
throw new InvalidConfiguration(
`The table ${rootTableName} has multiple blocks with slug ${block.slug}, but the schemas do not match. One is localized, but another is not. Block schemas of the same name must match exactly.`,
)
}
}
const InternalBlockTableNameIndex = Symbol('InternalBlockTableNameIndex')
export const setInternalBlockIndex = (block: FlattenedBlock, index: number) => {
block[InternalBlockTableNameIndex] = index
}

View File

@@ -550,6 +550,41 @@ export default buildConfigWithDefaults({
],
versions: true,
},
{
slug: 'blocks-test',
endpoints: [
{
path: '/:id',
handler: () => Response.json({ a: 1 }),
method: 'get',
},
],
fields: [
{
name: 'blocks',
type: 'blocks',
blocks: [
{
slug: 'normal',
fields: [{ name: 'text', type: 'text' }],
},
],
},
{
name: 'blocksLocalized',
type: 'blocks',
localized: true,
blocks: [
{
dbName: (collectionTableName) =>
`${collectionTableName.tableName!}_blocks_localized_normal`,
slug: 'normal',
fields: [{ name: 'text', type: 'text' }],
},
],
},
],
},
],
globals: [
{

View File

@@ -1466,4 +1466,9 @@ describe('database', () => {
expect(query2.totalDocs).toEqual(1)
expect(query3.totalDocs).toEqual(1)
})
it('can have localized and non localized blocks', async () => {
await payload.create({ collection: 'blocks-test', data: {} })
expect(true).toBeTruthy()
})
})

View File

@@ -23,6 +23,7 @@ export interface Config {
'custom-ids': CustomId;
'fake-custom-ids': FakeCustomId;
'relationships-migration': RelationshipsMigration;
'blocks-test': BlocksTest;
users: User;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference;
@@ -42,6 +43,7 @@ export interface Config {
'custom-ids': CustomIdsSelect<false> | CustomIdsSelect<true>;
'fake-custom-ids': FakeCustomIdsSelect<false> | FakeCustomIdsSelect<true>;
'relationships-migration': RelationshipsMigrationSelect<false> | RelationshipsMigrationSelect<true>;
'blocks-test': BlocksTestSelect<false> | BlocksTestSelect<true>;
users: UsersSelect<false> | UsersSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
@@ -345,6 +347,31 @@ export interface RelationshipsMigration {
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "blocks-test".
*/
export interface BlocksTest {
id: string;
blocks?:
| {
text?: string | null;
id?: string | null;
blockName?: string | null;
blockType: 'normal';
}[]
| null;
blocksLocalized?:
| {
text?: string | null;
id?: string | null;
blockName?: string | null;
blockType: 'normal';
}[]
| null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users".
@@ -417,6 +444,10 @@ export interface PayloadLockedDocument {
relationTo: 'relationships-migration';
value: string | RelationshipsMigration;
} | null)
| ({
relationTo: 'blocks-test';
value: string | BlocksTest;
} | null)
| ({
relationTo: 'users';
value: string | User;
@@ -700,6 +731,36 @@ export interface RelationshipsMigrationSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "blocks-test_select".
*/
export interface BlocksTestSelect<T extends boolean = true> {
blocks?:
| T
| {
normal?:
| T
| {
text?: T;
id?: T;
blockName?: T;
};
};
blocksLocalized?:
| T
| {
normal?:
| T
| {
text?: T;
id?: T;
blockName?: T;
};
};
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users_select".