From 727fba7b1c20477ede81b3840ae58a08f551f738 Mon Sep 17 00:00:00 2001 From: Sasha <64744993+r1tsuu@users.noreply.github.com> Date: Mon, 16 Dec 2024 17:17:18 +0200 Subject: [PATCH] refactor: deduplicate and abstract SQL schema building (#9987) ### What? Abstracts SQL schema building, significantly reducing code duplication for SQLite / Postgres db-sqlite lines count From: ```sh wc -l **/*.ts 62 src/connect.ts 32 src/countDistinct.ts 9 src/createJSONQuery/convertPathToJSONTraversal.ts 86 src/createJSONQuery/index.ts 15 src/defaultSnapshot.ts 6 src/deleteWhere.ts 21 src/dropDatabase.ts 15 src/execute.ts 178 src/index.ts 139 src/init.ts 19 src/insert.ts 19 src/requireDrizzleKit.ts 544 src/schema/build.ts 27 src/schema/createIndex.ts 38 src/schema/getIDColumn.ts 13 src/schema/idToUUID.ts 28 src/schema/setColumnID.ts 787 src/schema/traverseFields.ts 18 src/schema/withDefault.ts 248 src/types.ts 2304 total ``` To: ```sh wc -l **/*.ts 62 src/connect.ts 32 src/countDistinct.ts 9 src/createJSONQuery/convertPathToJSONTraversal.ts 86 src/createJSONQuery/index.ts 15 src/defaultSnapshot.ts 6 src/deleteWhere.ts 21 src/dropDatabase.ts 15 src/execute.ts 180 src/index.ts 39 src/init.ts 19 src/insert.ts 19 src/requireDrizzleKit.ts 149 src/schema/buildDrizzleTable.ts 32 src/schema/setColumnID.ts 258 src/types.ts 942 total ``` Builds abstract schema in shared drizzle package that later gets converted by SQLite/ Postgres specific implementation into drizzle schema. This, apparently can also help here https://github.com/payloadcms/payload/pull/9953. It should be very trivial to implement a MySQL adapter with this as well. --- packages/db-postgres/src/index.ts | 2 + packages/db-sqlite/src/index.ts | 2 + packages/db-sqlite/src/init.ts | 138 +-- packages/db-sqlite/src/schema/build.ts | 544 ------------ .../db-sqlite/src/schema/buildDrizzleTable.ts | 158 ++++ packages/db-sqlite/src/schema/createIndex.ts | 27 - packages/db-sqlite/src/schema/getIDColumn.ts | 38 - packages/db-sqlite/src/schema/setColumnID.ts | 32 +- .../db-sqlite/src/schema/traverseFields.ts | 788 ------------------ packages/db-sqlite/src/schema/withDefault.ts | 27 - packages/db-sqlite/src/types.ts | 22 +- packages/db-vercel-postgres/src/index.ts | 2 + packages/drizzle/src/index.ts | 2 + packages/drizzle/src/postgres/init.ts | 118 +-- packages/drizzle/src/postgres/schema/build.ts | 522 ------------ .../src/postgres/schema/buildDrizzleTable.ts | 170 ++++ .../src/postgres/schema/createIndex.ts | 27 - .../drizzle/src/postgres/schema/idToUUID.ts | 13 - .../src/postgres/schema/parentIDColumnMap.ts | 13 - .../src/postgres/schema/setColumnID.ts | 42 +- .../src/postgres/schema/withDefault.ts | 22 - packages/drizzle/src/schema/build.ts | 705 ++++++++++++++++ .../src/schema/buildDrizzleRelations.ts | 40 + packages/drizzle/src/schema/buildRawSchema.ts | 120 +++ .../src/schema/idToUUID.ts | 0 .../{postgres => }/schema/traverseFields.ts | 596 ++++++++----- packages/drizzle/src/schema/withDefault.ts | 22 + packages/drizzle/src/types.ts | 152 +++- .../src/utilities/executeSchemaHooks.ts | 4 +- .../validateExistingBlockIsIdentical.ts | 10 +- 30 files changed, 1872 insertions(+), 2486 deletions(-) delete mode 100644 packages/db-sqlite/src/schema/build.ts create mode 100644 packages/db-sqlite/src/schema/buildDrizzleTable.ts delete mode 100644 packages/db-sqlite/src/schema/createIndex.ts delete mode 100644 packages/db-sqlite/src/schema/getIDColumn.ts delete mode 100644 packages/db-sqlite/src/schema/traverseFields.ts delete mode 100644 packages/db-sqlite/src/schema/withDefault.ts delete mode 100644 packages/drizzle/src/postgres/schema/build.ts create mode 100644 packages/drizzle/src/postgres/schema/buildDrizzleTable.ts delete mode 100644 packages/drizzle/src/postgres/schema/createIndex.ts delete mode 100644 packages/drizzle/src/postgres/schema/idToUUID.ts delete mode 100644 packages/drizzle/src/postgres/schema/parentIDColumnMap.ts delete mode 100644 packages/drizzle/src/postgres/schema/withDefault.ts create mode 100644 packages/drizzle/src/schema/build.ts create mode 100644 packages/drizzle/src/schema/buildDrizzleRelations.ts create mode 100644 packages/drizzle/src/schema/buildRawSchema.ts rename packages/{db-sqlite => drizzle}/src/schema/idToUUID.ts (100%) rename packages/drizzle/src/{postgres => }/schema/traverseFields.ts (61%) create mode 100644 packages/drizzle/src/schema/withDefault.ts diff --git a/packages/db-postgres/src/index.ts b/packages/db-postgres/src/index.ts index fe6e4b2a8b5..d73bc5649b2 100644 --- a/packages/db-postgres/src/index.ts +++ b/packages/db-postgres/src/index.ts @@ -167,6 +167,8 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj packageName: '@payloadcms/db-postgres', payload, queryDrafts, + rawRelations: {}, + rawTables: {}, rejectInitializing, requireDrizzleKit, resolveInitializing, diff --git a/packages/db-sqlite/src/index.ts b/packages/db-sqlite/src/index.ts index 4259d0c8e28..7cbe3953191 100644 --- a/packages/db-sqlite/src/index.ts +++ b/packages/db-sqlite/src/index.ts @@ -100,6 +100,8 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj { operators, prodMigrations: args.prodMigrations, push: args.push, + rawRelations: {}, + rawTables: {}, relations: {}, relationshipsSuffix: args.relationshipsSuffix || '_rels', schema: {}, diff --git a/packages/db-sqlite/src/init.ts b/packages/db-sqlite/src/init.ts index ec435890334..00d9a1fa4be 100644 --- a/packages/db-sqlite/src/init.ts +++ b/packages/db-sqlite/src/init.ts @@ -1,138 +1,38 @@ import type { DrizzleAdapter } from '@payloadcms/drizzle/types' -import type { Init, SanitizedCollectionConfig } from 'payload' +import type { Init } from 'payload' -import { createTableName, executeSchemaHooks } from '@payloadcms/drizzle' -import { uniqueIndex } from 'drizzle-orm/sqlite-core' -import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload' -import toSnakeCase from 'to-snake-case' +import { buildDrizzleRelations, buildRawSchema, executeSchemaHooks } from '@payloadcms/drizzle' -import type { BaseExtraConfig } from './schema/build.js' import type { SQLiteAdapter } from './types.js' -import { buildTable } from './schema/build.js' +import { buildDrizzleTable } from './schema/buildDrizzleTable.js' +import { setColumnID } from './schema/setColumnID.js' export const init: Init = async function init(this: SQLiteAdapter) { - let locales: [string, ...string[]] | undefined - await executeSchemaHooks({ type: 'beforeSchemaInit', adapter: this }) + let locales: string[] | undefined + + this.rawRelations = {} + this.rawTables = {} if (this.payload.config.localization) { - locales = this.payload.config.localization.locales.map(({ code }) => code) as [ - string, - ...string[], - ] + locales = this.payload.config.localization.locales.map(({ code }) => code) } - this.payload.config.collections.forEach((collection: SanitizedCollectionConfig) => { - createTableName({ - adapter: this as unknown as DrizzleAdapter, - config: collection, - }) - - if (collection.versions) { - createTableName({ - adapter: this as unknown as DrizzleAdapter, - config: collection, - versions: true, - versionsCustomName: true, - }) - } - }) - this.payload.config.collections.forEach((collection: SanitizedCollectionConfig) => { - const tableName = this.tableNameMap.get(toSnakeCase(collection.slug)) - const config = this.payload.config - - const baseExtraConfig: BaseExtraConfig = {} - - if (collection.upload.filenameCompoundIndex) { - const indexName = `${tableName}_filename_compound_idx` - - baseExtraConfig.filename_compound_index = (cols) => { - const colsConstraint = collection.upload.filenameCompoundIndex.map((f) => { - return cols[f] - }) - return uniqueIndex(indexName).on(colsConstraint[0], ...colsConstraint.slice(1)) - } - } + const adapter = this as unknown as DrizzleAdapter - if (collection.upload.filenameCompoundIndex) { - const indexName = `${tableName}_filename_compound_idx` - - baseExtraConfig.filename_compound_index = (cols) => { - const colsConstraint = collection.upload.filenameCompoundIndex.map((f) => { - return cols[f] - }) - return uniqueIndex(indexName).on(colsConstraint[0], ...colsConstraint.slice(1)) - } - } - - buildTable({ - adapter: this, - disableNotNull: !!collection?.versions?.drafts, - disableUnique: false, - fields: collection.flattenedFields, - locales, - tableName, - timestamps: collection.timestamps, - versions: false, - }) - - if (collection.versions) { - const versionsTableName = this.tableNameMap.get( - `_${toSnakeCase(collection.slug)}${this.versionsSuffix}`, - ) - const versionFields = buildVersionCollectionFields(config, collection, true) - - buildTable({ - adapter: this, - disableNotNull: !!collection.versions?.drafts, - disableUnique: true, - fields: versionFields, - locales, - tableName: versionsTableName, - timestamps: true, - versions: true, - }) - } + buildRawSchema({ + adapter, + setColumnID, }) - this.payload.config.globals.forEach((global) => { - const tableName = createTableName({ - adapter: this as unknown as DrizzleAdapter, - config: global, - }) - - buildTable({ - adapter: this, - disableNotNull: !!global?.versions?.drafts, - disableUnique: false, - fields: global.flattenedFields, - locales, - tableName, - timestamps: false, - versions: false, - }) + await executeSchemaHooks({ type: 'beforeSchemaInit', adapter: this }) - if (global.versions) { - const versionsTableName = createTableName({ - adapter: this as unknown as DrizzleAdapter, - config: global, - versions: true, - versionsCustomName: true, - }) - const config = this.payload.config - const versionFields = buildVersionGlobalFields(config, global, true) + for (const tableName in this.rawTables) { + buildDrizzleTable({ adapter, locales, rawTable: this.rawTables[tableName] }) + } - buildTable({ - adapter: this, - disableNotNull: !!global.versions?.drafts, - disableUnique: true, - fields: versionFields, - locales, - tableName: versionsTableName, - timestamps: true, - versions: true, - }) - } + buildDrizzleRelations({ + adapter, }) await executeSchemaHooks({ type: 'afterSchemaInit', adapter: this }) diff --git a/packages/db-sqlite/src/schema/build.ts b/packages/db-sqlite/src/schema/build.ts deleted file mode 100644 index 8ade85794c7..00000000000 --- a/packages/db-sqlite/src/schema/build.ts +++ /dev/null @@ -1,544 +0,0 @@ -import type { DrizzleAdapter } from '@payloadcms/drizzle/types' -import type { Relation } from 'drizzle-orm' -import type { - AnySQLiteColumn, - ForeignKeyBuilder, - IndexBuilder, - SQLiteColumnBuilder, - SQLiteTableWithColumns, - UniqueConstraintBuilder, -} from 'drizzle-orm/sqlite-core' -import type { FlattenedField } from 'payload' - -import { buildIndexName, createTableName } from '@payloadcms/drizzle' -import { relations, sql } from 'drizzle-orm' -import { - foreignKey, - index, - integer, - numeric, - sqliteTable, - text, - unique, -} from 'drizzle-orm/sqlite-core' -import toSnakeCase from 'to-snake-case' - -import type { GenericColumns, GenericTable, IDType, SQLiteAdapter } from '../types.js' - -import { createIndex } from './createIndex.js' -import { getIDColumn } from './getIDColumn.js' -import { setColumnID } from './setColumnID.js' -import { traverseFields } from './traverseFields.js' - -export type BaseExtraConfig = Record< - string, - (cols: { - [x: string]: AnySQLiteColumn - }) => ForeignKeyBuilder | IndexBuilder | UniqueConstraintBuilder -> - -export type RelationMap = Map< - string, - { - localized: boolean - relationName?: string - target: string - type: 'many' | 'one' - } -> - -type Args = { - adapter: SQLiteAdapter - baseColumns?: Record - /** - * After table is created, run these functions to add extra config to the table - * ie. indexes, multiple columns, etc - */ - baseExtraConfig?: BaseExtraConfig - buildNumbers?: boolean - buildRelationships?: boolean - disableNotNull: boolean - disableRelsTableUnique?: boolean - disableUnique: boolean - fields: FlattenedField[] - locales?: [string, ...string[]] - rootRelationships?: Set - rootRelationsToBuild?: RelationMap - rootTableIDColType?: IDType - rootTableName?: string - rootUniqueRelationships?: Set - tableName: string - timestamps?: boolean - versions: boolean - /** - * Tracks whether or not this table is built - * from the result of a localized array or block field at some point - */ - withinLocalizedArrayOrBlock?: boolean -} - -type Result = { - hasLocalizedManyNumberField: boolean - hasLocalizedManyTextField: boolean - hasLocalizedRelationshipField: boolean - hasManyNumberField: 'index' | boolean - hasManyTextField: 'index' | boolean - relationsToBuild: RelationMap -} - -export const buildTable = ({ - adapter, - baseColumns = {}, - baseExtraConfig = {}, - disableNotNull, - disableRelsTableUnique, - disableUnique = false, - fields, - locales, - rootRelationships, - rootRelationsToBuild, - rootTableIDColType, - rootTableName: incomingRootTableName, - rootUniqueRelationships, - tableName, - timestamps, - versions, - withinLocalizedArrayOrBlock, -}: Args): Result => { - const isRoot = !incomingRootTableName - const rootTableName = incomingRootTableName || tableName - const columns: Record = baseColumns - const indexes: Record IndexBuilder> = {} - - const localesColumns: Record = {} - const localesIndexes: Record IndexBuilder> = {} - let localesTable: GenericTable | SQLiteTableWithColumns - let textsTable: GenericTable | SQLiteTableWithColumns - let numbersTable: GenericTable | SQLiteTableWithColumns - - // Relationships to the base collection - const relationships: Set = rootRelationships || new Set() - const uniqueRelationships: Set = rootUniqueRelationships || new Set() - - let relationshipsTable: GenericTable | SQLiteTableWithColumns - - // Drizzle relations - const relationsToBuild: RelationMap = new Map() - - const idColType: IDType = setColumnID({ columns, fields }) - - const { - hasLocalizedField, - hasLocalizedManyNumberField, - hasLocalizedManyTextField, - hasLocalizedRelationshipField, - hasManyNumberField, - hasManyTextField, - } = traverseFields({ - adapter, - columns, - disableNotNull, - disableRelsTableUnique, - disableUnique, - fields, - indexes, - locales, - localesColumns, - localesIndexes, - newTableName: tableName, - parentTableName: tableName, - relationships, - relationsToBuild, - rootRelationsToBuild: rootRelationsToBuild || relationsToBuild, - rootTableIDColType: rootTableIDColType || idColType, - rootTableName, - 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 = text('created_at') - .default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`) - .notNull() - columns.updatedAt = text('updated_at') - .default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`) - .notNull() - } - - const table = sqliteTable(tableName, columns, (cols) => { - const extraConfig = Object.entries(baseExtraConfig).reduce((config, [key, func]) => { - config[key] = func(cols) - return config - }, {}) - - const result = Object.entries(indexes).reduce((acc, [colName, func]) => { - acc[colName] = func(cols) - return acc - }, extraConfig) - - return result - }) - - adapter.tables[tableName] = table - - if (hasLocalizedField || localizedRelations.size) { - const localeTableName = `${tableName}${adapter.localesSuffix}` - localesColumns.id = integer('id', { mode: 'number' }).primaryKey({ autoIncrement: true }) - localesColumns._locale = text('_locale', { enum: locales }).notNull() - localesColumns._parentID = getIDColumn({ - name: '_parent_id', - type: idColType, - notNull: true, - primaryKey: false, - }) - - localesTable = sqliteTable(localeTableName, localesColumns, (cols) => { - return Object.entries(localesIndexes).reduce( - (acc, [colName, func]) => { - acc[colName] = func(cols) - return acc - }, - { - _localeParent: unique(`${localeTableName}_locale_parent_id_unique`).on( - cols._locale, - cols._parentID, - ), - _parentIdFk: foreignKey({ - name: `${localeTableName}_parent_id_fk`, - columns: [cols._parentID], - foreignColumns: [table.id], - }).onDelete('cascade'), - }, - ) - }) - - adapter.tables[localeTableName] = localesTable - - adapter.relations[`relations_${localeTableName}`] = relations(localesTable, ({ many, one }) => { - const result: Record> = {} - - result._parentID = one(table, { - fields: [localesTable._parentID], - references: [table.id], - // name the relationship by what the many() relationName is - relationName: '_locales', - }) - - localizedRelations.forEach(({ type, target }, key) => { - if (type === 'one') { - result[key] = one(adapter.tables[target], { - fields: [localesTable[key]], - references: [adapter.tables[target].id], - relationName: key, - }) - } - if (type === 'many') { - result[key] = many(adapter.tables[target], { - relationName: key, - }) - } - }) - - return result - }) - } - - if (isRoot) { - if (hasManyTextField) { - const textsTableName = `${rootTableName}_texts` - const columns: Record = { - id: integer('id', { mode: 'number' }).primaryKey({ autoIncrement: true }), - order: integer('order').notNull(), - parent: getIDColumn({ - name: 'parent_id', - type: idColType, - notNull: true, - primaryKey: false, - }), - path: text('path').notNull(), - text: text('text'), - } - - if (hasLocalizedManyTextField) { - columns.locale = text('locale', { enum: locales }) - } - - textsTable = sqliteTable(textsTableName, columns, (cols) => { - const config: Record = { - orderParentIdx: index(`${textsTableName}_order_parent_idx`).on(cols.order, cols.parent), - parentFk: foreignKey({ - name: `${textsTableName}_parent_fk`, - columns: [cols.parent], - foreignColumns: [table.id], - }).onDelete('cascade'), - } - - if (hasManyTextField === 'index') { - config.text_idx = index(`${textsTableName}_text_idx`).on(cols.text) - } - - if (hasLocalizedManyTextField) { - config.localeParent = index(`${textsTableName}_locale_parent`).on( - cols.locale, - cols.parent, - ) - } - - return config - }) - - adapter.tables[textsTableName] = textsTable - - adapter.relations[`relations_${textsTableName}`] = relations(textsTable, ({ one }) => ({ - parent: one(table, { - fields: [textsTable.parent], - references: [table.id], - relationName: '_texts', - }), - })) - } - - if (hasManyNumberField) { - const numbersTableName = `${rootTableName}_numbers` - const columns: Record = { - id: integer('id', { mode: 'number' }).primaryKey({ autoIncrement: true }), - number: numeric('number'), - order: integer('order').notNull(), - parent: getIDColumn({ - name: 'parent_id', - type: idColType, - notNull: true, - primaryKey: false, - }), - path: text('path').notNull(), - } - - if (hasLocalizedManyNumberField) { - columns.locale = text('locale', { enum: locales }) - } - - numbersTable = sqliteTable(numbersTableName, columns, (cols) => { - const config: Record = { - orderParentIdx: index(`${numbersTableName}_order_parent_idx`).on(cols.order, cols.parent), - parentFk: foreignKey({ - name: `${numbersTableName}_parent_fk`, - columns: [cols.parent], - foreignColumns: [table.id], - }).onDelete('cascade'), - } - - if (hasManyNumberField === 'index') { - config.numberIdx = index(`${numbersTableName}_number_idx`).on(cols.number) - } - - if (hasLocalizedManyNumberField) { - config.localeParent = index(`${numbersTableName}_locale_parent`).on( - cols.locale, - cols.parent, - ) - } - - return config - }) - - adapter.tables[numbersTableName] = numbersTable - - adapter.relations[`relations_${numbersTableName}`] = relations(numbersTable, ({ one }) => ({ - parent: one(table, { - fields: [numbersTable.parent], - references: [table.id], - relationName: '_numbers', - }), - })) - } - - if (relationships.size) { - const relationshipColumns: Record = { - id: integer('id', { mode: 'number' }).primaryKey({ autoIncrement: true }), - order: integer('order'), - parent: getIDColumn({ - name: 'parent_id', - type: idColType, - notNull: true, - primaryKey: false, - }), - path: text('path').notNull(), - } - - if (hasLocalizedRelationshipField) { - relationshipColumns.locale = text('locale', { enum: locales }) - } - - const relationExtraConfig: BaseExtraConfig = {} - const relationshipsTableName = `${tableName}${adapter.relationshipsSuffix}` - - relationships.forEach((relationTo) => { - const relationshipConfig = adapter.payload.collections[relationTo].config - const formattedRelationTo = createTableName({ - adapter, - config: relationshipConfig, - }) - let colType: IDType = 'integer' - const relatedCollectionCustomIDType = - adapter.payload.collections[relationshipConfig.slug]?.customIDType - - if (relatedCollectionCustomIDType === 'number') { - colType = 'numeric' - } - if (relatedCollectionCustomIDType === 'text') { - colType = 'text' - } - - const colName = `${relationTo}ID` - - relationshipColumns[colName] = getIDColumn({ - name: `${formattedRelationTo}_id`, - type: colType, - primaryKey: false, - }) - - relationExtraConfig[`${relationTo}IdFk`] = (cols) => - foreignKey({ - name: `${relationshipsTableName}_${toSnakeCase(relationTo)}_fk`, - columns: [cols[colName]], - foreignColumns: [adapter.tables[formattedRelationTo].id], - }).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: adapter as unknown as DrizzleAdapter, - }) - - relationExtraConfig[indexName] = createIndex({ - name: indexColumns, - indexName, - unique, - }) - }) - - relationshipsTable = sqliteTable(relationshipsTableName, relationshipColumns, (cols) => { - const result: Record = Object.entries( - relationExtraConfig, - ).reduce( - (config, [key, func]) => { - config[key] = func(cols) - return config - }, - { - order: index(`${relationshipsTableName}_order_idx`).on(cols.order), - parentFk: foreignKey({ - name: `${relationshipsTableName}_parent_fk`, - columns: [cols.parent], - foreignColumns: [table.id], - }).onDelete('cascade'), - parentIdx: index(`${relationshipsTableName}_parent_idx`).on(cols.parent), - pathIdx: index(`${relationshipsTableName}_path_idx`).on(cols.path), - }, - ) - - if (hasLocalizedRelationshipField) { - result.localeIdx = index(`${relationshipsTableName}_locale_idx`).on(cols.locale) - } - - return result - }) - - adapter.tables[relationshipsTableName] = relationshipsTable - - adapter.relations[`relations_${relationshipsTableName}`] = relations( - relationshipsTable, - ({ one }) => { - const result: Record> = { - parent: one(table, { - fields: [relationshipsTable.parent], - references: [table.id], - relationName: '_rels', - }), - } - - relationships.forEach((relationTo) => { - const relatedTableName = createTableName({ - adapter, - config: adapter.payload.collections[relationTo].config, - }) - const idColumnName = `${relationTo}ID` - result[idColumnName] = one(adapter.tables[relatedTableName], { - fields: [relationshipsTable[idColumnName]], - references: [adapter.tables[relatedTableName].id], - relationName: relationTo, - }) - }) - - return result - }, - ) - } - } - - adapter.relations[`relations_${tableName}`] = relations(table, ({ many, one }) => { - const result: Record> = {} - - nonLocalizedRelations.forEach(({ type, relationName, target }, key) => { - if (type === 'one') { - result[key] = one(adapter.tables[target], { - fields: [table[key]], - references: [adapter.tables[target].id], - relationName: key, - }) - } - if (type === 'many') { - result[key] = many(adapter.tables[target], { relationName: relationName || key }) - } - }) - - if (hasLocalizedField) { - result._locales = many(localesTable, { relationName: '_locales' }) - } - - if (hasManyTextField) { - result._texts = many(textsTable, { relationName: '_texts' }) - } - - if (hasManyNumberField) { - result._numbers = many(numbersTable, { relationName: '_numbers' }) - } - - if (relationships.size && relationshipsTable) { - result._rels = many(relationshipsTable, { - relationName: '_rels', - }) - } - - return result - }) - - return { - hasLocalizedManyNumberField, - hasLocalizedManyTextField, - hasLocalizedRelationshipField, - hasManyNumberField, - hasManyTextField, - relationsToBuild, - } -} diff --git a/packages/db-sqlite/src/schema/buildDrizzleTable.ts b/packages/db-sqlite/src/schema/buildDrizzleTable.ts new file mode 100644 index 00000000000..d5e273c875c --- /dev/null +++ b/packages/db-sqlite/src/schema/buildDrizzleTable.ts @@ -0,0 +1,158 @@ +import type { BuildDrizzleTable, RawColumn } from '@payloadcms/drizzle/types' +import type { ForeignKeyBuilder, IndexBuilder } from 'drizzle-orm/sqlite-core' + +import { sql } from 'drizzle-orm' +import { + foreignKey, + index, + integer, + numeric, + sqliteTable, + text, + uniqueIndex, +} from 'drizzle-orm/sqlite-core' +import { v4 as uuidv4 } from 'uuid' + +const rawColumnBuilderMap: Partial> = { + integer, + numeric, + text, +} + +export const buildDrizzleTable: BuildDrizzleTable = ({ adapter, locales, rawTable }) => { + const columns: Record = {} + + for (const [key, column] of Object.entries(rawTable.columns)) { + switch (column.type) { + case 'boolean': { + columns[key] = integer(column.name, { mode: 'boolean' }) + break + } + + case 'enum': + if ('locale' in column) { + columns[key] = text(column.name, { enum: locales as [string, ...string[]] }) + } else { + columns[key] = text(column.name, { enum: column.options as [string, ...string[]] }) + } + break + + case 'geometry': + case 'jsonb': { + columns[key] = text(column.name, { mode: 'json' }) + break + } + + case 'serial': { + columns[key] = integer(column.name) + break + } + + case 'timestamp': { + let builder = text(column.name) + + if (column.defaultNow) { + builder = builder.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`) + } + + columns[key] = builder + break + } + + // Not used yet in SQLite but ready here. + case 'uuid': { + let builder = text(column.name) + + if (column.defaultRandom) { + builder = builder.$defaultFn(() => uuidv4()) + } + + columns[key] = builder + break + } + + case 'varchar': { + columns[key] = text(column.name) + break + } + + default: + columns[key] = rawColumnBuilderMap[column.type](column.name) + break + } + + if (column.reference) { + columns[key].references(() => adapter.tables[column.reference.table][column.reference.name], { + onDelete: column.reference.onDelete, + }) + } + + if (column.primaryKey) { + columns[key].primaryKey() + } + + if (column.notNull) { + columns[key].notNull() + } + + if (typeof column.default !== 'undefined') { + let sanitizedDefault = column.default + + if (column.type === 'geometry' && Array.isArray(column.default)) { + sanitizedDefault = JSON.stringify({ + type: 'Point', + coordinates: [column.default[0], column.default[1]], + }) + } + + columns[key].default(sanitizedDefault) + } + } + + const extraConfig = (cols: any) => { + const config: Record = {} + + if (rawTable.indexes) { + for (const [key, rawIndex] of Object.entries(rawTable.indexes)) { + let fn: any = index + if (rawIndex.unique) { + fn = uniqueIndex + } + + if (Array.isArray(rawIndex.on)) { + if (rawIndex.on.length) { + config[key] = fn(rawIndex.name).on(...rawIndex.on.map((colName) => cols[colName])) + } + } else { + config[key] = fn(rawIndex.name).on(cols[rawIndex.on]) + } + } + } + + if (rawTable.foreignKeys) { + for (const [key, rawForeignKey] of Object.entries(rawTable.foreignKeys)) { + let builder = foreignKey({ + name: rawForeignKey.name, + columns: rawForeignKey.columns.map((colName) => cols[colName]) as any, + foreignColumns: rawForeignKey.foreignColumns.map( + (column) => adapter.tables[column.table][column.name], + ), + }) + + if (rawForeignKey.onDelete) { + builder = builder.onDelete(rawForeignKey.onDelete) + } + + if (rawForeignKey.onUpdate) { + builder = builder.onDelete(rawForeignKey.onUpdate) + } + + config[key] = builder + } + } + + return config + } + + adapter.tables[rawTable.name] = sqliteTable(rawTable.name, columns as any, extraConfig as any) +} diff --git a/packages/db-sqlite/src/schema/createIndex.ts b/packages/db-sqlite/src/schema/createIndex.ts deleted file mode 100644 index ea3a52c9177..00000000000 --- a/packages/db-sqlite/src/schema/createIndex.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { AnySQLiteColumn } from 'drizzle-orm/sqlite-core' - -import { index, uniqueIndex } from 'drizzle-orm/sqlite-core' - -type CreateIndexArgs = { - indexName: string - name: string | string[] - unique?: boolean -} - -export const createIndex = ({ name, indexName, unique }: CreateIndexArgs) => { - return (table: { [x: string]: AnySQLiteColumn }) => { - let columns - if (Array.isArray(name)) { - columns = name - .map((columnName) => table[columnName]) - // exclude fields were included in compound indexes but do not exist on the table - .filter((col) => typeof col !== 'undefined') - } else { - columns = [table[name]] - } - if (unique) { - return uniqueIndex(indexName).on(columns[0], ...columns.slice(1)) - } - return index(indexName).on(columns[0], ...columns.slice(1)) - } -} diff --git a/packages/db-sqlite/src/schema/getIDColumn.ts b/packages/db-sqlite/src/schema/getIDColumn.ts deleted file mode 100644 index e5356a9e93f..00000000000 --- a/packages/db-sqlite/src/schema/getIDColumn.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { integer, numeric, text } from 'drizzle-orm/sqlite-core' - -import type { IDType } from '../types.js' - -export const getIDColumn = ({ - name, - type, - notNull, - primaryKey, -}: { - name: string - notNull?: boolean - primaryKey: boolean - type: IDType -}) => { - let column - switch (type) { - case 'integer': - column = integer(name) - break - case 'numeric': - column = numeric(name) - break - case 'text': - column = text(name) - break - } - - if (notNull) { - column.notNull() - } - - if (primaryKey) { - column.primaryKey() - } - - return column -} diff --git a/packages/db-sqlite/src/schema/setColumnID.ts b/packages/db-sqlite/src/schema/setColumnID.ts index 95f4561e90c..cdcf6eb2220 100644 --- a/packages/db-sqlite/src/schema/setColumnID.ts +++ b/packages/db-sqlite/src/schema/setColumnID.ts @@ -1,28 +1,32 @@ -import type { SQLiteColumnBuilder } from 'drizzle-orm/sqlite-core' -import type { FlattenedField } from 'payload' +import type { SetColumnID } from '@payloadcms/drizzle/types' -import { integer, numeric, text } from 'drizzle-orm/sqlite-core' - -import type { IDType } from '../types.js' - -type Args = { - columns: Record - fields: FlattenedField[] -} -export const setColumnID = ({ columns, fields }: Args): IDType => { +export const setColumnID: SetColumnID = ({ columns, fields }) => { const idField = fields.find((field) => field.name === 'id') if (idField) { if (idField.type === 'number') { - columns.id = numeric('id').primaryKey() + columns.id = { + name: 'id', + type: 'numeric', + primaryKey: true, + } return 'numeric' } if (idField.type === 'text') { - columns.id = text('id').primaryKey() + columns.id = { + name: 'id', + type: 'text', + primaryKey: true, + } return 'text' } } - columns.id = integer('id').primaryKey() + columns.id = { + name: 'id', + type: 'integer', + primaryKey: true, + } + return 'integer' } diff --git a/packages/db-sqlite/src/schema/traverseFields.ts b/packages/db-sqlite/src/schema/traverseFields.ts deleted file mode 100644 index d5e766fce4e..00000000000 --- a/packages/db-sqlite/src/schema/traverseFields.ts +++ /dev/null @@ -1,788 +0,0 @@ -import type { DrizzleAdapter } from '@payloadcms/drizzle/types' -import type { Relation } from 'drizzle-orm' -import type { IndexBuilder, SQLiteColumnBuilder } from 'drizzle-orm/sqlite-core' -import type { FlattenedField } from 'payload' - -import { - buildIndexName, - createTableName, - hasLocalesTable, - validateExistingBlockIsIdentical, -} from '@payloadcms/drizzle' -import { relations } from 'drizzle-orm' -import { - foreignKey, - index, - integer, - numeric, - SQLiteIntegerBuilder, - SQLiteNumericBuilder, - SQLiteTextBuilder, - text, -} from 'drizzle-orm/sqlite-core' -import { InvalidConfiguration } from 'payload' -import { fieldAffectsData, fieldIsVirtual, optionIsObject } from 'payload/shared' -import toSnakeCase from 'to-snake-case' - -import type { GenericColumns, IDType, SQLiteAdapter } from '../types.js' -import type { BaseExtraConfig, RelationMap } from './build.js' - -import { buildTable } from './build.js' -import { createIndex } from './createIndex.js' -import { getIDColumn } from './getIDColumn.js' -import { idToUUID } from './idToUUID.js' -import { withDefault } from './withDefault.js' - -type Args = { - adapter: SQLiteAdapter - columnPrefix?: string - columns: Record - disableNotNull: boolean - disableRelsTableUnique?: boolean - disableUnique?: boolean - fieldPrefix?: string - fields: FlattenedField[] - forceLocalized?: boolean - indexes: Record IndexBuilder> - locales: [string, ...string[]] - localesColumns: Record - localesIndexes: Record IndexBuilder> - newTableName: string - parentTableName: string - relationships: Set - relationsToBuild: RelationMap - rootRelationsToBuild?: RelationMap - rootTableIDColType: IDType - rootTableName: string - uniqueRelationships: Set - versions: boolean - /** - * Tracks whether or not this table is built - * from the result of a localized array or block field at some point - */ - withinLocalizedArrayOrBlock?: boolean -} - -type Result = { - hasLocalizedField: boolean - hasLocalizedManyNumberField: boolean - hasLocalizedManyTextField: boolean - hasLocalizedRelationshipField: boolean - hasManyNumberField: 'index' | boolean - hasManyTextField: 'index' | boolean -} - -export const traverseFields = ({ - adapter, - columnPrefix, - columns, - disableNotNull, - disableRelsTableUnique, - disableUnique = false, - fieldPrefix, - fields, - forceLocalized, - indexes, - locales, - localesColumns, - localesIndexes, - newTableName, - parentTableName, - relationships, - relationsToBuild, - rootRelationsToBuild, - rootTableIDColType, - rootTableName, - uniqueRelationships, - versions, - withinLocalizedArrayOrBlock, -}: Args): Result => { - let hasLocalizedField = false - let hasLocalizedRelationshipField = false - let hasManyTextField: 'index' | boolean = false - let hasLocalizedManyTextField = false - let hasManyNumberField: 'index' | boolean = false - let hasLocalizedManyNumberField = false - - let parentIDColType: IDType = 'integer' - if (columns.id instanceof SQLiteIntegerBuilder) { - parentIDColType = 'integer' - } - if (columns.id instanceof SQLiteNumericBuilder) { - parentIDColType = 'numeric' - } - if (columns.id instanceof SQLiteTextBuilder) { - parentIDColType = 'text' - } - - 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}` - - // If field is localized, - // add the column to the locale table instead of main table - if ( - adapter.payload.config.localization && - (field.localized || forceLocalized) && - field.type !== 'array' && - field.type !== 'blocks' && - (('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', 'point'].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: adapter as unknown as DrizzleAdapter, - }) - - targetIndexes[indexName] = createIndex({ - name: field.localized ? [fieldName, '_locale'] : fieldName, - indexName, - unique, - }) - } - - switch (field.type) { - case 'array': { - const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull - - const arrayTableName = createTableName({ - adapter, - config: field, - parentTableName: newTableName, - prefix: `${newTableName}_`, - versionsCustomName: versions, - }) - - const baseColumns: Record = { - _order: integer('_order').notNull(), - _parentID: getIDColumn({ - name: '_parent_id', - type: parentIDColType, - notNull: true, - primaryKey: false, - }), - } - - const baseExtraConfig: BaseExtraConfig = { - _orderIdx: (cols) => index(`${arrayTableName}_order_idx`).on(cols._order), - _parentIDFk: (cols) => - foreignKey({ - name: `${arrayTableName}_parent_id_fk`, - columns: [cols['_parentID']], - foreignColumns: [adapter.tables[parentTableName].id], - }).onDelete('cascade'), - _parentIDIdx: (cols) => index(`${arrayTableName}_parent_id_idx`).on(cols._parentID), - } - - const isLocalized = - Boolean(field.localized && adapter.payload.config.localization) || - withinLocalizedArrayOrBlock || - forceLocalized - - if (isLocalized) { - baseColumns._locale = text('_locale', { enum: locales }).notNull() - baseExtraConfig._localeIdx = (cols) => - index(`${arrayTableName}_locale_idx`).on(cols._locale) - } - - const { - hasLocalizedManyNumberField: subHasLocalizedManyNumberField, - hasLocalizedManyTextField: subHasLocalizedManyTextField, - hasLocalizedRelationshipField: subHasLocalizedRelationshipField, - hasManyNumberField: subHasManyNumberField, - hasManyTextField: subHasManyTextField, - relationsToBuild: subRelationsToBuild, - } = buildTable({ - adapter, - baseColumns, - baseExtraConfig, - disableNotNull: disableNotNullFromHere, - disableRelsTableUnique: true, - disableUnique, - fields: disableUnique ? idToUUID(field.flattenedFields) : field.flattenedFields, - rootRelationships: relationships, - rootRelationsToBuild, - rootTableIDColType, - rootTableName, - rootUniqueRelationships: uniqueRelationships, - 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 - } - } - - relationsToBuild.set(fieldName, { - type: 'many', - // arrays have their own localized table, independent of the base table. - localized: false, - target: arrayTableName, - }) - - adapter.relations[`relations_${arrayTableName}`] = relations( - adapter.tables[arrayTableName], - ({ many, one }) => { - const result: Record> = { - _parentID: one(adapter.tables[parentTableName], { - fields: [adapter.tables[arrayTableName]._parentID], - references: [adapter.tables[parentTableName].id], - relationName: fieldName, - }), - } - - if (hasLocalesTable(field.fields)) { - result._locales = many(adapter.tables[`${arrayTableName}${adapter.localesSuffix}`], { - relationName: '_locales', - }) - } - - subRelationsToBuild.forEach(({ type, localized, target }, key) => { - if (type === 'one') { - const arrayWithLocalized = localized - ? `${arrayTableName}${adapter.localesSuffix}` - : arrayTableName - result[key] = one(adapter.tables[target], { - fields: [adapter.tables[arrayWithLocalized][key]], - references: [adapter.tables[target].id], - relationName: key, - }) - } - if (type === 'many') { - result[key] = many(adapter.tables[target], { relationName: key }) - } - }) - - return result - }, - ) - - break - } - case 'blocks': { - const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull - - field.blocks.forEach((block) => { - const blockTableName = createTableName({ - adapter, - config: block, - parentTableName: rootTableName, - prefix: `${rootTableName}_blocks_`, - versionsCustomName: versions, - }) - if (!adapter.tables[blockTableName]) { - const baseColumns: Record = { - _order: integer('_order').notNull(), - _parentID: getIDColumn({ - name: '_parent_id', - type: rootTableIDColType, - notNull: true, - primaryKey: false, - }), - _path: text('_path').notNull(), - } - - const baseExtraConfig: BaseExtraConfig = { - _orderIdx: (cols) => index(`${blockTableName}_order_idx`).on(cols._order), - _parentIdFk: (cols) => - foreignKey({ - name: `${blockTableName}_parent_id_fk`, - columns: [cols._parentID], - foreignColumns: [adapter.tables[rootTableName].id], - }).onDelete('cascade'), - _parentIDIdx: (cols) => index(`${blockTableName}_parent_id_idx`).on(cols._parentID), - _pathIdx: (cols) => index(`${blockTableName}_path_idx`).on(cols._path), - } - - const isLocalized = - Boolean(field.localized && adapter.payload.config.localization) || - withinLocalizedArrayOrBlock || - forceLocalized - - if (isLocalized) { - baseColumns._locale = text('_locale', { enum: locales }).notNull() - baseExtraConfig._localeIdx = (cols) => - index(`${blockTableName}_locale_idx`).on(cols._locale) - } - - const { - hasLocalizedManyNumberField: subHasLocalizedManyNumberField, - hasLocalizedManyTextField: subHasLocalizedManyTextField, - hasLocalizedRelationshipField: subHasLocalizedRelationshipField, - hasManyNumberField: subHasManyNumberField, - hasManyTextField: subHasManyTextField, - relationsToBuild: subRelationsToBuild, - } = buildTable({ - adapter, - baseColumns, - baseExtraConfig, - disableNotNull: disableNotNullFromHere, - disableRelsTableUnique: true, - disableUnique, - fields: disableUnique ? idToUUID(block.flattenedFields) : block.flattenedFields, - rootRelationships: relationships, - rootRelationsToBuild, - rootTableIDColType, - rootTableName, - rootUniqueRelationships: uniqueRelationships, - 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 - } - } - - adapter.relations[`relations_${blockTableName}`] = relations( - adapter.tables[blockTableName], - ({ many, one }) => { - const result: Record> = { - _parentID: one(adapter.tables[rootTableName], { - fields: [adapter.tables[blockTableName]._parentID], - references: [adapter.tables[rootTableName].id], - relationName: `_blocks_${block.slug}`, - }), - } - - if (hasLocalesTable(block.fields)) { - result._locales = many( - adapter.tables[`${blockTableName}${adapter.localesSuffix}`], - { relationName: '_locales' }, - ) - } - - subRelationsToBuild.forEach(({ type, localized, target }, key) => { - if (type === 'one') { - const blockWithLocalized = localized - ? `${blockTableName}${adapter.localesSuffix}` - : blockTableName - result[key] = one(adapter.tables[target], { - fields: [adapter.tables[blockWithLocalized][key]], - references: [adapter.tables[target].id], - relationName: key, - }) - } - if (type === 'many') { - result[key] = many(adapter.tables[target], { relationName: key }) - } - }) - - return result - }, - ) - } else if (process.env.NODE_ENV !== 'production' && !versions) { - validateExistingBlockIsIdentical({ - block, - localized: field.localized, - rootTableName, - table: adapter.tables[blockTableName], - tableLocales: adapter.tables[`${blockTableName}${adapter.localesSuffix}`], - }) - } - // blocks relationships are defined from the collection or globals table down to the block, bypassing any subBlocks - rootRelationsToBuild.set(`_blocks_${block.slug}`, { - type: 'many', - // blocks are not localized on the parent table - localized: false, - target: blockTableName, - }) - }) - - break - } - case 'checkbox': { - targetTable[fieldName] = withDefault(integer(columnName, { mode: 'boolean' }), field) - break - } - - case 'code': - case 'email': - case 'textarea': { - targetTable[fieldName] = withDefault(text(columnName), field) - break - } - - case 'date': { - targetTable[fieldName] = withDefault(text(columnName), 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, - columnPrefix: `${columnName}_`, - columns, - disableNotNull: disableNotNullFromHere, - disableUnique, - fieldPrefix: `${fieldName}.`, - fields: field.flattenedFields, - forceLocalized: field.localized, - indexes, - locales, - localesColumns, - localesIndexes, - newTableName: `${parentTableName}_${columnName}`, - parentTableName, - relationships, - relationsToBuild, - rootRelationsToBuild, - rootTableIDColType, - rootTableName, - uniqueRelationships, - versions, - withinLocalizedArrayOrBlock: withinLocalizedArrayOrBlock || field.localized, - }) - - 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(text(columnName, { mode: 'json' }), field) - break - } - - case 'number': { - if (field.hasMany) { - const isLocalized = - Boolean(field.localized && 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(numeric(columnName), field) - } - break - } - - case 'point': { - targetTable[fieldName] = withDefault(text(columnName, { mode: 'json' }), field) - break - } - - case 'radio': - case 'select': { - const options = field.options.map((option) => { - if (optionIsObject(option)) { - return option.value - } - - return option - }) as [string, ...string[]] - - if (field.type === 'select' && field.hasMany) { - const selectTableName = createTableName({ - adapter, - config: field, - parentTableName: newTableName, - prefix: `${newTableName}_`, - versionsCustomName: versions, - }) - const baseColumns: Record = { - order: integer('order').notNull(), - parent: getIDColumn({ - name: 'parent_id', - type: parentIDColType, - notNull: true, - primaryKey: false, - }), - value: text('value', { enum: options }), - } - - const baseExtraConfig: BaseExtraConfig = { - orderIdx: (cols) => index(`${selectTableName}_order_idx`).on(cols.order), - parentFk: (cols) => - foreignKey({ - name: `${selectTableName}_parent_fk`, - columns: [cols.parent], - foreignColumns: [adapter.tables[parentTableName].id], - }).onDelete('cascade'), - parentIdx: (cols) => index(`${selectTableName}_parent_idx`).on(cols.parent), - } - - const isLocalized = - Boolean(field.localized && adapter.payload.config.localization) || - withinLocalizedArrayOrBlock || - forceLocalized - - if (isLocalized) { - baseColumns.locale = text('locale', { enum: locales }).notNull() - baseExtraConfig.localeIdx = (cols) => - index(`${selectTableName}_locale_idx`).on(cols.locale) - } - - if (field.index) { - baseExtraConfig.value = (cols) => index(`${selectTableName}_value_idx`).on(cols.value) - } - - buildTable({ - adapter, - baseColumns, - baseExtraConfig, - disableNotNull, - disableUnique, - fields: [], - rootTableName, - tableName: selectTableName, - versions, - }) - - relationsToBuild.set(fieldName, { - type: 'many', - // selects have their own localized table, independent of the base table. - localized: false, - target: selectTableName, - }) - - adapter.relations[`relations_${selectTableName}`] = relations( - adapter.tables[selectTableName], - ({ one }) => ({ - parent: one(adapter.tables[parentTableName], { - fields: [adapter.tables[selectTableName].parent], - references: [adapter.tables[parentTableName].id], - relationName: fieldName, - }), - }), - ) - } else { - targetTable[fieldName] = withDefault( - text(columnName, { - enum: 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: IDType = 'integer' - const relatedCollectionCustomID = relationshipConfig.fields.find( - (field) => fieldAffectsData(field) && field.name === 'id', - ) - if (relatedCollectionCustomID?.type === 'number') { - colType = 'numeric' - } - if (relatedCollectionCustomID?.type === 'text') { - colType = 'text' - } - - // make the foreign key column for relationship using the correct id column type - targetTable[fieldName] = getIDColumn({ - name: `${columnName}_id`, - type: colType, - primaryKey: false, - }).references(() => adapter.tables[tableName].id, { onDelete: 'set null' }) - - // add relationship to table - relationsToBuild.set(fieldName, { - type: 'one', - localized: adapter.payload.config.localization && (field.localized || forceLocalized), - target: tableName, - }) - - // add notNull when not required - if (!disableNotNull && field.required && !field.admin?.condition) { - targetTable[fieldName].notNull() - } - break - } - if ( - Boolean(field.localized && adapter.payload.config.localization) || - withinLocalizedArrayOrBlock - ) { - hasLocalizedRelationshipField = true - } - - break - - case 'text': { - if (field.hasMany) { - const isLocalized = - Boolean(field.localized && 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 SQLite for hasMany text fields.', - ) - } - } else { - targetTable[fieldName] = withDefault(text(columnName), field) - } - break - } - - default: - break - } - - const condition = field.admin && field.admin.condition - - if ( - !disableNotNull && - targetTable[fieldName] && - 'required' in field && - field.required && - !condition - ) { - targetTable[fieldName].notNull() - } - }) - - return { - hasLocalizedField, - hasLocalizedManyNumberField, - hasLocalizedManyTextField, - hasLocalizedRelationshipField, - hasManyNumberField, - hasManyTextField, - } -} diff --git a/packages/db-sqlite/src/schema/withDefault.ts b/packages/db-sqlite/src/schema/withDefault.ts deleted file mode 100644 index ed0ddfd2dfb..00000000000 --- a/packages/db-sqlite/src/schema/withDefault.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { SQLiteColumnBuilder } from 'drizzle-orm/sqlite-core' -import type { FieldAffectingData } from 'payload' - -export const withDefault = ( - column: SQLiteColumnBuilder, - field: FieldAffectingData, -): SQLiteColumnBuilder => { - if (typeof field.defaultValue === 'undefined' || typeof field.defaultValue === 'function') { - return column - } - - if (field.type === 'point' && Array.isArray(field.defaultValue)) { - return column.default( - JSON.stringify({ - type: 'Point', - coordinates: [field.defaultValue[0], field.defaultValue[1]], - }), - ) - } - - if (typeof field.defaultValue === 'string' && field.defaultValue.includes("'")) { - const escapedString = field.defaultValue.replaceAll("'", "''") - return column.default(escapedString) - } - - return column.default(field.defaultValue) -} diff --git a/packages/db-sqlite/src/types.ts b/packages/db-sqlite/src/types.ts index 6e07a7756cc..358cb764993 100644 --- a/packages/db-sqlite/src/types.ts +++ b/packages/db-sqlite/src/types.ts @@ -38,6 +38,8 @@ export type Args = { */ beforeSchemaInit?: SQLiteSchemaHook[] client: Config + /** Generated schema from payload generate:db-schema file path */ + generateSchemaOutputFile?: string idType?: 'serial' | 'uuid' localesSuffix?: string logger?: DrizzleConfig['logger'] @@ -109,6 +111,16 @@ type SQLiteDrizzleAdapter = Omit< | 'relations' > +export interface GeneratedDatabaseSchema { + schemaUntyped: Record +} + +type ResolveSchemaType = 'schema' extends keyof T + ? T['schema'] + : GeneratedDatabaseSchema['schemaUntyped'] + +type Drizzle = { $client: Client } & LibSQLDatabase> + export type SQLiteAdapter = { afterSchemaInit: SQLiteSchemaHook[] beforeSchemaInit: SQLiteSchemaHook[] @@ -117,9 +129,7 @@ export type SQLiteAdapter = { countDistinct: CountDistinct defaultDrizzleSnapshot: any deleteWhere: DeleteWhere - drizzle: { $client: Client } & LibSQLDatabase< - Record & Record - > + drizzle: Drizzle dropDatabase: DropDatabase execute: Execute /** @@ -165,7 +175,7 @@ export type MigrateUpArgs = { * } * ``` */ - db: LibSQLDatabase + db: Drizzle /** * The Payload instance that you can use to execute Local API methods * To use the current transaction you must pass `req` to arguments @@ -196,7 +206,7 @@ export type MigrateDownArgs = { * } * ``` */ - db: LibSQLDatabase + db: Drizzle /** * The Payload instance that you can use to execute Local API methods * To use the current transaction you must pass `req` to arguments @@ -221,7 +231,7 @@ declare module 'payload' { extends Omit, DrizzleAdapter { beginTransaction: (options?: SQLiteTransactionConfig) => Promise - drizzle: LibSQLDatabase + drizzle: Drizzle /** * An object keyed on each table, with a key value pair where the constraint name is the key, followed by the dot-notation field name * Used for returning properly formed errors from unique fields diff --git a/packages/db-vercel-postgres/src/index.ts b/packages/db-vercel-postgres/src/index.ts index e735190932b..575223d9c51 100644 --- a/packages/db-vercel-postgres/src/index.ts +++ b/packages/db-vercel-postgres/src/index.ts @@ -110,6 +110,8 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj { - createTableName({ - adapter: this, - config: collection, - }) - - if (collection.versions) { - createTableName({ - adapter: this, - config: collection, - versions: true, - versionsCustomName: true, - }) - } - }) - this.payload.config.collections.forEach((collection: SanitizedCollectionConfig) => { - const tableName = this.tableNameMap.get(toSnakeCase(collection.slug)) - - const baseExtraConfig: BaseExtraConfig = {} - - if (collection.upload.filenameCompoundIndex) { - const indexName = `${tableName}_filename_compound_idx` - - baseExtraConfig.filename_compound_index = (cols) => { - const colsConstraint = collection.upload.filenameCompoundIndex.map((f) => { - return cols[f] - }) - return uniqueIndex(indexName).on(colsConstraint[0], ...colsConstraint.slice(1)) - } - } - - buildTable({ - adapter: this, - baseExtraConfig, - disableNotNull: !!collection?.versions?.drafts, - disableUnique: false, - fields: collection.flattenedFields, - tableName, - timestamps: collection.timestamps, - versions: false, - }) - - if (collection.versions) { - const versionsTableName = this.tableNameMap.get( - `_${toSnakeCase(collection.slug)}${this.versionsSuffix}`, - ) - const versionFields = buildVersionCollectionFields(this.payload.config, collection, true) - - buildTable({ - adapter: this, - disableNotNull: !!collection.versions?.drafts, - disableUnique: true, - fields: versionFields, - tableName: versionsTableName, - timestamps: true, - versions: true, - }) - } - }) - - this.payload.config.globals.forEach((global) => { - const tableName = createTableName({ adapter: this, config: global }) - - buildTable({ - adapter: this, - disableNotNull: !!global?.versions?.drafts, - disableUnique: false, - fields: global.flattenedFields, - tableName, - timestamps: false, - versions: false, - }) - - if (global.versions) { - const versionsTableName = createTableName({ - adapter: this, - config: global, - versions: true, - versionsCustomName: true, - }) - const versionFields = buildVersionGlobalFields(this.payload.config, global, true) + for (const tableName in this.rawTables) { + buildDrizzleTable({ adapter: this, rawTable: this.rawTables[tableName] }) + } - buildTable({ - adapter: this, - disableNotNull: !!global.versions?.drafts, - disableUnique: true, - fields: versionFields, - tableName: versionsTableName, - timestamps: true, - versions: true, - }) - } + buildDrizzleRelations({ + adapter: this, }) await executeSchemaHooks({ type: 'afterSchemaInit', adapter: this }) diff --git a/packages/drizzle/src/postgres/schema/build.ts b/packages/drizzle/src/postgres/schema/build.ts deleted file mode 100644 index e60f1382dc4..00000000000 --- a/packages/drizzle/src/postgres/schema/build.ts +++ /dev/null @@ -1,522 +0,0 @@ -import type { Relation } from 'drizzle-orm' -import type { - ForeignKeyBuilder, - IndexBuilder, - PgColumnBuilder, - PgTableWithColumns, -} from 'drizzle-orm/pg-core' -import type { FlattenedField } from 'payload' - -import { relations } from 'drizzle-orm' -import { - foreignKey, - index, - integer, - numeric, - serial, - timestamp, - unique, - varchar, -} from 'drizzle-orm/pg-core' -import toSnakeCase from 'to-snake-case' - -import type { - BaseExtraConfig, - BasePostgresAdapter, - GenericColumns, - GenericTable, - IDType, - RelationMap, -} from '../types.js' - -import { createTableName } from '../../createTableName.js' -import { buildIndexName } from '../../utilities/buildIndexName.js' -import { createIndex } from './createIndex.js' -import { parentIDColumnMap } from './parentIDColumnMap.js' -import { setColumnID } from './setColumnID.js' -import { traverseFields } from './traverseFields.js' - -type Args = { - adapter: BasePostgresAdapter - baseColumns?: Record - /** - * After table is created, run these functions to add extra config to the table - * ie. indexes, multiple columns, etc - */ - baseExtraConfig?: BaseExtraConfig - buildNumbers?: boolean - buildRelationships?: boolean - disableNotNull: boolean - disableRelsTableUnique?: boolean - disableUnique: boolean - fields: FlattenedField[] - rootRelationships?: Set - rootRelationsToBuild?: RelationMap - rootTableIDColType?: string - rootTableName?: string - rootUniqueRelationships?: Set - tableName: string - timestamps?: boolean - versions: boolean - /** - * Tracks whether or not this table is built - * from the result of a localized array or block field at some point - */ - withinLocalizedArrayOrBlock?: boolean -} - -type Result = { - hasLocalizedManyNumberField: boolean - hasLocalizedManyTextField: boolean - hasLocalizedRelationshipField: boolean - hasManyNumberField: 'index' | boolean - hasManyTextField: 'index' | boolean - relationsToBuild: RelationMap -} - -export const buildTable = ({ - adapter, - baseColumns = {}, - baseExtraConfig = {}, - disableNotNull, - disableRelsTableUnique = false, - disableUnique = false, - fields, - rootRelationships, - rootRelationsToBuild, - rootTableIDColType, - rootTableName: incomingRootTableName, - rootUniqueRelationships, - tableName, - timestamps, - versions, - withinLocalizedArrayOrBlock, -}: Args): Result => { - const isRoot = !incomingRootTableName - const rootTableName = incomingRootTableName || tableName - const columns: Record = baseColumns - const indexes: Record IndexBuilder> = {} - - const localesColumns: Record = {} - const localesIndexes: Record IndexBuilder> = {} - let localesTable: GenericTable | PgTableWithColumns - let textsTable: GenericTable | PgTableWithColumns - let numbersTable: GenericTable | PgTableWithColumns - - // Relationships to the base collection - const relationships: Set = rootRelationships || new Set() - - // Unique relationships to the base collection - const uniqueRelationships: Set = rootUniqueRelationships || new Set() - - let relationshipsTable: GenericTable | PgTableWithColumns - - // Drizzle relations - const relationsToBuild: RelationMap = new Map() - - const idColType: IDType = setColumnID({ adapter, columns, fields }) - - const { - hasLocalizedField, - hasLocalizedManyNumberField, - hasLocalizedManyTextField, - hasLocalizedRelationshipField, - hasManyNumberField, - hasManyTextField, - } = traverseFields({ - adapter, - columns, - disableNotNull, - disableRelsTableUnique, - disableUnique, - fields, - indexes, - localesColumns, - localesIndexes, - newTableName: tableName, - parentTableName: tableName, - relationships, - relationsToBuild, - rootRelationsToBuild: rootRelationsToBuild || relationsToBuild, - rootTableIDColType: rootTableIDColType || idColType, - rootTableName, - 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 = timestamp('created_at', { - mode: 'string', - precision: 3, - withTimezone: true, - }) - .defaultNow() - .notNull() - columns.updatedAt = timestamp('updated_at', { - mode: 'string', - precision: 3, - withTimezone: true, - }) - .defaultNow() - .notNull() - } - - const table = adapter.pgSchema.table(tableName, columns, (cols) => { - const extraConfig = Object.entries(baseExtraConfig).reduce((config, [key, func]) => { - config[key] = func(cols) - return config - }, {}) - - const result = Object.entries(indexes).reduce((acc, [colName, func]) => { - acc[colName] = func(cols) - return acc - }, extraConfig) - - return result - }) - - adapter.tables[tableName] = table - - if (hasLocalizedField || localizedRelations.size) { - const localeTableName = `${tableName}${adapter.localesSuffix}` - localesColumns.id = serial('id').primaryKey() - localesColumns._locale = adapter.enums.enum__locales('_locale').notNull() - localesColumns._parentID = parentIDColumnMap[idColType]('_parent_id').notNull() - - localesTable = adapter.pgSchema.table(localeTableName, localesColumns, (cols) => { - return Object.entries(localesIndexes).reduce( - (acc, [colName, func]) => { - acc[colName] = func(cols) - return acc - }, - { - _localeParent: unique(`${localeTableName}_locale_parent_id_unique`).on( - cols._locale, - cols._parentID, - ), - _parentIdFk: foreignKey({ - name: `${localeTableName}_parent_id_fk`, - columns: [cols._parentID], - foreignColumns: [table.id], - }).onDelete('cascade'), - }, - ) - }) - - adapter.tables[localeTableName] = localesTable - - adapter.relations[`relations_${localeTableName}`] = relations(localesTable, ({ many, one }) => { - const result: Record> = {} - - result._parentID = one(table, { - fields: [localesTable._parentID], - references: [table.id], - // name the relationship by what the many() relationName is - relationName: '_locales', - }) - - localizedRelations.forEach(({ type, target }, key) => { - if (type === 'one') { - result[key] = one(adapter.tables[target], { - fields: [localesTable[key]], - references: [adapter.tables[target].id], - relationName: key, - }) - } - if (type === 'many') { - result[key] = many(adapter.tables[target], { - relationName: key, - }) - } - }) - - return result - }) - } - - if (isRoot) { - if (hasManyTextField) { - const textsTableName = `${rootTableName}_texts` - const columns: Record = { - id: serial('id').primaryKey(), - order: integer('order').notNull(), - parent: parentIDColumnMap[idColType]('parent_id').notNull(), - path: varchar('path').notNull(), - text: varchar('text'), - } - - if (hasLocalizedManyTextField) { - columns.locale = adapter.enums.enum__locales('locale') - } - - textsTable = adapter.pgSchema.table(textsTableName, columns, (cols) => { - const config: Record = { - orderParentIdx: index(`${textsTableName}_order_parent_idx`).on(cols.order, cols.parent), - parentFk: foreignKey({ - name: `${textsTableName}_parent_fk`, - columns: [cols.parent], - foreignColumns: [table.id], - }).onDelete('cascade'), - } - - if (hasManyTextField === 'index') { - config.text_idx = index(`${textsTableName}_text_idx`).on(cols.text) - } - - if (hasLocalizedManyTextField) { - config.localeParent = index(`${textsTableName}_locale_parent`).on( - cols.locale, - cols.parent, - ) - } - - return config - }) - - adapter.tables[textsTableName] = textsTable - - adapter.relations[`relations_${textsTableName}`] = relations(textsTable, ({ one }) => ({ - parent: one(table, { - fields: [textsTable.parent], - references: [table.id], - relationName: '_texts', - }), - })) - } - - if (hasManyNumberField) { - const numbersTableName = `${rootTableName}_numbers` - const columns: Record = { - id: serial('id').primaryKey(), - number: numeric('number'), - order: integer('order').notNull(), - parent: parentIDColumnMap[idColType]('parent_id').notNull(), - path: varchar('path').notNull(), - } - - if (hasLocalizedManyNumberField) { - columns.locale = adapter.enums.enum__locales('locale') - } - - numbersTable = adapter.pgSchema.table(numbersTableName, columns, (cols) => { - const config: Record = { - orderParentIdx: index(`${numbersTableName}_order_parent_idx`).on(cols.order, cols.parent), - parentFk: foreignKey({ - name: `${numbersTableName}_parent_fk`, - columns: [cols.parent], - foreignColumns: [table.id], - }).onDelete('cascade'), - } - - if (hasManyNumberField === 'index') { - config.numberIdx = index(`${numbersTableName}_number_idx`).on(cols.number) - } - - if (hasLocalizedManyNumberField) { - config.localeParent = index(`${numbersTableName}_locale_parent`).on( - cols.locale, - cols.parent, - ) - } - - return config - }) - - adapter.tables[numbersTableName] = numbersTable - - adapter.relations[`relations_${numbersTableName}`] = relations(numbersTable, ({ one }) => ({ - parent: one(table, { - fields: [numbersTable.parent], - references: [table.id], - relationName: '_numbers', - }), - })) - } - - if (relationships.size) { - const relationshipColumns: Record = { - id: serial('id').primaryKey(), - order: integer('order'), - parent: parentIDColumnMap[idColType]('parent_id').notNull(), - path: varchar('path').notNull(), - } - - if (hasLocalizedRelationshipField) { - relationshipColumns.locale = adapter.enums.enum__locales('locale') - } - - const relationExtraConfig: BaseExtraConfig = {} - const relationshipsTableName = `${tableName}${adapter.relationshipsSuffix}` - - 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] = parentIDColumnMap[colType](`${formattedRelationTo}_id`) - - relationExtraConfig[`${relationTo}IdFk`] = (cols) => - foreignKey({ - name: `${relationshipsTableName}_${toSnakeCase(relationTo)}_fk`, - columns: [cols[colName]], - foreignColumns: [adapter.tables[formattedRelationTo].id], - }).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, - }) - - relationExtraConfig[indexName] = createIndex({ - name: indexColumns, - indexName, - unique, - }) - }) - - relationshipsTable = adapter.pgSchema.table( - relationshipsTableName, - relationshipColumns, - (cols) => { - const result: Record = Object.entries( - relationExtraConfig, - ).reduce( - (config, [key, func]) => { - config[key] = func(cols) - return config - }, - { - order: index(`${relationshipsTableName}_order_idx`).on(cols.order), - parentFk: foreignKey({ - name: `${relationshipsTableName}_parent_fk`, - columns: [cols.parent], - foreignColumns: [table.id], - }).onDelete('cascade'), - parentIdx: index(`${relationshipsTableName}_parent_idx`).on(cols.parent), - pathIdx: index(`${relationshipsTableName}_path_idx`).on(cols.path), - }, - ) - - if (hasLocalizedRelationshipField) { - result.localeIdx = index(`${relationshipsTableName}_locale_idx`).on(cols.locale) - } - - return result - }, - ) - - adapter.tables[relationshipsTableName] = relationshipsTable - - adapter.relations[`relations_${relationshipsTableName}`] = relations( - relationshipsTable, - ({ one }) => { - const result: Record> = { - parent: one(table, { - fields: [relationshipsTable.parent], - references: [table.id], - relationName: '_rels', - }), - } - - relationships.forEach((relationTo) => { - const relatedTableName = createTableName({ - adapter, - config: adapter.payload.collections[relationTo].config, - throwValidationError: true, - }) - const idColumnName = `${relationTo}ID` - result[idColumnName] = one(adapter.tables[relatedTableName], { - fields: [relationshipsTable[idColumnName]], - references: [adapter.tables[relatedTableName].id], - relationName: relationTo, - }) - }) - - return result - }, - ) - } - } - - adapter.relations[`relations_${tableName}`] = relations(table, ({ many, one }) => { - const result: Record> = {} - - nonLocalizedRelations.forEach(({ type, relationName, target }, key) => { - if (type === 'one') { - result[key] = one(adapter.tables[target], { - fields: [table[key]], - references: [adapter.tables[target].id], - relationName: key, - }) - } - if (type === 'many') { - result[key] = many(adapter.tables[target], { relationName: relationName || key }) - } - }) - - if (hasLocalizedField) { - result._locales = many(localesTable, { relationName: '_locales' }) - } - - if (hasManyTextField) { - result._texts = many(textsTable, { relationName: '_texts' }) - } - - if (hasManyNumberField) { - result._numbers = many(numbersTable, { relationName: '_numbers' }) - } - - if (relationships.size && relationshipsTable) { - result._rels = many(relationshipsTable, { - relationName: '_rels', - }) - } - - return result - }) - - return { - hasLocalizedManyNumberField, - hasLocalizedManyTextField, - hasLocalizedRelationshipField, - hasManyNumberField, - hasManyTextField, - relationsToBuild, - } -} diff --git a/packages/drizzle/src/postgres/schema/buildDrizzleTable.ts b/packages/drizzle/src/postgres/schema/buildDrizzleTable.ts new file mode 100644 index 00000000000..8c6b9252b74 --- /dev/null +++ b/packages/drizzle/src/postgres/schema/buildDrizzleTable.ts @@ -0,0 +1,170 @@ +import type { ForeignKeyBuilder, IndexBuilder } from 'drizzle-orm/pg-core' + +import { + boolean, + foreignKey, + index, + integer, + jsonb, + numeric, + serial, + text, + timestamp, + uniqueIndex, + uuid, + varchar, +} from 'drizzle-orm/pg-core' + +import type { RawColumn, RawTable } from '../../types.js' +import type { BasePostgresAdapter } from '../types.js' + +import { geometryColumn } from './geometryColumn.js' + +const rawColumnBuilderMap: Partial> = { + boolean, + geometry: geometryColumn, + integer, + jsonb, + numeric, + serial, + text, + uuid, + varchar, +} + +export const buildDrizzleTable = ({ + adapter, + rawTable, +}: { + adapter: BasePostgresAdapter + rawTable: RawTable +}) => { + const columns: Record = {} + + for (const [key, column] of Object.entries(rawTable.columns)) { + switch (column.type) { + case 'enum': + if ('locale' in column) { + columns[key] = adapter.enums.enum__locales(column.name) + } else { + adapter.enums[column.enumName] = adapter.pgSchema.enum( + column.enumName, + column.options as [string, ...string[]], + ) + columns[key] = adapter.enums[column.enumName](column.name) + } + break + + case 'timestamp': { + let builder = timestamp(column.name, { + mode: column.mode, + precision: column.precision, + withTimezone: column.withTimezone, + }) + + if (column.defaultNow) { + builder = builder.defaultNow() + } + + columns[key] = builder + break + } + + case 'uuid': { + let builder = uuid(column.name) + + if (column.defaultRandom) { + builder = builder.defaultRandom() + } + + columns[key] = builder + break + } + + default: + columns[key] = rawColumnBuilderMap[column.type](column.name) + break + } + + if (column.reference) { + columns[key].references(() => adapter.tables[column.reference.table][column.reference.name], { + onDelete: column.reference.onDelete, + }) + } + + if (column.primaryKey) { + columns[key].primaryKey() + } + + if (column.notNull) { + columns[key].notNull() + } + + if (typeof column.default !== 'undefined') { + let sanitizedDefault = column.default + + if (column.type === 'geometry' && Array.isArray(column.default)) { + sanitizedDefault = `SRID=4326;POINT(${column.default[0]} ${column.default[1]})` + } + + columns[key].default(sanitizedDefault) + } + + if (column.type === 'geometry') { + if (!adapter.extensions.postgis) { + adapter.extensions.postgis = true + } + } + } + + const extraConfig = (cols: any) => { + const config: Record = {} + + if (rawTable.indexes) { + for (const [key, rawIndex] of Object.entries(rawTable.indexes)) { + let fn: any = index + if (rawIndex.unique) { + fn = uniqueIndex + } + + if (Array.isArray(rawIndex.on)) { + if (rawIndex.on.length) { + config[key] = fn(rawIndex.name).on(...rawIndex.on.map((colName) => cols[colName])) + } + } else { + config[key] = fn(rawIndex.name).on(cols[rawIndex.on]) + } + } + } + + if (rawTable.foreignKeys) { + for (const [key, rawForeignKey] of Object.entries(rawTable.foreignKeys)) { + let builder = foreignKey({ + name: rawForeignKey.name, + columns: rawForeignKey.columns.map((colName) => cols[colName]) as any, + foreignColumns: rawForeignKey.foreignColumns.map( + (column) => adapter.tables[column.table][column.name], + ), + }) + + if (rawForeignKey.onDelete) { + builder = builder.onDelete(rawForeignKey.onDelete) + } + + if (rawForeignKey.onUpdate) { + builder = builder.onDelete(rawForeignKey.onUpdate) + } + + config[key] = builder + } + } + + return config + } + + adapter.tables[rawTable.name] = adapter.pgSchema.table( + rawTable.name, + columns as any, + extraConfig as any, + ) +} diff --git a/packages/drizzle/src/postgres/schema/createIndex.ts b/packages/drizzle/src/postgres/schema/createIndex.ts deleted file mode 100644 index 0a28648432f..00000000000 --- a/packages/drizzle/src/postgres/schema/createIndex.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { index, uniqueIndex } from 'drizzle-orm/pg-core' - -import type { GenericColumn } from '../types.js' - -type CreateIndexArgs = { - indexName: string - name: string | string[] - unique?: boolean -} - -export const createIndex = ({ name, indexName, unique }: CreateIndexArgs) => { - return (table: { [x: string]: GenericColumn }) => { - let columns - if (Array.isArray(name)) { - columns = name - .map((columnName) => table[columnName]) - // exclude fields were included in compound indexes but do not exist on the table - .filter((col) => typeof col !== 'undefined') - } else { - columns = [table[name]] - } - if (unique) { - return uniqueIndex(indexName).on(columns[0], ...columns.slice(1)) - } - return index(indexName).on(columns[0], ...columns.slice(1)) - } -} diff --git a/packages/drizzle/src/postgres/schema/idToUUID.ts b/packages/drizzle/src/postgres/schema/idToUUID.ts deleted file mode 100644 index aa2b81f9452..00000000000 --- a/packages/drizzle/src/postgres/schema/idToUUID.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { FlattenedField } from 'payload' - -export const idToUUID = (fields: FlattenedField[]): FlattenedField[] => - fields.map((field) => { - if ('name' in field && field.name === 'id') { - return { - ...field, - name: '_uuid', - } - } - - return field - }) diff --git a/packages/drizzle/src/postgres/schema/parentIDColumnMap.ts b/packages/drizzle/src/postgres/schema/parentIDColumnMap.ts deleted file mode 100644 index 90b128f44f1..00000000000 --- a/packages/drizzle/src/postgres/schema/parentIDColumnMap.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { integer, numeric, uuid, varchar } from 'drizzle-orm/pg-core' - -import type { IDType } from '../types.js' - -export const parentIDColumnMap: Record< - IDType, - typeof integer | typeof numeric | typeof uuid | typeof varchar -> = { - integer, - numeric, - uuid, - varchar, -} diff --git a/packages/drizzle/src/postgres/schema/setColumnID.ts b/packages/drizzle/src/postgres/schema/setColumnID.ts index 11549a13569..c2515637d3d 100644 --- a/packages/drizzle/src/postgres/schema/setColumnID.ts +++ b/packages/drizzle/src/postgres/schema/setColumnID.ts @@ -1,34 +1,44 @@ -import type { PgColumnBuilder } from 'drizzle-orm/pg-core' -import type { FlattenedField } from 'payload' +import type { SetColumnID } from '../../types.js' -import { numeric, serial, uuid, varchar } from 'drizzle-orm/pg-core' - -import type { BasePostgresAdapter, IDType } from '../types.js' - -type Args = { - adapter: BasePostgresAdapter - columns: Record - fields: FlattenedField[] -} -export const setColumnID = ({ adapter, columns, fields }: Args): IDType => { +export const setColumnID: SetColumnID = ({ adapter, columns, fields }) => { const idField = fields.find((field) => field.name === 'id') if (idField) { if (idField.type === 'number') { - columns.id = numeric('id').primaryKey() + columns.id = { + name: 'id', + type: 'numeric', + primaryKey: true, + } + return 'numeric' } if (idField.type === 'text') { - columns.id = varchar('id').primaryKey() + columns.id = { + name: 'id', + type: 'varchar', + primaryKey: true, + } return 'varchar' } } if (adapter.idType === 'uuid') { - columns.id = uuid('id').defaultRandom().primaryKey() + columns.id = { + name: 'id', + type: 'uuid', + defaultRandom: true, + primaryKey: true, + } + return 'uuid' } - columns.id = serial('id').primaryKey() + columns.id = { + name: 'id', + type: 'serial', + primaryKey: true, + } + return 'integer' } diff --git a/packages/drizzle/src/postgres/schema/withDefault.ts b/packages/drizzle/src/postgres/schema/withDefault.ts deleted file mode 100644 index f5d0cd90ae2..00000000000 --- a/packages/drizzle/src/postgres/schema/withDefault.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { PgColumnBuilder } from 'drizzle-orm/pg-core' -import type { FieldAffectingData } from 'payload' - -export const withDefault = ( - column: PgColumnBuilder, - field: FieldAffectingData, -): PgColumnBuilder => { - if (typeof field.defaultValue === 'undefined' || typeof field.defaultValue === 'function') { - return column - } - - if (typeof field.defaultValue === 'string' && field.defaultValue.includes("'")) { - const escapedString = field.defaultValue.replaceAll("'", "''") - return column.default(escapedString) - } - - if (field.type === 'point' && Array.isArray(field.defaultValue)) { - return column.default(`SRID=4326;POINT(${field.defaultValue[0]} ${field.defaultValue[1]})`) - } - - return column.default(field.defaultValue) -} diff --git a/packages/drizzle/src/schema/build.ts b/packages/drizzle/src/schema/build.ts new file mode 100644 index 00000000000..048112245f7 --- /dev/null +++ b/packages/drizzle/src/schema/build.ts @@ -0,0 +1,705 @@ +import type { FlattenedField } from 'payload' + +import toSnakeCase from 'to-snake-case' + +import type { + DrizzleAdapter, + IDType, + RawColumn, + RawForeignKey, + RawIndex, + RawRelation, + RawTable, + RelationMap, + SetColumnID, +} from '../types.js' + +import { createTableName } from '../createTableName.js' +import { buildIndexName } from '../utilities/buildIndexName.js' +import { traverseFields } from './traverseFields.js' + +type Args = { + adapter: DrizzleAdapter + baseColumns?: Record + /** + * After table is created, run these functions to add extra config to the table + * ie. indexes, multiple columns, etc + */ + baseForeignKeys?: Record + /** + * After table is created, run these functions to add extra config to the table + * ie. indexes, multiple columns, etc + */ + baseIndexes?: Record + buildNumbers?: boolean + buildRelationships?: boolean + disableNotNull: boolean + disableRelsTableUnique?: boolean + disableUnique: boolean + fields: FlattenedField[] + rootRelationships?: Set + rootRelationsToBuild?: RelationMap + rootTableIDColType?: IDType + rootTableName?: string + rootUniqueRelationships?: Set + setColumnID: SetColumnID + tableName: string + timestamps?: boolean + versions: boolean + /** + * Tracks whether or not this table is built + * from the result of a localized array or block field at some point + */ + withinLocalizedArrayOrBlock?: boolean +} + +type Result = { + hasLocalizedManyNumberField: boolean + hasLocalizedManyTextField: boolean + hasLocalizedRelationshipField: boolean + hasManyNumberField: 'index' | boolean + hasManyTextField: 'index' | boolean + relationsToBuild: RelationMap +} + +export const buildTable = ({ + adapter, + baseColumns = {}, + baseForeignKeys = {}, + baseIndexes = {}, + disableNotNull, + disableRelsTableUnique = false, + disableUnique = false, + fields, + rootRelationships, + rootRelationsToBuild, + rootTableIDColType, + rootTableName: incomingRootTableName, + rootUniqueRelationships, + setColumnID, + tableName, + timestamps, + versions, + withinLocalizedArrayOrBlock, +}: Args): Result => { + const isRoot = !incomingRootTableName + const rootTableName = incomingRootTableName || tableName + const columns: Record = baseColumns + const indexes: Record = baseIndexes + + const localesColumns: Record = {} + const localesIndexes: Record = {} + let localesTable: RawTable + let textsTable: RawTable + let numbersTable: RawTable + + // Relationships to the base collection + const relationships: Set = rootRelationships || new Set() + + // Unique relationships to the base collection + const uniqueRelationships: Set = rootUniqueRelationships || new Set() + + let relationshipsTable: RawTable + + // Drizzle relations + const relationsToBuild: RelationMap = new Map() + + const idColType: IDType = setColumnID({ adapter, columns, fields }) + + const { + hasLocalizedField, + hasLocalizedManyNumberField, + hasLocalizedManyTextField, + hasLocalizedRelationshipField, + hasManyNumberField, + hasManyTextField, + } = traverseFields({ + adapter, + columns, + disableNotNull, + disableRelsTableUnique, + disableUnique, + fields, + indexes, + localesColumns, + localesIndexes, + newTableName: tableName, + 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: RawTable = { + name: tableName, + columns, + foreignKeys: baseForeignKeys, + indexes, + } + + adapter.rawTables[tableName] = table + + if (hasLocalizedField || localizedRelations.size) { + const localeTableName = `${tableName}${adapter.localesSuffix}` + 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: `${localeTableName}_locale_parent_id_unique`, + on: ['_locale', '_parentID'], + unique: true, + } + + localesTable = { + name: localeTableName, + columns: localesColumns, + foreignKeys: { + _parentIdFk: { + name: `${localeTableName}_parent_id_fk`, + columns: ['_parentID'], + foreignColumns: [ + { + name: 'id', + table: tableName, + }, + ], + onDelete: 'cascade', + }, + }, + indexes: localesIndexes, + } + + adapter.rawTables[localeTableName] = localesTable + + const localeRelations: Record = { + _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 (isRoot) { + if (hasManyTextField) { + const textsTableName = `${rootTableName}_texts` + + const columns: Record = { + 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: Record = { + orderParentIdx: { + name: `${textsTableName}_order_parent_idx`, + on: ['order', 'parent'], + }, + } + + if (hasManyTextField === 'index') { + textsTableIndexes.text_idx = { + name: `${textsTableName}_text_idx`, + on: 'text', + } + } + + if (hasLocalizedManyTextField) { + textsTableIndexes.localeParent = { + name: `${textsTableName}_locale_parent`, + on: ['locale', 'parent'], + } + } + + textsTable = { + name: textsTableName, + columns, + foreignKeys: { + parentFk: { + name: `${textsTableName}_parent_fk`, + 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` + const columns: Record = { + 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: Record = { + orderParentIdx: { name: `${numbersTableName}_order_parent_idx`, on: ['order', 'parent'] }, + } + + if (hasManyNumberField === 'index') { + numbersTableIndexes.numberIdx = { + name: `${numbersTableName}_number_idx`, + on: 'number', + } + } + + if (hasLocalizedManyNumberField) { + numbersTableIndexes.localeParent = { + name: `${numbersTableName}_locale_parent`, + on: ['locale', 'parent'], + } + } + + numbersTable = { + name: numbersTableName, + columns, + foreignKeys: { + parentFk: { + name: `${numbersTableName}_parent_fk`, + 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: Record = { + 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: Record = { + order: { + name: `${relationshipsTableName}_order_idx`, + on: 'order', + }, + parentIdx: { + name: `${relationshipsTableName}_parent_idx`, + on: 'parent', + }, + pathIdx: { + name: `${relationshipsTableName}_path_idx`, + on: 'path', + }, + } + + if (hasLocalizedRelationshipField) { + relationshipIndexes.localeIdx = { + name: `${relationshipsTableName}_locale_idx`, + on: 'locale', + } + } + + const relationshipForeignKeys: Record = { + parentFk: { + name: `${relationshipsTableName}_parent_fk`, + 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: 'integer' | 'numeric' | 'uuid' | 'varchar' = + 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: `${relationshipsTableName}_${toSnakeCase(relationTo)}_fk`, + 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: Record = { + 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: Record = {} + + 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, + } +} diff --git a/packages/drizzle/src/schema/buildDrizzleRelations.ts b/packages/drizzle/src/schema/buildDrizzleRelations.ts new file mode 100644 index 00000000000..54f5a0009d3 --- /dev/null +++ b/packages/drizzle/src/schema/buildDrizzleRelations.ts @@ -0,0 +1,40 @@ +import type { Relation } from 'drizzle-orm' + +import { relations } from 'drizzle-orm' + +import type { DrizzleAdapter } from '../types.js' + +export const buildDrizzleRelations = ({ adapter }: { adapter: DrizzleAdapter }) => { + for (const tableName in adapter.rawRelations) { + const rawRelations = adapter.rawRelations[tableName] + + adapter.relations[`relations_${tableName}`] = relations( + adapter.tables[tableName], + ({ many, one }) => { + const result: Record> = {} + + for (const key in rawRelations) { + const relation = rawRelations[key] + + if (relation.type === 'one') { + result[key] = one(adapter.tables[relation.to], { + fields: relation.fields.map( + (field) => adapter.tables[field.table][field.name], + ) as any, + references: relation.references.map( + (reference) => adapter.tables[relation.to][reference], + ), + relationName: relation.relationName, + }) + } else { + result[key] = many(adapter.tables[relation.to], { + relationName: relation.relationName, + }) + } + } + + return result + }, + ) + } +} diff --git a/packages/drizzle/src/schema/buildRawSchema.ts b/packages/drizzle/src/schema/buildRawSchema.ts new file mode 100644 index 00000000000..6c6f0cf3510 --- /dev/null +++ b/packages/drizzle/src/schema/buildRawSchema.ts @@ -0,0 +1,120 @@ +import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload' +import toSnakeCase from 'to-snake-case' + +import type { DrizzleAdapter, RawIndex, SetColumnID } from '../types.js' + +import { createTableName } from '../createTableName.js' +import { buildTable } from './build.js' + +/** + * Builds abstract Payload SQL schema + */ +export const buildRawSchema = ({ + adapter, + setColumnID, +}: { + adapter: DrizzleAdapter + setColumnID: SetColumnID +}) => { + adapter.payload.config.collections.forEach((collection) => { + createTableName({ + adapter, + config: collection, + }) + + if (collection.versions) { + createTableName({ + adapter, + config: collection, + versions: true, + versionsCustomName: true, + }) + } + }) + + adapter.payload.config.collections.forEach((collection) => { + const tableName = adapter.tableNameMap.get(toSnakeCase(collection.slug)) + const config = adapter.payload.config + + const baseIndexes: Record = {} + + if (collection.upload.filenameCompoundIndex) { + const indexName = `${tableName}_filename_compound_idx` + + baseIndexes.filename_compound_index = { + name: indexName, + on: collection.upload.filenameCompoundIndex.map((f) => f), + unique: true, + } + } + + buildTable({ + adapter, + disableNotNull: !!collection?.versions?.drafts, + disableUnique: false, + fields: collection.flattenedFields, + setColumnID, + tableName, + timestamps: collection.timestamps, + versions: false, + }) + + if (collection.versions) { + const versionsTableName = adapter.tableNameMap.get( + `_${toSnakeCase(collection.slug)}${adapter.versionsSuffix}`, + ) + const versionFields = buildVersionCollectionFields(config, collection, true) + + buildTable({ + adapter, + disableNotNull: !!collection.versions?.drafts, + disableUnique: true, + fields: versionFields, + setColumnID, + tableName: versionsTableName, + timestamps: true, + versions: true, + }) + } + }) + + adapter.payload.config.globals.forEach((global) => { + const tableName = createTableName({ + adapter, + config: global, + }) + + buildTable({ + adapter, + disableNotNull: !!global?.versions?.drafts, + disableUnique: false, + fields: global.flattenedFields, + setColumnID, + tableName, + timestamps: false, + versions: false, + }) + + if (global.versions) { + const versionsTableName = createTableName({ + adapter, + config: global, + versions: true, + versionsCustomName: true, + }) + const config = adapter.payload.config + const versionFields = buildVersionGlobalFields(config, global, true) + + buildTable({ + adapter, + disableNotNull: !!global.versions?.drafts, + disableUnique: true, + fields: versionFields, + setColumnID, + tableName: versionsTableName, + timestamps: true, + versions: true, + }) + } + }) +} diff --git a/packages/db-sqlite/src/schema/idToUUID.ts b/packages/drizzle/src/schema/idToUUID.ts similarity index 100% rename from packages/db-sqlite/src/schema/idToUUID.ts rename to packages/drizzle/src/schema/idToUUID.ts diff --git a/packages/drizzle/src/postgres/schema/traverseFields.ts b/packages/drizzle/src/schema/traverseFields.ts similarity index 61% rename from packages/drizzle/src/postgres/schema/traverseFields.ts rename to packages/drizzle/src/schema/traverseFields.ts index a1a441a7adc..9cd8161cfc2 100644 --- a/packages/drizzle/src/postgres/schema/traverseFields.ts +++ b/packages/drizzle/src/schema/traverseFields.ts @@ -1,65 +1,49 @@ -import type { Relation } from 'drizzle-orm' -import type { IndexBuilder, PgColumnBuilder } from 'drizzle-orm/pg-core' import type { FlattenedField } from 'payload' -import { relations } from 'drizzle-orm' -import { - boolean, - foreignKey, - index, - integer, - jsonb, - numeric, - PgNumericBuilder, - PgUUIDBuilder, - PgVarcharBuilder, - text, - timestamp, - varchar, -} from 'drizzle-orm/pg-core' import { InvalidConfiguration } from 'payload' import { fieldAffectsData, fieldIsVirtual, optionIsObject } from 'payload/shared' import toSnakeCase from 'to-snake-case' import type { - BaseExtraConfig, - BasePostgresAdapter, - GenericColumns, + DrizzleAdapter, IDType, + RawColumn, + RawForeignKey, + RawIndex, + RawRelation, RelationMap, + SetColumnID, } from '../types.js' -import { createTableName } from '../../createTableName.js' -import { buildIndexName } from '../../utilities/buildIndexName.js' -import { hasLocalesTable } from '../../utilities/hasLocalesTable.js' -import { validateExistingBlockIsIdentical } from '../../utilities/validateExistingBlockIsIdentical.js' +import { createTableName } from '../createTableName.js' +import { buildIndexName } from '../utilities/buildIndexName.js' +import { hasLocalesTable } from '../utilities/hasLocalesTable.js' +import { validateExistingBlockIsIdentical } from '../utilities/validateExistingBlockIsIdentical.js' import { buildTable } from './build.js' -import { createIndex } from './createIndex.js' -import { geometryColumn } from './geometryColumn.js' import { idToUUID } from './idToUUID.js' -import { parentIDColumnMap } from './parentIDColumnMap.js' import { withDefault } from './withDefault.js' type Args = { - adapter: BasePostgresAdapter + adapter: DrizzleAdapter columnPrefix?: string - columns: Record + columns: Record disableNotNull: boolean disableRelsTableUnique?: boolean disableUnique?: boolean fieldPrefix?: string fields: FlattenedField[] forceLocalized?: boolean - indexes: Record IndexBuilder> - localesColumns: Record - localesIndexes: Record IndexBuilder> + indexes: Record + localesColumns: Record + localesIndexes: Record newTableName: string parentTableName: string relationships: Set relationsToBuild: RelationMap rootRelationsToBuild?: RelationMap - rootTableIDColType: string + rootTableIDColType: IDType rootTableName: string + setColumnID: SetColumnID uniqueRelationships: Set versions: boolean /** @@ -98,6 +82,7 @@ export const traverseFields = ({ rootRelationsToBuild, rootTableIDColType, rootTableName, + setColumnID, uniqueRelationships, versions, withinLocalizedArrayOrBlock, @@ -111,14 +96,11 @@ export const traverseFields = ({ let hasLocalizedManyNumberField = false let parentIDColType: IDType = 'integer' - if (columns.id instanceof PgUUIDBuilder) { - parentIDColType = 'uuid' - } - if (columns.id instanceof PgNumericBuilder) { - parentIDColType = 'numeric' - } - if (columns.id instanceof PgVarcharBuilder) { - parentIDColType = 'varchar' + + const idColumn = columns.id + + if (idColumn && ['numeric', 'text', 'uuid', 'varchar'].includes(idColumn.type)) { + parentIDColType = idColumn.type as IDType } fields.forEach((field) => { @@ -168,11 +150,11 @@ export const traverseFields = ({ const indexName = buildIndexName({ name: `${newTableName}_${columnName}`, adapter }) - targetIndexes[indexName] = createIndex({ - name: field.localized ? [fieldName, '_locale'] : fieldName, - indexName, + targetIndexes[indexName] = { + name: indexName, + on: field.localized ? [fieldName, '_locale'] : fieldName, unique, - }) + } } switch (field.type) { @@ -188,20 +170,42 @@ export const traverseFields = ({ versionsCustomName: versions, }) - const baseColumns: Record = { - _order: integer('_order').notNull(), - _parentID: parentIDColumnMap[parentIDColType]('_parent_id').notNull(), + const baseColumns: Record = { + _order: { + name: '_order', + type: 'integer', + notNull: true, + }, + _parentID: { + name: '_parent_id', + type: parentIDColType, + notNull: true, + }, } - const baseExtraConfig: BaseExtraConfig = { - _orderIdx: (cols) => index(`${arrayTableName}_order_idx`).on(cols._order), - _parentIDFk: (cols) => - foreignKey({ - name: `${arrayTableName}_parent_id_fk`, - columns: [cols['_parentID']], - foreignColumns: [adapter.tables[parentTableName].id], - }).onDelete('cascade'), - _parentIDIdx: (cols) => index(`${arrayTableName}_parent_id_idx`).on(cols._parentID), + const baseIndexes: Record = { + _orderIdx: { + name: `${arrayTableName}_order_idx`, + on: ['_order'], + }, + _parentIDIdx: { + name: `${arrayTableName}_parent_id_idx`, + on: '_parentID', + }, + } + + const baseForeignKeys: Record = { + _parentIDFk: { + name: `${arrayTableName}_parent_id_fk`, + columns: ['_parentID'], + foreignColumns: [ + { + name: 'id', + table: parentTableName, + }, + ], + onDelete: 'cascade', + }, } const isLocalized = @@ -210,9 +214,17 @@ export const traverseFields = ({ forceLocalized if (isLocalized) { - baseColumns._locale = adapter.enums.enum__locales('_locale').notNull() - baseExtraConfig._localeIdx = (cols) => - index(`${arrayTableName}_locale_idx`).on(cols._locale) + baseColumns._locale = { + name: '_locale', + type: 'enum', + locale: true, + notNull: true, + } + + baseIndexes._localeIdx = { + name: `${arrayTableName}_locale_idx`, + on: '_locale', + } } const { @@ -225,7 +237,8 @@ export const traverseFields = ({ } = buildTable({ adapter, baseColumns, - baseExtraConfig, + baseForeignKeys, + baseIndexes, disableNotNull: disableNotNullFromHere, disableRelsTableUnique: true, disableUnique, @@ -235,6 +248,7 @@ export const traverseFields = ({ rootTableIDColType, rootTableName, rootUniqueRelationships: uniqueRelationships, + setColumnID, tableName: arrayTableName, versions, withinLocalizedArrayOrBlock: isLocalized, @@ -270,42 +284,59 @@ export const traverseFields = ({ target: arrayTableName, }) - adapter.relations[`relations_${arrayTableName}`] = relations( - adapter.tables[arrayTableName], - ({ many, one }) => { - const result: Record> = { - _parentID: one(adapter.tables[parentTableName], { - fields: [adapter.tables[arrayTableName]._parentID], - references: [adapter.tables[parentTableName].id], - relationName: fieldName, - }), - } + const arrayRelations: Record = { + _parentID: { + type: 'one', + fields: [ + { + name: '_parentID', + table: arrayTableName, + }, + ], + references: ['id'], + relationName: fieldName, + to: parentTableName, + }, + } - if (hasLocalesTable(field.fields)) { - result._locales = many(adapter.tables[`${arrayTableName}${adapter.localesSuffix}`], { - relationName: '_locales', - }) + if (hasLocalesTable(field.fields)) { + 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, } + } - subRelationsToBuild.forEach(({ type, localized, target }, key) => { - if (type === 'one') { - const arrayWithLocalized = localized - ? `${arrayTableName}${adapter.localesSuffix}` - : arrayTableName - result[key] = one(adapter.tables[target], { - fields: [adapter.tables[arrayWithLocalized][key]], - references: [adapter.tables[target].id], - relationName: key, - }) - } - if (type === 'many') { - result[key] = many(adapter.tables[target], { relationName: key }) - } - }) + if (type === 'many') { + arrayRelations[key] = { + type: 'many', + relationName: key, + to: target, + } + } + }) - return result - }, - ) + adapter.rawRelations[arrayTableName] = arrayRelations break } @@ -321,23 +352,52 @@ export const traverseFields = ({ throwValidationError, versionsCustomName: versions, }) - if (!adapter.tables[blockTableName]) { - const baseColumns: Record = { - _order: integer('_order').notNull(), - _parentID: parentIDColumnMap[rootTableIDColType]('_parent_id').notNull(), - _path: text('_path').notNull(), + if (!adapter.rawTables[blockTableName]) { + const baseColumns: Record = { + _order: { + name: '_order', + type: 'integer', + notNull: true, + }, + _parentID: { + name: '_parent_id', + type: rootTableIDColType, + notNull: true, + }, + _path: { + name: '_path', + type: 'text', + notNull: true, + }, } - const baseExtraConfig: BaseExtraConfig = { - _orderIdx: (cols) => index(`${blockTableName}_order_idx`).on(cols._order), - _parentIdFk: (cols) => - foreignKey({ - name: `${blockTableName}_parent_id_fk`, - columns: [cols._parentID], - foreignColumns: [adapter.tables[rootTableName].id], - }).onDelete('cascade'), - _parentIDIdx: (cols) => index(`${blockTableName}_parent_id_idx`).on(cols._parentID), - _pathIdx: (cols) => index(`${blockTableName}_path_idx`).on(cols._path), + const baseIndexes: Record = { + _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 = { + _parentIdFk: { + name: `${blockTableName}_parent_id_fk`, + columns: ['_parentID'], + foreignColumns: [ + { + name: 'id', + table: rootTableName, + }, + ], + onDelete: 'cascade', + }, } const isLocalized = @@ -346,9 +406,17 @@ export const traverseFields = ({ forceLocalized if (isLocalized) { - baseColumns._locale = adapter.enums.enum__locales('_locale').notNull() - baseExtraConfig._localeIdx = (cols) => - index(`${blockTableName}_locale_idx`).on(cols._locale) + baseColumns._locale = { + name: '_locale', + type: 'enum', + locale: true, + notNull: true, + } + + baseIndexes._localeIdx = { + name: `${blockTableName}_locale_idx`, + on: '_locale', + } } const { @@ -361,7 +429,8 @@ export const traverseFields = ({ } = buildTable({ adapter, baseColumns, - baseExtraConfig, + baseForeignKeys, + baseIndexes, disableNotNull: disableNotNullFromHere, disableRelsTableUnique: true, disableUnique, @@ -371,6 +440,7 @@ export const traverseFields = ({ rootTableIDColType, rootTableName, rootUniqueRelationships: uniqueRelationships, + setColumnID, tableName: blockTableName, versions, withinLocalizedArrayOrBlock: isLocalized, @@ -400,50 +470,66 @@ export const traverseFields = ({ } } - adapter.relations[`relations_${blockTableName}`] = relations( - adapter.tables[blockTableName], - ({ many, one }) => { - const result: Record> = { - _parentID: one(adapter.tables[rootTableName], { - fields: [adapter.tables[blockTableName]._parentID], - references: [adapter.tables[rootTableName].id], - relationName: `_blocks_${block.slug}`, - }), + const blockRelations: Record = { + _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 (hasLocalesTable(block.fields)) { - result._locales = many( - adapter.tables[`${blockTableName}${adapter.localesSuffix}`], - { relationName: '_locales' }, - ) + if (type === 'many') { + blockRelations[key] = { + type: 'many', + relationName: key, + to: target, } + } + }) - subRelationsToBuild.forEach(({ type, localized, target }, key) => { - if (type === 'one') { - const blockWithLocalized = localized - ? `${blockTableName}${adapter.localesSuffix}` - : blockTableName - result[key] = one(adapter.tables[target], { - fields: [adapter.tables[blockWithLocalized][key]], - references: [adapter.tables[target].id], - relationName: key, - }) - } - if (type === 'many') { - result[key] = many(adapter.tables[target], { relationName: key }) - } - }) - - return result - }, - ) + adapter.rawRelations[blockTableName] = blockRelations } else if (process.env.NODE_ENV !== 'production' && !versions) { validateExistingBlockIsIdentical({ block, localized: field.localized, rootTableName, - table: adapter.tables[blockTableName], - tableLocales: adapter.tables[`${blockTableName}${adapter.localesSuffix}`], + table: adapter.rawTables[blockTableName], + tableLocales: adapter.rawTables[`${blockTableName}${adapter.localesSuffix}`], }) } // blocks relationships are defined from the collection or globals table down to the block, bypassing any subBlocks @@ -458,26 +544,43 @@ export const traverseFields = ({ break } case 'checkbox': { - targetTable[fieldName] = withDefault(boolean(columnName), field) + targetTable[fieldName] = withDefault( + { + name: columnName, + type: 'boolean', + }, + field, + ) + break } case 'code': case 'email': case 'textarea': { - targetTable[fieldName] = withDefault(varchar(columnName), field) + targetTable[fieldName] = withDefault( + { + name: columnName, + type: 'varchar', + }, + field, + ) + break } case 'date': { targetTable[fieldName] = withDefault( - timestamp(columnName, { + { + name: columnName, + type: 'timestamp', mode: 'string', precision: 3, withTimezone: true, - }), + }, field, ) + break } @@ -511,6 +614,7 @@ export const traverseFields = ({ rootRelationsToBuild, rootTableIDColType, rootTableName, + setColumnID, uniqueRelationships, versions, withinLocalizedArrayOrBlock: withinLocalizedArrayOrBlock || field.localized, @@ -539,7 +643,14 @@ export const traverseFields = ({ case 'json': case 'richText': { - targetTable[fieldName] = withDefault(jsonb(columnName), field) + targetTable[fieldName] = withDefault( + { + name: columnName, + type: 'jsonb', + }, + field, + ) + break } @@ -566,16 +677,27 @@ export const traverseFields = ({ ) } } else { - targetTable[fieldName] = withDefault(numeric(columnName), field) + targetTable[fieldName] = withDefault( + { + name: columnName, + type: 'numeric', + }, + field, + ) } + break } case 'point': { - targetTable[fieldName] = withDefault(geometryColumn(columnName), field) - if (!adapter.extensions.postgis) { - adapter.extensions.postgis = true - } + targetTable[fieldName] = withDefault( + { + name: columnName, + type: 'geometry', + }, + field, + ) + break } @@ -590,16 +712,13 @@ export const traverseFields = ({ throwValidationError, }) - adapter.enums[enumName] = adapter.pgSchema.enum( - enumName, - field.options.map((option) => { - if (optionIsObject(option)) { - return option.value - } + const options = field.options.map((option) => { + if (optionIsObject(option)) { + return option.value + } - return option - }) as [string, ...string[]], - ) + return option + }) if (field.type === 'select' && field.hasMany) { const selectTableName = createTableName({ @@ -610,21 +729,56 @@ export const traverseFields = ({ throwValidationError, versionsCustomName: versions, }) - const baseColumns: Record = { - order: integer('order').notNull(), - parent: parentIDColumnMap[parentIDColType]('parent_id').notNull(), - value: adapter.enums[enumName]('value'), + + const baseColumns: Record = { + 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: Record = { + orderIdx: { + name: `${selectTableName}_order_idx`, + on: 'order', + }, + parentIdx: { + name: `${selectTableName}_parent_idx`, + on: 'parent', + }, } - const baseExtraConfig: BaseExtraConfig = { - orderIdx: (cols) => index(`${selectTableName}_order_idx`).on(cols.order), - parentFk: (cols) => - foreignKey({ - name: `${selectTableName}_parent_fk`, - columns: [cols.parent], - foreignColumns: [adapter.tables[parentTableName].id], - }).onDelete('cascade'), - parentIdx: (cols) => index(`${selectTableName}_parent_idx`).on(cols.parent), + const baseForeignKeys: Record = { + parentFk: { + name: `${selectTableName}_parent_fk`, + columns: ['parent'], + foreignColumns: [ + { + name: 'id', + table: parentTableName, + }, + ], + onDelete: 'cascade', + }, } const isLocalized = @@ -633,23 +787,36 @@ export const traverseFields = ({ forceLocalized if (isLocalized) { - baseColumns.locale = adapter.enums.enum__locales('locale').notNull() - baseExtraConfig.localeIdx = (cols) => - index(`${selectTableName}_locale_idx`).on(cols.locale) + baseColumns.locale = { + name: 'locale', + type: 'enum', + locale: true, + notNull: true, + } + + baseIndexes.localeIdx = { + name: `${selectTableName}_locale_idx`, + on: 'locale', + } } if (field.index) { - baseExtraConfig.value = (cols) => index(`${selectTableName}_value_idx`).on(cols.value) + baseIndexes.value = { + name: `${selectTableName}_value_idx`, + on: 'value', + } } buildTable({ adapter, baseColumns, - baseExtraConfig, + baseForeignKeys, + baseIndexes, disableNotNull, disableUnique, fields: [], rootTableName, + setColumnID, tableName: selectTableName, versions, }) @@ -661,18 +828,30 @@ export const traverseFields = ({ target: selectTableName, }) - adapter.relations[`relations_${selectTableName}`] = relations( - adapter.tables[selectTableName], - ({ one }) => ({ - parent: one(adapter.tables[parentTableName], { - fields: [adapter.tables[selectTableName].parent], - references: [adapter.tables[parentTableName].id], - relationName: fieldName, - }), - }), - ) + adapter.rawRelations[selectTableName] = { + parent: { + type: 'one', + fields: [ + { + name: 'parent', + table: selectTableName, + }, + ], + references: ['id'], + relationName: fieldName, + to: parentTableName, + }, + } } else { - targetTable[fieldName] = withDefault(adapter.enums[enumName](columnName), field) + targetTable[fieldName] = withDefault( + { + name: columnName, + type: 'enum', + enumName, + options, + }, + field, + ) } break } @@ -698,7 +877,7 @@ export const traverseFields = ({ const tableName = adapter.tableNameMap.get(toSnakeCase(field.relationTo)) // get the id type of the related collection - let colType = adapter.idType === 'uuid' ? 'uuid' : 'integer' + let colType: IDType = adapter.idType === 'uuid' ? 'uuid' : 'integer' const relatedCollectionCustomID = relationshipConfig.fields.find( (field) => fieldAffectsData(field) && field.name === 'id', ) @@ -710,10 +889,15 @@ export const traverseFields = ({ } // make the foreign key column for relationship using the correct id column type - targetTable[fieldName] = parentIDColumnMap[colType](`${columnName}_id`).references( - () => adapter.tables[tableName].id, - { onDelete: 'set null' }, - ) + targetTable[fieldName] = { + name: `${columnName}_id`, + type: colType, + reference: { + name: 'id', + onDelete: 'set null', + table: tableName, + }, + } // add relationship to table relationsToBuild.set(fieldName, { @@ -724,7 +908,7 @@ export const traverseFields = ({ // add notNull when not required if (!disableNotNull && field.required && !field.admin?.condition) { - targetTable[fieldName].notNull() + targetTable[fieldName].notNull = true } break } @@ -761,7 +945,13 @@ export const traverseFields = ({ ) } } else { - targetTable[fieldName] = withDefault(varchar(columnName), field) + targetTable[fieldName] = withDefault( + { + name: columnName, + type: 'varchar', + }, + field, + ) } break } @@ -779,7 +969,7 @@ export const traverseFields = ({ field.required && !condition ) { - targetTable[fieldName].notNull() + targetTable[fieldName].notNull = true } }) diff --git a/packages/drizzle/src/schema/withDefault.ts b/packages/drizzle/src/schema/withDefault.ts new file mode 100644 index 00000000000..03d09453d04 --- /dev/null +++ b/packages/drizzle/src/schema/withDefault.ts @@ -0,0 +1,22 @@ +import type { FieldAffectingData } from 'payload' + +import type { RawColumn } from '../types.js' + +export const withDefault = (column: RawColumn, field: FieldAffectingData): RawColumn => { + if (typeof field.defaultValue === 'undefined' || typeof field.defaultValue === 'function') { + return column + } + + if (typeof field.defaultValue === 'string' && field.defaultValue.includes("'")) { + const escapedString = field.defaultValue.replaceAll("'", "''") + return { + ...column, + default: escapedString, + } + } + + return { + ...column, + default: field.defaultValue, + } +} diff --git a/packages/drizzle/src/types.ts b/packages/drizzle/src/types.ts index dc940b1d37a..e22d376faf9 100644 --- a/packages/drizzle/src/types.ts +++ b/packages/drizzle/src/types.ts @@ -11,10 +11,22 @@ import type { } from 'drizzle-orm' import type { LibSQLDatabase } from 'drizzle-orm/libsql' import type { NodePgDatabase, NodePgQueryResultHKT } from 'drizzle-orm/node-postgres' -import type { PgColumn, PgTable, PgTransaction } from 'drizzle-orm/pg-core' +import type { + PgColumn, + PgTable, + PgTransaction, + Precision, + UpdateDeleteAction, +} from 'drizzle-orm/pg-core' import type { SQLiteColumn, SQLiteTable, SQLiteTransaction } from 'drizzle-orm/sqlite-core' import type { Result } from 'drizzle-orm/sqlite-core/session' -import type { BaseDatabaseAdapter, MigrationData, Payload, PayloadRequest } from 'payload' +import type { + BaseDatabaseAdapter, + FlattenedField, + MigrationData, + Payload, + PayloadRequest, +} from 'payload' import type { BuildQueryJoinAliases } from './queries/buildQuery.js' @@ -157,6 +169,129 @@ export type CreateJSONQueryArgs = { value: boolean | number | string } +/** + * Abstract relation link + */ +export type RawRelation = + | { + fields: { name: string; table: string }[] + references: string[] + relationName?: string + to: string + type: 'one' + } + | { + relationName?: string + to: string + type: 'many' + } + +/** + * Abstract SQL table that later gets converted by database specific implementation to Drizzle + */ +export type RawTable = { + columns: Record + foreignKeys?: Record + indexes?: Record + name: string +} + +/** + * Abstract SQL foreign key that later gets converted by database specific implementation to Drizzle + */ +export type RawForeignKey = { + columns: string[] + foreignColumns: { name: string; table: string }[] + name: string + onDelete?: UpdateDeleteAction + onUpdate?: UpdateDeleteAction +} + +/** + * Abstract SQL index that later gets converted by database specific implementation to Drizzle + */ +export type RawIndex = { + name: string + on: string | string[] + unique?: boolean +} + +/** + * Abstract SQL column that later gets converted by database specific implementation to Drizzle + */ +export type BaseRawColumn = { + default?: any + name: string + notNull?: boolean + primaryKey?: boolean + reference?: { + name: string + onDelete: UpdateDeleteAction + table: string + } +} + +/** + * Postgres: native timestamp type + * SQLite: text column, defaultNow achieved through strftime('%Y-%m-%dT%H:%M:%fZ', 'now'). withTimezone/precision have no any effect. + */ +export type TimestampRawColumn = { + defaultNow?: boolean + mode: 'date' | 'string' + precision: Precision + type: 'timestamp' + withTimezone?: boolean +} & BaseRawColumn + +/** + * Postgres: native UUID type and db lavel defaultRandom + * SQLite: text type and defaultRandom in the app level + */ +export type UUIDRawColumn = { + defaultRandom?: boolean + type: 'uuid' +} & BaseRawColumn + +/** + * Accepts either `locale: true` to have options from locales or `options` string array + * Postgres: native enums + * SQLite: text column with checks. + */ +export type EnumRawColumn = ( + | { + enumName: string + options: string[] + type: 'enum' + } + | { + locale: true + type: 'enum' + } +) & + BaseRawColumn + +export type RawColumn = + | ({ + type: 'boolean' | 'geometry' | 'integer' | 'jsonb' | 'numeric' | 'serial' | 'text' | 'varchar' + } & BaseRawColumn) + | EnumRawColumn + | TimestampRawColumn + | UUIDRawColumn + +export type IDType = 'integer' | 'numeric' | 'text' | 'uuid' | 'varchar' + +export type SetColumnID = (args: { + adapter: DrizzleAdapter + columns: Record + fields: FlattenedField[] +}) => IDType + +export type BuildDrizzleTable = (args: { + adapter: T + locales: string[] + rawTable: RawTable +}) => void + export interface DrizzleAdapter extends BaseDatabaseAdapter { convertPathToJSONTraversal?: (incomingSegments: string[]) => string countDistinct: CountDistinct @@ -184,7 +319,10 @@ export interface DrizzleAdapter extends BaseDatabaseAdapter { logger: DrizzleConfig['logger'] operators: Operators push: boolean + rawRelations: Record> + rawTables: Record rejectInitializing: () => void + relations: Record relationshipsSuffix?: string requireDrizzleKit: RequireDrizzleKit @@ -204,3 +342,13 @@ export interface DrizzleAdapter extends BaseDatabaseAdapter { transactionOptions: unknown versionsSuffix?: string } + +export type RelationMap = Map< + string, + { + localized: boolean + relationName?: string + target: string + type: 'many' | 'one' + } +> diff --git a/packages/drizzle/src/utilities/executeSchemaHooks.ts b/packages/drizzle/src/utilities/executeSchemaHooks.ts index 4c7f22d0778..159659e5997 100644 --- a/packages/drizzle/src/utilities/executeSchemaHooks.ts +++ b/packages/drizzle/src/utilities/executeSchemaHooks.ts @@ -27,9 +27,9 @@ type Args = { } export const executeSchemaHooks = async ({ type, adapter }: Args): Promise => { - for (const hook of adapter[type]) { + for (const hook of (adapter as unknown as Adapter)[type]) { const result = await hook({ - adapter, + adapter: adapter as unknown as Adapter, extendTable: extendDrizzleTable, schema: { enums: adapter.enums, diff --git a/packages/drizzle/src/utilities/validateExistingBlockIsIdentical.ts b/packages/drizzle/src/utilities/validateExistingBlockIsIdentical.ts index 8a92876774c..32309020a15 100644 --- a/packages/drizzle/src/utilities/validateExistingBlockIsIdentical.ts +++ b/packages/drizzle/src/utilities/validateExistingBlockIsIdentical.ts @@ -3,12 +3,14 @@ import type { Block, Field } from 'payload' import { InvalidConfiguration } from 'payload' import { fieldAffectsData, fieldHasSubFields, tabHasName } from 'payload/shared' +import type { RawTable } from '../types.js' + type Args = { block: Block localized: boolean rootTableName: string - table: Record - tableLocales?: Record + table: RawTable + tableLocales?: RawTable } const getFlattenedFieldNames = ( @@ -72,7 +74,7 @@ export const validateExistingBlockIsIdentical = ({ // ensure every field from the config is in the matching table fieldNames.find(({ name, localized }) => { const fieldTable = localized && tableLocales ? tableLocales : table - return Object.keys(fieldTable).indexOf(name) === -1 + return Object.keys(fieldTable.columns).indexOf(name) === -1 }) || // ensure every table column is matched for every field from the config Object.keys(table).find((fieldName) => { @@ -91,7 +93,7 @@ export const validateExistingBlockIsIdentical = ({ ) } - if (Boolean(localized) !== Boolean(table._locale)) { + if (Boolean(localized) !== Boolean(table.columns._locale)) { 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.`, )