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.
This commit is contained in:
Sasha
2024-12-16 17:17:18 +02:00
committed by GitHub
parent c187bff581
commit 727fba7b1c
30 changed files with 1878 additions and 2492 deletions

View File

@@ -167,6 +167,8 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
packageName: '@payloadcms/db-postgres',
payload,
queryDrafts,
rawRelations: {},
rawTables: {},
rejectInitializing,
requireDrizzleKit,
resolveInitializing,

View File

@@ -100,6 +100,8 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
operators,
prodMigrations: args.prodMigrations,
push: args.push,
rawRelations: {},
rawTables: {},
relations: {},
relationshipsSuffix: args.relationshipsSuffix || '_rels',
schema: {},

View File

@@ -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,
const adapter = this as unknown as DrizzleAdapter
buildRawSchema({
adapter,
setColumnID,
})
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
await executeSchemaHooks({ type: 'beforeSchemaInit', adapter: this })
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))
}
for (const tableName in this.rawTables) {
buildDrizzleTable({ adapter, locales, rawTable: this.rawTables[tableName] })
}
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,
})
}
})
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,
})
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)
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 })

View File

@@ -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<string, SQLiteColumnBuilder>
/**
* 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<string>
rootRelationsToBuild?: RelationMap
rootTableIDColType?: IDType
rootTableName?: string
rootUniqueRelationships?: Set<string>
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<string, SQLiteColumnBuilder> = baseColumns
const indexes: Record<string, (cols: GenericColumns) => IndexBuilder> = {}
const localesColumns: Record<string, SQLiteColumnBuilder> = {}
const localesIndexes: Record<string, (cols: GenericColumns) => IndexBuilder> = {}
let localesTable: GenericTable | SQLiteTableWithColumns<any>
let textsTable: GenericTable | SQLiteTableWithColumns<any>
let numbersTable: GenericTable | SQLiteTableWithColumns<any>
// Relationships to the base collection
const relationships: Set<string> = rootRelationships || new Set()
const uniqueRelationships: Set<string> = rootUniqueRelationships || new Set()
let relationshipsTable: GenericTable | SQLiteTableWithColumns<any>
// 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<string, Relation<string>> = {}
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<string, SQLiteColumnBuilder> = {
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<string, ForeignKeyBuilder | IndexBuilder> = {
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<string, SQLiteColumnBuilder> = {
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<string, ForeignKeyBuilder | IndexBuilder> = {
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<string, SQLiteColumnBuilder> = {
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<string, ForeignKeyBuilder | IndexBuilder> = 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<string, Relation<string>> = {
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<string, Relation<string>> = {}
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,
}
}

View File

@@ -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<Record<RawColumn['type'], any>> = {
integer,
numeric,
text,
}
export const buildDrizzleTable: BuildDrizzleTable = ({ adapter, locales, rawTable }) => {
const columns: Record<string, any> = {}
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<string, ForeignKeyBuilder | IndexBuilder> = {}
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)
}

View File

@@ -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))
}
}

View File

@@ -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
}

View File

@@ -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<string, SQLiteColumnBuilder>
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'
}

View File

@@ -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<string, SQLiteColumnBuilder>
disableNotNull: boolean
disableRelsTableUnique?: boolean
disableUnique?: boolean
fieldPrefix?: string
fields: FlattenedField[]
forceLocalized?: boolean
indexes: Record<string, (cols: GenericColumns) => IndexBuilder>
locales: [string, ...string[]]
localesColumns: Record<string, SQLiteColumnBuilder>
localesIndexes: Record<string, (cols: GenericColumns) => IndexBuilder>
newTableName: string
parentTableName: string
relationships: Set<string>
relationsToBuild: RelationMap
rootRelationsToBuild?: RelationMap
rootTableIDColType: IDType
rootTableName: string
uniqueRelationships: Set<string>
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<string, SQLiteColumnBuilder> = {
_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<string, Relation<string>> = {
_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<string, SQLiteColumnBuilder> = {
_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<string, Relation<string>> = {
_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<string, SQLiteColumnBuilder> = {
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,
}
}

View File

@@ -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)
}

View File

@@ -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<string, unknown>
}
type ResolveSchemaType<T> = 'schema' extends keyof T
? T['schema']
: GeneratedDatabaseSchema['schemaUntyped']
type Drizzle = { $client: Client } & LibSQLDatabase<ResolveSchemaType<GeneratedDatabaseSchema>>
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<string, GenericRelation | GenericTable> & Record<string, unknown>
>
drizzle: Drizzle
dropDatabase: DropDatabase
execute: Execute<unknown>
/**
@@ -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<Args, 'idType' | 'logger' | 'migrationDir' | 'pool'>,
DrizzleAdapter {
beginTransaction: (options?: SQLiteTransactionConfig) => Promise<null | number | string>
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

View File

@@ -110,6 +110,8 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj<Verce
poolOptions: args.pool,
prodMigrations: args.prodMigrations,
push: args.push,
rawRelations: {},
rawTables: {},
relations: {},
relationshipsSuffix: args.relationshipsSuffix || '_rels',
schema: {},

View File

@@ -26,6 +26,8 @@ export { migrateStatus } from './migrateStatus.js'
export { operatorMap } from './queries/operatorMap.js'
export type { Operators } from './queries/operatorMap.js'
export { queryDrafts } from './queryDrafts.js'
export { buildDrizzleRelations } from './schema/buildDrizzleRelations.js'
export { buildRawSchema } from './schema/buildRawSchema.js'
export { beginTransaction } from './transactions/beginTransaction.js'
export { commitTransaction } from './transactions/commitTransaction.js'
export { rollbackTransaction } from './transactions/rollbackTransaction.js'

View File

@@ -1,16 +1,22 @@
import type { Init, SanitizedCollectionConfig } from 'payload'
import type { Init } from 'payload'
import { uniqueIndex } from 'drizzle-orm/pg-core'
import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload'
import toSnakeCase from 'to-snake-case'
import type { BasePostgresAdapter } from './types.js'
import type { BaseExtraConfig, BasePostgresAdapter } from './types.js'
import { createTableName } from '../createTableName.js'
import { buildDrizzleRelations } from '../schema/buildDrizzleRelations.js'
import { buildRawSchema } from '../schema/buildRawSchema.js'
import { executeSchemaHooks } from '../utilities/executeSchemaHooks.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: BasePostgresAdapter) {
this.rawRelations = {}
this.rawTables = {}
buildRawSchema({
adapter: this,
setColumnID,
})
await executeSchemaHooks({ type: 'beforeSchemaInit', adapter: this })
if (this.payload.config.localization) {
@@ -20,98 +26,12 @@ export const init: Init = async function init(this: BasePostgresAdapter) {
)
}
this.payload.config.collections.forEach((collection: SanitizedCollectionConfig) => {
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))
}
for (const tableName in this.rawTables) {
buildDrizzleTable({ adapter: this, rawTable: this.rawTables[tableName] })
}
buildTable({
buildDrizzleRelations({
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)
buildTable({
adapter: this,
disableNotNull: !!global.versions?.drafts,
disableUnique: true,
fields: versionFields,
tableName: versionsTableName,
timestamps: true,
versions: true,
})
}
})
await executeSchemaHooks({ type: 'afterSchemaInit', adapter: this })

View File

@@ -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<string, PgColumnBuilder>
/**
* 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<string>
rootRelationsToBuild?: RelationMap
rootTableIDColType?: string
rootTableName?: string
rootUniqueRelationships?: Set<string>
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<string, PgColumnBuilder> = baseColumns
const indexes: Record<string, (cols: GenericColumns) => IndexBuilder> = {}
const localesColumns: Record<string, PgColumnBuilder> = {}
const localesIndexes: Record<string, (cols: GenericColumns) => IndexBuilder> = {}
let localesTable: GenericTable | PgTableWithColumns<any>
let textsTable: GenericTable | PgTableWithColumns<any>
let numbersTable: GenericTable | PgTableWithColumns<any>
// Relationships to the base collection
const relationships: Set<string> = rootRelationships || new Set()
// Unique relationships to the base collection
const uniqueRelationships: Set<string> = rootUniqueRelationships || new Set()
let relationshipsTable: GenericTable | PgTableWithColumns<any>
// 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<string, Relation<string>> = {}
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<string, PgColumnBuilder> = {
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<string, ForeignKeyBuilder | IndexBuilder> = {
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<string, PgColumnBuilder> = {
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<string, ForeignKeyBuilder | IndexBuilder> = {
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<string, PgColumnBuilder> = {
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<string, ForeignKeyBuilder | IndexBuilder> = 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<string, Relation<string>> = {
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<string, Relation<string>> = {}
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,
}
}

View File

@@ -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<Record<RawColumn['type'], any>> = {
boolean,
geometry: geometryColumn,
integer,
jsonb,
numeric,
serial,
text,
uuid,
varchar,
}
export const buildDrizzleTable = ({
adapter,
rawTable,
}: {
adapter: BasePostgresAdapter
rawTable: RawTable
}) => {
const columns: Record<string, any> = {}
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<string, ForeignKeyBuilder | IndexBuilder> = {}
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,
)
}

View File

@@ -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))
}
}

View File

@@ -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
})

View File

@@ -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<string> | typeof numeric<string> | typeof uuid<string> | typeof varchar
> = {
integer,
numeric,
uuid,
varchar,
}

View File

@@ -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<string, PgColumnBuilder>
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'
}

View File

@@ -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)
}

View File

@@ -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<string, RawColumn>
/**
* After table is created, run these functions to add extra config to the table
* ie. indexes, multiple columns, etc
*/
baseForeignKeys?: Record<string, RawForeignKey>
/**
* After table is created, run these functions to add extra config to the table
* ie. indexes, multiple columns, etc
*/
baseIndexes?: Record<string, RawIndex>
buildNumbers?: boolean
buildRelationships?: boolean
disableNotNull: boolean
disableRelsTableUnique?: boolean
disableUnique: boolean
fields: FlattenedField[]
rootRelationships?: Set<string>
rootRelationsToBuild?: RelationMap
rootTableIDColType?: IDType
rootTableName?: string
rootUniqueRelationships?: Set<string>
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<string, RawColumn> = baseColumns
const indexes: Record<string, RawIndex> = baseIndexes
const localesColumns: Record<string, RawColumn> = {}
const localesIndexes: Record<string, RawIndex> = {}
let localesTable: RawTable
let textsTable: RawTable
let numbersTable: RawTable
// Relationships to the base collection
const relationships: Set<string> = rootRelationships || new Set()
// Unique relationships to the base collection
const uniqueRelationships: Set<string> = 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<string, RawRelation> = {
_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<string, RawColumn> = {
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<string, RawIndex> = {
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<string, RawColumn> = {
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<string, RawIndex> = {
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<string, RawColumn> = {
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<string, RawIndex> = {
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<string, RawForeignKey> = {
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<string, RawRelation> = {
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<string, RawRelation> = {}
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,
}
}

View File

@@ -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<string, Relation<string>> = {}
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
},
)
}
}

View File

@@ -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<string, RawIndex> = {}
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,
})
}
})
}

View File

@@ -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<string, PgColumnBuilder>
columns: Record<string, RawColumn>
disableNotNull: boolean
disableRelsTableUnique?: boolean
disableUnique?: boolean
fieldPrefix?: string
fields: FlattenedField[]
forceLocalized?: boolean
indexes: Record<string, (cols: GenericColumns) => IndexBuilder>
localesColumns: Record<string, PgColumnBuilder>
localesIndexes: Record<string, (cols: GenericColumns) => IndexBuilder>
indexes: Record<string, RawIndex>
localesColumns: Record<string, RawColumn>
localesIndexes: Record<string, RawIndex>
newTableName: string
parentTableName: string
relationships: Set<string>
relationsToBuild: RelationMap
rootRelationsToBuild?: RelationMap
rootTableIDColType: string
rootTableIDColType: IDType
rootTableName: string
setColumnID: SetColumnID
uniqueRelationships: Set<string>
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<string, PgColumnBuilder> = {
_order: integer('_order').notNull(),
_parentID: parentIDColumnMap[parentIDColType]('_parent_id').notNull(),
const baseColumns: Record<string, RawColumn> = {
_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({
const baseIndexes: Record<string, RawIndex> = {
_orderIdx: {
name: `${arrayTableName}_order_idx`,
on: ['_order'],
},
_parentIDIdx: {
name: `${arrayTableName}_parent_id_idx`,
on: '_parentID',
},
}
const baseForeignKeys: Record<string, RawForeignKey> = {
_parentIDFk: {
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),
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,21 +284,27 @@ export const traverseFields = ({
target: arrayTableName,
})
adapter.relations[`relations_${arrayTableName}`] = relations(
adapter.tables[arrayTableName],
({ many, one }) => {
const result: Record<string, Relation<string>> = {
_parentID: one(adapter.tables[parentTableName], {
fields: [adapter.tables[arrayTableName]._parentID],
references: [adapter.tables[parentTableName].id],
const arrayRelations: Record<string, RawRelation> = {
_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}`], {
arrayRelations._locales = {
type: 'many',
relationName: '_locales',
})
to: `${arrayTableName}${adapter.localesSuffix}`,
}
}
subRelationsToBuild.forEach(({ type, localized, target }, key) => {
@@ -292,20 +312,31 @@ export const traverseFields = ({
const arrayWithLocalized = localized
? `${arrayTableName}${adapter.localesSuffix}`
: arrayTableName
result[key] = one(adapter.tables[target], {
fields: [adapter.tables[arrayWithLocalized][key]],
references: [adapter.tables[target].id],
arrayRelations[key] = {
type: 'one',
fields: [
{
name: key,
table: arrayWithLocalized,
},
],
references: ['id'],
relationName: key,
})
to: target,
}
}
if (type === 'many') {
result[key] = many(adapter.tables[target], { relationName: key })
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<string, PgColumnBuilder> = {
_order: integer('_order').notNull(),
_parentID: parentIDColumnMap[rootTableIDColType]('_parent_id').notNull(),
_path: text('_path').notNull(),
if (!adapter.rawTables[blockTableName]) {
const baseColumns: Record<string, RawColumn> = {
_order: {
name: '_order',
type: 'integer',
notNull: true,
},
_parentID: {
name: '_parent_id',
type: rootTableIDColType,
notNull: true,
},
_path: {
name: '_path',
type: 'text',
notNull: true,
},
}
const baseExtraConfig: BaseExtraConfig = {
_orderIdx: (cols) => index(`${blockTableName}_order_idx`).on(cols._order),
_parentIdFk: (cols) =>
foreignKey({
const baseIndexes: Record<string, RawIndex> = {
_orderIdx: {
name: `${blockTableName}_order_idx`,
on: '_order',
},
_parentIDIdx: {
name: `${blockTableName}_parent_id_idx`,
on: ['_parentID'],
},
_pathIdx: {
name: `${blockTableName}_path_idx`,
on: '_path',
},
}
const baseForeignKeys: Record<string, RawForeignKey> = {
_parentIdFk: {
name: `${blockTableName}_parent_id_fk`,
columns: [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),
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,22 +470,27 @@ export const traverseFields = ({
}
}
adapter.relations[`relations_${blockTableName}`] = relations(
adapter.tables[blockTableName],
({ many, one }) => {
const result: Record<string, Relation<string>> = {
_parentID: one(adapter.tables[rootTableName], {
fields: [adapter.tables[blockTableName]._parentID],
references: [adapter.tables[rootTableName].id],
const blockRelations: Record<string, RawRelation> = {
_parentID: {
type: 'one',
fields: [
{
name: '_parentID',
table: blockTableName,
},
],
references: ['id'],
relationName: `_blocks_${block.slug}`,
}),
to: rootTableName,
},
}
if (hasLocalesTable(block.fields)) {
result._locales = many(
adapter.tables[`${blockTableName}${adapter.localesSuffix}`],
{ relationName: '_locales' },
)
blockRelations._locales = {
type: 'many',
relationName: '_locales',
to: `${blockTableName}${adapter.localesSuffix}`,
}
}
subRelationsToBuild.forEach(({ type, localized, target }, key) => {
@@ -423,27 +498,38 @@ export const traverseFields = ({
const blockWithLocalized = localized
? `${blockTableName}${adapter.localesSuffix}`
: blockTableName
result[key] = one(adapter.tables[target], {
fields: [adapter.tables[blockWithLocalized][key]],
references: [adapter.tables[target].id],
blockRelations[key] = {
type: 'one',
fields: [
{
name: key,
table: blockWithLocalized,
},
],
references: ['id'],
relationName: key,
})
to: target,
}
}
if (type === 'many') {
result[key] = many(adapter.tables[target], { relationName: key })
blockRelations[key] = {
type: 'many',
relationName: key,
to: target,
}
}
})
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) => {
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({
@@ -610,21 +729,56 @@ export const traverseFields = ({
throwValidationError,
versionsCustomName: versions,
})
const baseColumns: Record<string, PgColumnBuilder> = {
order: integer('order').notNull(),
parent: parentIDColumnMap[parentIDColType]('parent_id').notNull(),
value: adapter.enums[enumName]('value'),
const baseColumns: Record<string, RawColumn> = {
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 baseExtraConfig: BaseExtraConfig = {
orderIdx: (cols) => index(`${selectTableName}_order_idx`).on(cols.order),
parentFk: (cols) =>
foreignKey({
const baseIndexes: Record<string, RawIndex> = {
orderIdx: {
name: `${selectTableName}_order_idx`,
on: 'order',
},
parentIdx: {
name: `${selectTableName}_parent_idx`,
on: 'parent',
},
}
const baseForeignKeys: Record<string, RawForeignKey> = {
parentFk: {
name: `${selectTableName}_parent_fk`,
columns: [cols.parent],
foreignColumns: [adapter.tables[parentTableName].id],
}).onDelete('cascade'),
parentIdx: (cols) => index(`${selectTableName}_parent_idx`).on(cols.parent),
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],
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
}
})

View File

@@ -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,
}
}

View File

@@ -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<string, RawColumn>
foreignKeys?: Record<string, RawForeignKey>
indexes?: Record<string, RawIndex>
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<string, RawColumn>
fields: FlattenedField[]
}) => IDType
export type BuildDrizzleTable<T extends DrizzleAdapter = DrizzleAdapter> = (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<string, Record<string, RawRelation>>
rawTables: Record<string, RawTable>
rejectInitializing: () => void
relations: Record<string, GenericRelation>
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'
}
>

View File

@@ -27,9 +27,9 @@ type Args = {
}
export const executeSchemaHooks = async ({ type, adapter }: Args): Promise<void> => {
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,

View File

@@ -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<string, unknown>
tableLocales?: Record<string, unknown>
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.`,
)