import { InvalidConfiguration } from 'payload'; import { fieldAffectsData, fieldIsVirtual, fieldShouldBeLocalized, optionIsObject } from 'payload/shared'; import toSnakeCase from 'to-snake-case'; import { createTableName } from '../createTableName.js'; import { buildIndexName } from '../utilities/buildIndexName.js'; import { getArrayRelationName } from '../utilities/getArrayRelationName.js'; import { hasLocalesTable } from '../utilities/hasLocalesTable.js'; import { InternalBlockTableNameIndex, setInternalBlockIndex, validateExistingBlockIsIdentical } from '../utilities/validateExistingBlockIsIdentical.js'; import { buildTable } from './build.js'; import { idToUUID } from './idToUUID.js'; import { withDefault } from './withDefault.js'; export const traverseFields = ({ adapter, blocksTableNameMap, columnPrefix, columns, disableNotNull, disableRelsTableUnique, disableUnique = false, fieldPrefix, fields, forceLocalized, indexes, localesColumns, localesIndexes, newTableName, parentIsLocalized, parentTableName, relationships, relationsToBuild, rootRelationsToBuild, rootTableIDColType, rootTableName, setColumnID, uniqueRelationships, versions, withinLocalizedArrayOrBlock })=>{ const throwValidationError = true; let hasLocalizedField = false; let hasLocalizedRelationshipField = false; let hasManyTextField = false; let hasLocalizedManyTextField = false; let hasManyNumberField = false; let hasLocalizedManyNumberField = false; let parentIDColType = 'integer'; const idColumn = columns.id; if (idColumn && [ 'numeric', 'text', 'uuid', 'varchar' ].includes(idColumn.type)) { parentIDColType = idColumn.type; } fields.forEach((field)=>{ if ('name' in field && field.name === 'id') { return; } if (fieldIsVirtual(field)) { return; } let targetTable = columns; let targetIndexes = indexes; const columnName = `${columnPrefix || ''}${field.name[0] === '_' ? '_' : ''}${toSnakeCase(field.name)}`; const fieldName = `${fieldPrefix?.replace('.', '_') || ''}${field.name}`; const isFieldLocalized = fieldShouldBeLocalized({ field, parentIsLocalized }); // If field is localized, // add the column to the locale table instead of main table if (adapter.payload.config.localization && (isFieldLocalized || forceLocalized) && field.type !== 'array' && (field.type !== 'blocks' || adapter.blocksAsJSON) && ('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 }); targetIndexes[indexName] = { name: indexName, on: isFieldLocalized ? [ fieldName, '_locale' ] : fieldName, unique }; } switch(field.type){ case 'array': { const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull; const arrayTableName = createTableName({ adapter, config: field, parentTableName: newTableName, prefix: `${newTableName}_`, throwValidationError, versionsCustomName: versions }); const baseColumns = { _order: { name: '_order', type: 'integer', notNull: true }, _parentID: { name: '_parent_id', type: parentIDColType, notNull: true } }; const baseIndexes = { _orderIdx: { name: `${arrayTableName}_order_idx`, on: [ '_order' ] }, _parentIDIdx: { name: `${arrayTableName}_parent_id_idx`, on: '_parentID' } }; const baseForeignKeys = { _parentIDFk: { name: `${arrayTableName}_parent_id_fk`, columns: [ '_parentID' ], foreignColumns: [ { name: 'id', table: parentTableName } ], onDelete: 'cascade' } }; const isLocalized = Boolean(isFieldLocalized && adapter.payload.config.localization) || withinLocalizedArrayOrBlock || forceLocalized; if (isLocalized) { baseColumns._locale = { name: '_locale', type: 'enum', locale: true, notNull: true }; baseIndexes._localeIdx = { name: `${arrayTableName}_locale_idx`, on: '_locale' }; } const { hasLocalizedManyNumberField: subHasLocalizedManyNumberField, hasLocalizedManyTextField: subHasLocalizedManyTextField, hasLocalizedRelationshipField: subHasLocalizedRelationshipField, hasManyNumberField: subHasManyNumberField, hasManyTextField: subHasManyTextField, relationsToBuild: subRelationsToBuild } = buildTable({ adapter, baseColumns, baseForeignKeys, baseIndexes, blocksTableNameMap, disableNotNull: disableNotNullFromHere, disableRelsTableUnique: true, disableUnique, fields: disableUnique ? idToUUID(field.flattenedFields) : field.flattenedFields, parentIsLocalized: parentIsLocalized || field.localized, rootRelationships: relationships, rootRelationsToBuild, rootTableIDColType, rootTableName, rootUniqueRelationships: uniqueRelationships, setColumnID, tableName: arrayTableName, 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 relationName = getArrayRelationName({ field, path: fieldName, tableName: arrayTableName }); relationsToBuild.set(relationName, { type: 'many', // arrays have their own localized table, independent of the base table. localized: false, target: arrayTableName }); const arrayRelations = { _parentID: { type: 'one', fields: [ { name: '_parentID', table: arrayTableName } ], references: [ 'id' ], relationName, to: parentTableName } }; if (hasLocalesTable({ fields: field.fields, parentIsLocalized: parentIsLocalized || field.localized })) { arrayRelations._locales = { type: 'many', relationName: '_locales', to: `${arrayTableName}${adapter.localesSuffix}` }; } subRelationsToBuild.forEach(({ type, localized, target }, key)=>{ if (type === 'one') { const arrayWithLocalized = localized ? `${arrayTableName}${adapter.localesSuffix}` : arrayTableName; arrayRelations[key] = { type: 'one', fields: [ { name: key, table: arrayWithLocalized } ], references: [ 'id' ], relationName: key, to: target }; } if (type === 'many') { arrayRelations[key] = { type: 'many', relationName: key, to: target }; } }); adapter.rawRelations[arrayTableName] = arrayRelations; break; } case 'blocks': { if (adapter.blocksAsJSON) { targetTable[fieldName] = withDefault({ name: columnName, type: 'jsonb' }, field); break; } const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull; (field.blockReferences ?? field.blocks).forEach((_block)=>{ const block = typeof _block === 'string' ? adapter.payload.blocks[_block] : _block; let blockTableName = createTableName({ adapter, config: block, parentTableName: rootTableName, prefix: `${rootTableName}_blocks_`, throwValidationError, versionsCustomName: versions }); if (typeof blocksTableNameMap[blockTableName] === 'undefined') { blocksTableNameMap[blockTableName] = 1; } else if (!adapter.rawTables[blockTableName] || !validateExistingBlockIsIdentical({ block, localized: field.localized, rootTableName, table: adapter.rawTables[blockTableName], tableLocales: adapter.rawTables[`${blockTableName}${adapter.localesSuffix}`] })) { blocksTableNameMap[blockTableName]++; setInternalBlockIndex(block, blocksTableNameMap[blockTableName]); blockTableName = `${blockTableName}_${blocksTableNameMap[blockTableName]}`; } let relationName = `_blocks_${block.slug}`; if (typeof block[InternalBlockTableNameIndex] !== 'undefined') { relationName = `_blocks_${block.slug}_${block[InternalBlockTableNameIndex]}`; } if (!adapter.rawTables[blockTableName]) { const baseColumns = { _order: { name: '_order', type: 'integer', notNull: true }, _parentID: { name: '_parent_id', type: rootTableIDColType, notNull: true }, _path: { name: '_path', type: 'text', notNull: true } }; const baseIndexes = { _orderIdx: { name: `${blockTableName}_order_idx`, on: '_order' }, _parentIDIdx: { name: `${blockTableName}_parent_id_idx`, on: [ '_parentID' ] }, _pathIdx: { name: `${blockTableName}_path_idx`, on: '_path' } }; const baseForeignKeys = { _parentIdFk: { name: `${blockTableName}_parent_id_fk`, columns: [ '_parentID' ], foreignColumns: [ { name: 'id', table: rootTableName } ], onDelete: 'cascade' } }; const isLocalized = Boolean(isFieldLocalized && 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, blocksTableNameMap, disableNotNull: disableNotNullFromHere, disableRelsTableUnique: true, disableUnique, fields: disableUnique ? idToUUID(block.flattenedFields) : block.flattenedFields, parentIsLocalized: parentIsLocalized || field.localized, 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 = { _parentID: { type: 'one', fields: [ { name: '_parentID', table: blockTableName } ], references: [ 'id' ], relationName, to: rootTableName } }; if (hasLocalesTable({ fields: block.fields, parentIsLocalized: parentIsLocalized || field.localized })) { 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(relationName, { type: 'many', // blocks are not localized on the parent table localized: false, target: blockTableName }); }); break; } case 'checkbox': { targetTable[fieldName] = withDefault({ name: columnName, type: 'boolean' }, field); break; } case 'code': case 'email': case 'textarea': { targetTable[fieldName] = withDefault({ name: columnName, type: 'varchar' }, field); break; } case 'date': { targetTable[fieldName] = withDefault({ name: columnName, type: 'timestamp', mode: 'string', precision: 3, withTimezone: true }, field); break; } case 'group': case 'tab': { const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull; const { hasLocalizedField: groupHasLocalizedField, hasLocalizedManyNumberField: groupHasLocalizedManyNumberField, hasLocalizedManyTextField: groupHasLocalizedManyTextField, hasLocalizedRelationshipField: groupHasLocalizedRelationshipField, hasManyNumberField: groupHasManyNumberField, hasManyTextField: groupHasManyTextField } = traverseFields({ adapter, blocksTableNameMap, columnPrefix: `${columnName}_`, columns, disableNotNull: disableNotNullFromHere, disableUnique, fieldPrefix: `${fieldName}.`, fields: field.flattenedFields, forceLocalized: isFieldLocalized, indexes, localesColumns, localesIndexes, newTableName: `${parentTableName}_${columnName}`, parentIsLocalized: parentIsLocalized || field.localized, parentTableName, relationships, relationsToBuild, rootRelationsToBuild, rootTableIDColType, rootTableName, setColumnID, uniqueRelationships, versions, withinLocalizedArrayOrBlock: withinLocalizedArrayOrBlock || isFieldLocalized }); 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; } case 'json': case 'richText': { targetTable[fieldName] = withDefault({ name: columnName, type: 'jsonb' }, field); break; } case 'number': { if (field.hasMany) { const isLocalized = Boolean(isFieldLocalized && adapter.payload.config.localization) || withinLocalizedArrayOrBlock || forceLocalized; if (isLocalized) { hasLocalizedManyNumberField = true; } if (field.index) { hasManyNumberField = 'index'; } else if (!hasManyNumberField) { hasManyNumberField = true; } if (field.unique) { throw new InvalidConfiguration('Unique is not supported in Postgres for hasMany number fields.'); } } else { targetTable[fieldName] = withDefault({ name: columnName, type: 'numeric' }, field); } break; } case 'point': { targetTable[fieldName] = withDefault({ name: columnName, type: 'geometry' }, field); break; } case 'radio': case 'select': { const enumName = createTableName({ adapter, config: field, parentTableName: newTableName, prefix: `enum_${newTableName}_`, target: 'enumName', throwValidationError }); const options = field.options.map((option)=>{ if (optionIsObject(option)) { return option.value; } return option; }); if (field.type === 'select' && field.hasMany) { const selectTableName = createTableName({ adapter, config: field, parentTableName: newTableName, prefix: `${newTableName}_`, throwValidationError, versionsCustomName: versions }); const baseColumns = { order: { name: 'order', type: 'integer', notNull: true }, parent: { name: 'parent_id', type: parentIDColType, notNull: true }, value: { name: 'value', type: 'enum', enumName: createTableName({ adapter, config: field, parentTableName: newTableName, prefix: `enum_${newTableName}_`, target: 'enumName', throwValidationError }), options } }; const baseIndexes = { orderIdx: { name: `${selectTableName}_order_idx`, on: 'order' }, parentIdx: { name: `${selectTableName}_parent_idx`, on: 'parent' } }; const baseForeignKeys = { parentFk: { name: `${selectTableName}_parent_fk`, columns: [ 'parent' ], foreignColumns: [ { name: 'id', table: parentTableName } ], onDelete: 'cascade' } }; const isLocalized = Boolean(isFieldLocalized && adapter.payload.config.localization) || withinLocalizedArrayOrBlock || forceLocalized; if (isLocalized) { baseColumns.locale = { name: 'locale', type: 'enum', locale: true, notNull: true }; baseIndexes.localeIdx = { name: `${selectTableName}_locale_idx`, on: 'locale' }; } if (field.index) { baseIndexes.value = { name: `${selectTableName}_value_idx`, on: 'value' }; } buildTable({ adapter, baseColumns, baseForeignKeys, baseIndexes, blocksTableNameMap, disableNotNull, disableUnique, fields: [], parentIsLocalized: parentIsLocalized || field.localized, rootTableName, setColumnID, tableName: selectTableName, versions }); relationsToBuild.set(fieldName, { type: 'many', // selects have their own localized table, independent of the base table. localized: false, target: selectTableName }); adapter.rawRelations[selectTableName] = { parent: { type: 'one', fields: [ { name: 'parent', table: selectTableName } ], references: [ 'id' ], relationName: fieldName, to: parentTableName } }; } else { targetTable[fieldName] = withDefault({ name: columnName, type: 'enum', enumName, options }, field); } break; } case 'relationship': case 'upload': if (Array.isArray(field.relationTo)) { field.relationTo.forEach((relation)=>{ relationships.add(relation); if (field.unique && !disableUnique && !disableRelsTableUnique) { uniqueRelationships.add(relation); } }); } else if (field.hasMany) { relationships.add(field.relationTo); if (field.unique && !disableUnique && !disableRelsTableUnique) { uniqueRelationships.add(field.relationTo); } } else { // simple relationships get a column on the targetTable with a foreign key to the relationTo table const relationshipConfig = adapter.payload.collections[field.relationTo].config; const tableName = adapter.tableNameMap.get(toSnakeCase(field.relationTo)); // get the id type of the related collection let colType = adapter.idType === 'uuid' ? 'uuid' : 'integer'; const relatedCollectionCustomID = relationshipConfig.fields.find((field)=>fieldAffectsData(field) && field.name === 'id'); if (relatedCollectionCustomID?.type === 'number') { colType = 'numeric'; } if (relatedCollectionCustomID?.type === 'text') { colType = 'varchar'; } // make the foreign key column for relationship using the correct id column type targetTable[fieldName] = { name: `${columnName}_id`, type: colType, reference: { name: 'id', onDelete: 'set null', table: tableName } }; // add relationship to table relationsToBuild.set(fieldName, { type: 'one', localized: adapter.payload.config.localization && (isFieldLocalized || forceLocalized), target: tableName }); // add notNull when not required if (!disableNotNull && field.required && !field.admin?.condition) { targetTable[fieldName].notNull = true; } break; } if (Boolean(isFieldLocalized && adapter.payload.config.localization) || withinLocalizedArrayOrBlock) { hasLocalizedRelationshipField = true; } break; case 'text': { if (field.hasMany) { const isLocalized = Boolean(isFieldLocalized && adapter.payload.config.localization) || withinLocalizedArrayOrBlock || forceLocalized; if (isLocalized) { hasLocalizedManyTextField = true; } if (field.index) { hasManyTextField = 'index'; } else if (!hasManyTextField) { hasManyTextField = true; } if (field.unique) { throw new InvalidConfiguration('Unique is not supported in Postgres for hasMany text fields.'); } } else { targetTable[fieldName] = withDefault({ name: columnName, type: 'varchar' }, field); } break; } default: break; } const condition = field.admin && field.admin.condition; if (!disableNotNull && targetTable[fieldName] && 'required' in field && field.required && !condition) { targetTable[fieldName].notNull = true; } }); return { hasLocalizedField, hasLocalizedManyNumberField, hasLocalizedManyTextField, hasLocalizedRelationshipField, hasManyNumberField, hasManyTextField }; }; //# sourceMappingURL=traverseFields.js.map