import { InvalidConfiguration } from 'payload'; import toSnakeCase from 'to-snake-case'; import { createTableName } from '../createTableName.js'; import { buildForeignKeyName } from '../utilities/buildForeignKeyName.js'; import { buildIndexName } from '../utilities/buildIndexName.js'; import { traverseFields } from './traverseFields.js'; export const buildTable = ({ adapter, baseColumns = {}, baseForeignKeys = {}, baseIndexes = {}, blocksTableNameMap, compoundIndexes, disableNotNull, disableRelsTableUnique = false, disableUnique = false, fields, parentIsLocalized, rootRelationships, rootRelationsToBuild, rootTableIDColType, rootTableName: incomingRootTableName, rootUniqueRelationships, setColumnID, tableName, timestamps, versions, withinLocalizedArrayOrBlock })=>{ const isRoot = !incomingRootTableName; const rootTableName = incomingRootTableName || tableName; const columns = baseColumns; const indexes = baseIndexes; const localesColumns = {}; const localesIndexes = {}; let localesTable; let textsTable; let numbersTable; // Relationships to the base collection const relationships = rootRelationships || new Set(); // Unique relationships to the base collection const uniqueRelationships = rootUniqueRelationships || new Set(); let relationshipsTable; // Drizzle relations const relationsToBuild = new Map(); const idColType = setColumnID({ adapter, columns, fields }); const { hasLocalizedField, hasLocalizedManyNumberField, hasLocalizedManyTextField, hasLocalizedRelationshipField, hasManyNumberField, hasManyTextField } = traverseFields({ adapter, blocksTableNameMap, columns, disableNotNull, disableRelsTableUnique, disableUnique, fields, indexes, localesColumns, localesIndexes, newTableName: tableName, parentIsLocalized, parentTableName: tableName, relationships, relationsToBuild, rootRelationsToBuild: rootRelationsToBuild || relationsToBuild, rootTableIDColType: rootTableIDColType || idColType, rootTableName, setColumnID, uniqueRelationships, versions, withinLocalizedArrayOrBlock }); // split the relationsToBuild by localized and non-localized const localizedRelations = new Map(); const nonLocalizedRelations = new Map(); relationsToBuild.forEach(({ type, localized, relationName, target }, key)=>{ const map = localized ? localizedRelations : nonLocalizedRelations; map.set(key, { type, relationName, target }); }); if (timestamps) { columns.createdAt = { name: 'created_at', type: 'timestamp', defaultNow: true, mode: 'string', notNull: true, precision: 3, withTimezone: true }; columns.updatedAt = { name: 'updated_at', type: 'timestamp', defaultNow: true, mode: 'string', notNull: true, precision: 3, withTimezone: true }; } const table = { name: tableName, columns, foreignKeys: baseForeignKeys, indexes }; adapter.rawTables[tableName] = table; if (hasLocalizedField || localizedRelations.size) { const localeTableName = `${tableName}${adapter.localesSuffix}`; adapter.rawTables[localeTableName] = localesTable; localesColumns.id = { name: 'id', type: 'serial', primaryKey: true }; localesColumns._locale = { name: '_locale', type: 'enum', locale: true, notNull: true }; localesColumns._parentID = { name: '_parent_id', type: idColType, notNull: true }; localesIndexes._localeParent = { name: buildIndexName({ name: `${localeTableName}_locale_parent_id_unique`, adapter, appendSuffix: false }), on: [ '_locale', '_parentID' ], unique: true }; localesTable = { name: localeTableName, columns: localesColumns, foreignKeys: { _parentIdFk: { name: buildForeignKeyName({ name: `${localeTableName}_parent_id`, adapter }), columns: [ '_parentID' ], foreignColumns: [ { name: 'id', table: tableName } ], onDelete: 'cascade' } }, indexes: localesIndexes }; adapter.rawTables[localeTableName] = localesTable; const localeRelations = { _parentID: { type: 'one', fields: [ { name: '_parentID', table: localeTableName } ], references: [ 'id' ], relationName: '_locales', to: tableName } }; localizedRelations.forEach(({ type, target }, key)=>{ if (type === 'one') { localeRelations[key] = { type: 'one', fields: [ { name: key, table: localeTableName } ], references: [ 'id' ], relationName: key, to: target }; } if (type === 'many') { localeRelations[key] = { type: 'many', relationName: key, to: target }; } }); adapter.rawRelations[localeTableName] = localeRelations; } if (compoundIndexes) { for (const index of compoundIndexes){ let someLocalized = null; const columns = []; const getTableToUse = ()=>{ if (someLocalized) { return localesTable; } return table; }; for (const { path, pathHasLocalized } of index.fields){ if (someLocalized === null) { someLocalized = pathHasLocalized; } if (someLocalized !== pathHasLocalized) { throw new InvalidConfiguration(`Compound indexes within localized and non localized fields are not supported in SQL. Expected ${path} to be ${someLocalized ? 'non' : ''} localized.`); } const columnPath = path.replaceAll('.', '_'); if (!getTableToUse().columns[columnPath]) { throw new InvalidConfiguration(`Column ${columnPath} for compound index on ${path} was not found in the ${getTableToUse().name} table.`); } columns.push(columnPath); } if (someLocalized) { columns.push('_locale'); } let name = columns.join('_'); // truncate against the limit, buildIndexName will handle collisions if (name.length > 63) { name = 'compound_index'; } const indexName = buildIndexName({ name, adapter }); getTableToUse().indexes[indexName] = { name: indexName, on: columns, unique: disableUnique ? false : index.unique }; } } if (isRoot) { if (hasManyTextField) { const textsTableName = `${rootTableName}_texts`; adapter.rawTables[textsTableName] = textsTable; const columns = { id: { name: 'id', type: 'serial', primaryKey: true }, order: { name: 'order', type: 'integer', notNull: true }, parent: { name: 'parent_id', type: idColType, notNull: true }, path: { name: 'path', type: 'varchar', notNull: true }, text: { name: 'text', type: 'varchar' } }; if (hasLocalizedManyTextField) { columns.locale = { name: 'locale', type: 'enum', locale: true }; } const textsTableIndexes = { orderParentIdx: { name: buildIndexName({ name: `${textsTableName}_order_parent`, adapter, appendSuffix: false }), on: [ 'order', 'parent' ] } }; if (hasManyTextField === 'index') { textsTableIndexes.text_idx = { name: buildIndexName({ name: `${textsTableName}_text`, adapter }), on: 'text' }; } if (hasLocalizedManyTextField) { textsTableIndexes.localeParent = { name: buildIndexName({ name: `${textsTableName}_locale_parent`, adapter, appendSuffix: false }), on: [ 'locale', 'parent' ] }; } textsTable = { name: textsTableName, columns, foreignKeys: { parentFk: { name: buildForeignKeyName({ name: `${textsTableName}_parent`, adapter }), columns: [ 'parent' ], foreignColumns: [ { name: 'id', table: tableName } ], onDelete: 'cascade' } }, indexes: textsTableIndexes }; adapter.rawTables[textsTableName] = textsTable; adapter.rawRelations[textsTableName] = { parent: { type: 'one', fields: [ { name: 'parent', table: textsTableName } ], references: [ 'id' ], relationName: '_texts', to: tableName } }; } if (hasManyNumberField) { const numbersTableName = `${rootTableName}_numbers`; adapter.rawTables[numbersTableName] = numbersTable; const columns = { id: { name: 'id', type: 'serial', primaryKey: true }, number: { name: 'number', type: 'numeric' }, order: { name: 'order', type: 'integer', notNull: true }, parent: { name: 'parent_id', type: idColType, notNull: true }, path: { name: 'path', type: 'varchar', notNull: true } }; if (hasLocalizedManyNumberField) { columns.locale = { name: 'locale', type: 'enum', locale: true }; } const numbersTableIndexes = { orderParentIdx: { name: buildIndexName({ name: `${numbersTableName}_order_parent`, adapter }), on: [ 'order', 'parent' ] } }; if (hasManyNumberField === 'index') { numbersTableIndexes.numberIdx = { name: buildIndexName({ name: `${numbersTableName}_number`, adapter }), on: 'number' }; } if (hasLocalizedManyNumberField) { numbersTableIndexes.localeParent = { name: buildIndexName({ name: `${numbersTableName}_locale_parent`, adapter, appendSuffix: false }), on: [ 'locale', 'parent' ] }; } numbersTable = { name: numbersTableName, columns, foreignKeys: { parentFk: { name: buildForeignKeyName({ name: `${numbersTableName}_parent`, adapter }), columns: [ 'parent' ], foreignColumns: [ { name: 'id', table: tableName } ], onDelete: 'cascade' } }, indexes: numbersTableIndexes }; adapter.rawTables[numbersTableName] = numbersTable; adapter.rawRelations[numbersTableName] = { parent: { type: 'one', fields: [ { name: 'parent', table: numbersTableName } ], references: [ 'id' ], relationName: '_numbers', to: tableName } }; } if (relationships.size) { const relationshipColumns = { id: { name: 'id', type: 'serial', primaryKey: true }, order: { name: 'order', type: 'integer' }, parent: { name: 'parent_id', type: idColType, notNull: true }, path: { name: 'path', type: 'varchar', notNull: true } }; if (hasLocalizedRelationshipField) { relationshipColumns.locale = { name: 'locale', type: 'enum', locale: true }; } const relationshipsTableName = `${tableName}${adapter.relationshipsSuffix}`; const relationshipIndexes = { order: { name: buildIndexName({ name: `${relationshipsTableName}_order`, adapter }), on: 'order' }, parentIdx: { name: buildIndexName({ name: `${relationshipsTableName}_parent`, adapter }), on: 'parent' }, pathIdx: { name: buildIndexName({ name: `${relationshipsTableName}_path`, adapter }), on: 'path' } }; if (hasLocalizedRelationshipField) { relationshipIndexes.localeIdx = { name: buildIndexName({ name: `${relationshipsTableName}_locale`, adapter }), on: 'locale' }; } const relationshipForeignKeys = { parentFk: { name: buildForeignKeyName({ name: `${relationshipsTableName}_parent`, adapter }), columns: [ 'parent' ], foreignColumns: [ { name: 'id', table: tableName } ], onDelete: 'cascade' } }; relationships.forEach((relationTo)=>{ const relationshipConfig = adapter.payload.collections[relationTo].config; const formattedRelationTo = createTableName({ adapter, config: relationshipConfig, throwValidationError: true }); let colType = adapter.idType === 'uuid' ? 'uuid' : 'integer'; const relatedCollectionCustomIDType = adapter.payload.collections[relationshipConfig.slug]?.customIDType; if (relatedCollectionCustomIDType === 'number') { colType = 'numeric'; } if (relatedCollectionCustomIDType === 'text') { colType = 'varchar'; } const colName = `${relationTo}ID`; relationshipColumns[colName] = { name: `${formattedRelationTo}_id`, type: colType }; relationshipForeignKeys[`${relationTo}IdFk`] = { name: buildForeignKeyName({ name: `${relationshipsTableName}_${toSnakeCase(relationTo)}`, adapter }), columns: [ colName ], foreignColumns: [ { name: 'id', table: formattedRelationTo } ], onDelete: 'cascade' }; const indexColumns = [ colName ]; const unique = !disableUnique && uniqueRelationships.has(relationTo); if (unique) { indexColumns.push('path'); } if (hasLocalizedRelationshipField) { indexColumns.push('locale'); } const indexName = buildIndexName({ name: `${relationshipsTableName}_${formattedRelationTo}_id`, adapter }); relationshipIndexes[indexName] = { name: indexName, on: indexColumns, unique }; }); relationshipsTable = { name: relationshipsTableName, columns: relationshipColumns, foreignKeys: relationshipForeignKeys, indexes: relationshipIndexes }; adapter.rawTables[relationshipsTableName] = relationshipsTable; const relationshipsTableRelations = { parent: { type: 'one', fields: [ { name: 'parent', table: relationshipsTableName } ], references: [ 'id' ], relationName: '_rels', to: tableName } }; relationships.forEach((relationTo)=>{ const relatedTableName = createTableName({ adapter, config: adapter.payload.collections[relationTo].config, throwValidationError: true }); const idColumnName = `${relationTo}ID`; relationshipsTableRelations[idColumnName] = { type: 'one', fields: [ { name: idColumnName, table: relationshipsTableName } ], references: [ 'id' ], relationName: relationTo, to: relatedTableName }; }); adapter.rawRelations[relationshipsTableName] = relationshipsTableRelations; } } const tableRelations = {}; nonLocalizedRelations.forEach(({ type, relationName, target }, key)=>{ if (type === 'one') { tableRelations[key] = { type: 'one', fields: [ { name: key, table: tableName } ], references: [ 'id' ], relationName: key, to: target }; } if (type === 'many') { tableRelations[key] = { type: 'many', relationName: relationName || key, to: target }; } }); if (hasLocalizedField) { tableRelations._locales = { type: 'many', relationName: '_locales', to: localesTable.name }; } if (isRoot && textsTable) { tableRelations._texts = { type: 'many', relationName: '_texts', to: textsTable.name }; } if (isRoot && numbersTable) { tableRelations._numbers = { type: 'many', relationName: '_numbers', to: numbersTable.name }; } if (relationships.size && relationshipsTable) { tableRelations._rels = { type: 'many', relationName: '_rels', to: relationshipsTable.name }; } adapter.rawRelations[tableName] = tableRelations; return { hasLocalizedManyNumberField, hasLocalizedManyTextField, hasLocalizedRelationshipField, hasManyNumberField, hasManyTextField, relationsToBuild }; }; //# sourceMappingURL=build.js.map