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:
@@ -167,6 +167,8 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
|
|||||||
packageName: '@payloadcms/db-postgres',
|
packageName: '@payloadcms/db-postgres',
|
||||||
payload,
|
payload,
|
||||||
queryDrafts,
|
queryDrafts,
|
||||||
|
rawRelations: {},
|
||||||
|
rawTables: {},
|
||||||
rejectInitializing,
|
rejectInitializing,
|
||||||
requireDrizzleKit,
|
requireDrizzleKit,
|
||||||
resolveInitializing,
|
resolveInitializing,
|
||||||
|
|||||||
@@ -100,6 +100,8 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
|
|||||||
operators,
|
operators,
|
||||||
prodMigrations: args.prodMigrations,
|
prodMigrations: args.prodMigrations,
|
||||||
push: args.push,
|
push: args.push,
|
||||||
|
rawRelations: {},
|
||||||
|
rawTables: {},
|
||||||
relations: {},
|
relations: {},
|
||||||
relationshipsSuffix: args.relationshipsSuffix || '_rels',
|
relationshipsSuffix: args.relationshipsSuffix || '_rels',
|
||||||
schema: {},
|
schema: {},
|
||||||
|
|||||||
@@ -1,138 +1,38 @@
|
|||||||
import type { DrizzleAdapter } from '@payloadcms/drizzle/types'
|
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 { buildDrizzleRelations, buildRawSchema, executeSchemaHooks } from '@payloadcms/drizzle'
|
||||||
import { uniqueIndex } from 'drizzle-orm/sqlite-core'
|
|
||||||
import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload'
|
|
||||||
import toSnakeCase from 'to-snake-case'
|
|
||||||
|
|
||||||
import type { BaseExtraConfig } from './schema/build.js'
|
|
||||||
import type { SQLiteAdapter } from './types.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) {
|
export const init: Init = async function init(this: SQLiteAdapter) {
|
||||||
let locales: [string, ...string[]] | undefined
|
let locales: string[] | undefined
|
||||||
await executeSchemaHooks({ type: 'beforeSchemaInit', adapter: this })
|
|
||||||
|
this.rawRelations = {}
|
||||||
|
this.rawTables = {}
|
||||||
|
|
||||||
if (this.payload.config.localization) {
|
if (this.payload.config.localization) {
|
||||||
locales = this.payload.config.localization.locales.map(({ code }) => code) as [
|
locales = this.payload.config.localization.locales.map(({ code }) => code)
|
||||||
string,
|
|
||||||
...string[],
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.payload.config.collections.forEach((collection: SanitizedCollectionConfig) => {
|
const adapter = this as unknown as DrizzleAdapter
|
||||||
createTableName({
|
|
||||||
adapter: this as unknown as DrizzleAdapter,
|
buildRawSchema({
|
||||||
config: collection,
|
adapter,
|
||||||
|
setColumnID,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (collection.versions) {
|
await executeSchemaHooks({ type: 'beforeSchemaInit', adapter: this })
|
||||||
createTableName({
|
|
||||||
adapter: this as unknown as DrizzleAdapter,
|
|
||||||
config: collection,
|
|
||||||
versions: true,
|
|
||||||
versionsCustomName: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
this.payload.config.collections.forEach((collection: SanitizedCollectionConfig) => {
|
|
||||||
const tableName = this.tableNameMap.get(toSnakeCase(collection.slug))
|
|
||||||
const config = this.payload.config
|
|
||||||
|
|
||||||
const baseExtraConfig: BaseExtraConfig = {}
|
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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (collection.upload.filenameCompoundIndex) {
|
buildDrizzleRelations({
|
||||||
const indexName = `${tableName}_filename_compound_idx`
|
adapter,
|
||||||
|
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
await executeSchemaHooks({ type: 'afterSchemaInit', adapter: this })
|
await executeSchemaHooks({ type: 'afterSchemaInit', adapter: this })
|
||||||
|
|||||||
@@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
158
packages/db-sqlite/src/schema/buildDrizzleTable.ts
Normal file
158
packages/db-sqlite/src/schema/buildDrizzleTable.ts
Normal 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)
|
||||||
|
}
|
||||||
@@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -1,28 +1,32 @@
|
|||||||
import type { SQLiteColumnBuilder } from 'drizzle-orm/sqlite-core'
|
import type { SetColumnID } from '@payloadcms/drizzle/types'
|
||||||
import type { FlattenedField } from 'payload'
|
|
||||||
|
|
||||||
import { integer, numeric, text } from 'drizzle-orm/sqlite-core'
|
export const setColumnID: SetColumnID = ({ columns, fields }) => {
|
||||||
|
|
||||||
import type { IDType } from '../types.js'
|
|
||||||
|
|
||||||
type Args = {
|
|
||||||
columns: Record<string, SQLiteColumnBuilder>
|
|
||||||
fields: FlattenedField[]
|
|
||||||
}
|
|
||||||
export const setColumnID = ({ columns, fields }: Args): IDType => {
|
|
||||||
const idField = fields.find((field) => field.name === 'id')
|
const idField = fields.find((field) => field.name === 'id')
|
||||||
if (idField) {
|
if (idField) {
|
||||||
if (idField.type === 'number') {
|
if (idField.type === 'number') {
|
||||||
columns.id = numeric('id').primaryKey()
|
columns.id = {
|
||||||
|
name: 'id',
|
||||||
|
type: 'numeric',
|
||||||
|
primaryKey: true,
|
||||||
|
}
|
||||||
return 'numeric'
|
return 'numeric'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (idField.type === 'text') {
|
if (idField.type === 'text') {
|
||||||
columns.id = text('id').primaryKey()
|
columns.id = {
|
||||||
|
name: 'id',
|
||||||
|
type: 'text',
|
||||||
|
primaryKey: true,
|
||||||
|
}
|
||||||
return 'text'
|
return 'text'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
columns.id = integer('id').primaryKey()
|
columns.id = {
|
||||||
|
name: 'id',
|
||||||
|
type: 'integer',
|
||||||
|
primaryKey: true,
|
||||||
|
}
|
||||||
|
|
||||||
return 'integer'
|
return 'integer'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -38,6 +38,8 @@ export type Args = {
|
|||||||
*/
|
*/
|
||||||
beforeSchemaInit?: SQLiteSchemaHook[]
|
beforeSchemaInit?: SQLiteSchemaHook[]
|
||||||
client: Config
|
client: Config
|
||||||
|
/** Generated schema from payload generate:db-schema file path */
|
||||||
|
generateSchemaOutputFile?: string
|
||||||
idType?: 'serial' | 'uuid'
|
idType?: 'serial' | 'uuid'
|
||||||
localesSuffix?: string
|
localesSuffix?: string
|
||||||
logger?: DrizzleConfig['logger']
|
logger?: DrizzleConfig['logger']
|
||||||
@@ -109,6 +111,16 @@ type SQLiteDrizzleAdapter = Omit<
|
|||||||
| 'relations'
|
| '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 = {
|
export type SQLiteAdapter = {
|
||||||
afterSchemaInit: SQLiteSchemaHook[]
|
afterSchemaInit: SQLiteSchemaHook[]
|
||||||
beforeSchemaInit: SQLiteSchemaHook[]
|
beforeSchemaInit: SQLiteSchemaHook[]
|
||||||
@@ -117,9 +129,7 @@ export type SQLiteAdapter = {
|
|||||||
countDistinct: CountDistinct
|
countDistinct: CountDistinct
|
||||||
defaultDrizzleSnapshot: any
|
defaultDrizzleSnapshot: any
|
||||||
deleteWhere: DeleteWhere
|
deleteWhere: DeleteWhere
|
||||||
drizzle: { $client: Client } & LibSQLDatabase<
|
drizzle: Drizzle
|
||||||
Record<string, GenericRelation | GenericTable> & Record<string, unknown>
|
|
||||||
>
|
|
||||||
dropDatabase: DropDatabase
|
dropDatabase: DropDatabase
|
||||||
execute: Execute<unknown>
|
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
|
* The Payload instance that you can use to execute Local API methods
|
||||||
* To use the current transaction you must pass `req` to arguments
|
* 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
|
* The Payload instance that you can use to execute Local API methods
|
||||||
* To use the current transaction you must pass `req` to arguments
|
* 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'>,
|
extends Omit<Args, 'idType' | 'logger' | 'migrationDir' | 'pool'>,
|
||||||
DrizzleAdapter {
|
DrizzleAdapter {
|
||||||
beginTransaction: (options?: SQLiteTransactionConfig) => Promise<null | number | string>
|
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
|
* 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
|
* Used for returning properly formed errors from unique fields
|
||||||
|
|||||||
@@ -110,6 +110,8 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj<Verce
|
|||||||
poolOptions: args.pool,
|
poolOptions: args.pool,
|
||||||
prodMigrations: args.prodMigrations,
|
prodMigrations: args.prodMigrations,
|
||||||
push: args.push,
|
push: args.push,
|
||||||
|
rawRelations: {},
|
||||||
|
rawTables: {},
|
||||||
relations: {},
|
relations: {},
|
||||||
relationshipsSuffix: args.relationshipsSuffix || '_rels',
|
relationshipsSuffix: args.relationshipsSuffix || '_rels',
|
||||||
schema: {},
|
schema: {},
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export { migrateStatus } from './migrateStatus.js'
|
|||||||
export { operatorMap } from './queries/operatorMap.js'
|
export { operatorMap } from './queries/operatorMap.js'
|
||||||
export type { Operators } from './queries/operatorMap.js'
|
export type { Operators } from './queries/operatorMap.js'
|
||||||
export { queryDrafts } from './queryDrafts.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 { beginTransaction } from './transactions/beginTransaction.js'
|
||||||
export { commitTransaction } from './transactions/commitTransaction.js'
|
export { commitTransaction } from './transactions/commitTransaction.js'
|
||||||
export { rollbackTransaction } from './transactions/rollbackTransaction.js'
|
export { rollbackTransaction } from './transactions/rollbackTransaction.js'
|
||||||
|
|||||||
@@ -1,16 +1,22 @@
|
|||||||
import type { Init, SanitizedCollectionConfig } from 'payload'
|
import type { Init } from 'payload'
|
||||||
|
|
||||||
import { uniqueIndex } from 'drizzle-orm/pg-core'
|
import type { BasePostgresAdapter } from './types.js'
|
||||||
import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload'
|
|
||||||
import toSnakeCase from 'to-snake-case'
|
|
||||||
|
|
||||||
import type { BaseExtraConfig, BasePostgresAdapter } from './types.js'
|
import { buildDrizzleRelations } from '../schema/buildDrizzleRelations.js'
|
||||||
|
import { buildRawSchema } from '../schema/buildRawSchema.js'
|
||||||
import { createTableName } from '../createTableName.js'
|
|
||||||
import { executeSchemaHooks } from '../utilities/executeSchemaHooks.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) {
|
export const init: Init = async function init(this: BasePostgresAdapter) {
|
||||||
|
this.rawRelations = {}
|
||||||
|
this.rawTables = {}
|
||||||
|
|
||||||
|
buildRawSchema({
|
||||||
|
adapter: this,
|
||||||
|
setColumnID,
|
||||||
|
})
|
||||||
|
|
||||||
await executeSchemaHooks({ type: 'beforeSchemaInit', adapter: this })
|
await executeSchemaHooks({ type: 'beforeSchemaInit', adapter: this })
|
||||||
|
|
||||||
if (this.payload.config.localization) {
|
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) => {
|
for (const tableName in this.rawTables) {
|
||||||
createTableName({
|
buildDrizzleTable({ adapter: this, rawTable: this.rawTables[tableName] })
|
||||||
adapter: this,
|
|
||||||
config: collection,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (collection.versions) {
|
|
||||||
createTableName({
|
|
||||||
adapter: this,
|
|
||||||
config: collection,
|
|
||||||
versions: true,
|
|
||||||
versionsCustomName: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
this.payload.config.collections.forEach((collection: SanitizedCollectionConfig) => {
|
|
||||||
const tableName = this.tableNameMap.get(toSnakeCase(collection.slug))
|
|
||||||
|
|
||||||
const baseExtraConfig: BaseExtraConfig = {}
|
|
||||||
|
|
||||||
if (collection.upload.filenameCompoundIndex) {
|
|
||||||
const indexName = `${tableName}_filename_compound_idx`
|
|
||||||
|
|
||||||
baseExtraConfig.filename_compound_index = (cols) => {
|
|
||||||
const colsConstraint = collection.upload.filenameCompoundIndex.map((f) => {
|
|
||||||
return cols[f]
|
|
||||||
})
|
|
||||||
return uniqueIndex(indexName).on(colsConstraint[0], ...colsConstraint.slice(1))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTable({
|
buildDrizzleRelations({
|
||||||
adapter: this,
|
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 })
|
await executeSchemaHooks({ type: 'afterSchemaInit', adapter: this })
|
||||||
|
|||||||
@@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
170
packages/drizzle/src/postgres/schema/buildDrizzleTable.ts
Normal file
170
packages/drizzle/src/postgres/schema/buildDrizzleTable.ts
Normal 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,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
})
|
|
||||||
@@ -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,
|
|
||||||
}
|
|
||||||
@@ -1,34 +1,44 @@
|
|||||||
import type { PgColumnBuilder } from 'drizzle-orm/pg-core'
|
import type { SetColumnID } from '../../types.js'
|
||||||
import type { FlattenedField } from 'payload'
|
|
||||||
|
|
||||||
import { numeric, serial, uuid, varchar } from 'drizzle-orm/pg-core'
|
export const setColumnID: SetColumnID = ({ adapter, columns, fields }) => {
|
||||||
|
|
||||||
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 => {
|
|
||||||
const idField = fields.find((field) => field.name === 'id')
|
const idField = fields.find((field) => field.name === 'id')
|
||||||
if (idField) {
|
if (idField) {
|
||||||
if (idField.type === 'number') {
|
if (idField.type === 'number') {
|
||||||
columns.id = numeric('id').primaryKey()
|
columns.id = {
|
||||||
|
name: 'id',
|
||||||
|
type: 'numeric',
|
||||||
|
primaryKey: true,
|
||||||
|
}
|
||||||
|
|
||||||
return 'numeric'
|
return 'numeric'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (idField.type === 'text') {
|
if (idField.type === 'text') {
|
||||||
columns.id = varchar('id').primaryKey()
|
columns.id = {
|
||||||
|
name: 'id',
|
||||||
|
type: 'varchar',
|
||||||
|
primaryKey: true,
|
||||||
|
}
|
||||||
return 'varchar'
|
return 'varchar'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (adapter.idType === 'uuid') {
|
if (adapter.idType === 'uuid') {
|
||||||
columns.id = uuid('id').defaultRandom().primaryKey()
|
columns.id = {
|
||||||
|
name: 'id',
|
||||||
|
type: 'uuid',
|
||||||
|
defaultRandom: true,
|
||||||
|
primaryKey: true,
|
||||||
|
}
|
||||||
|
|
||||||
return 'uuid'
|
return 'uuid'
|
||||||
}
|
}
|
||||||
|
|
||||||
columns.id = serial('id').primaryKey()
|
columns.id = {
|
||||||
|
name: 'id',
|
||||||
|
type: 'serial',
|
||||||
|
primaryKey: true,
|
||||||
|
}
|
||||||
|
|
||||||
return 'integer'
|
return 'integer'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
705
packages/drizzle/src/schema/build.ts
Normal file
705
packages/drizzle/src/schema/build.ts
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
40
packages/drizzle/src/schema/buildDrizzleRelations.ts
Normal file
40
packages/drizzle/src/schema/buildDrizzleRelations.ts
Normal 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
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
120
packages/drizzle/src/schema/buildRawSchema.ts
Normal file
120
packages/drizzle/src/schema/buildRawSchema.ts
Normal 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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 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 { InvalidConfiguration } from 'payload'
|
||||||
import { fieldAffectsData, fieldIsVirtual, optionIsObject } from 'payload/shared'
|
import { fieldAffectsData, fieldIsVirtual, optionIsObject } from 'payload/shared'
|
||||||
import toSnakeCase from 'to-snake-case'
|
import toSnakeCase from 'to-snake-case'
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
BaseExtraConfig,
|
DrizzleAdapter,
|
||||||
BasePostgresAdapter,
|
|
||||||
GenericColumns,
|
|
||||||
IDType,
|
IDType,
|
||||||
|
RawColumn,
|
||||||
|
RawForeignKey,
|
||||||
|
RawIndex,
|
||||||
|
RawRelation,
|
||||||
RelationMap,
|
RelationMap,
|
||||||
|
SetColumnID,
|
||||||
} from '../types.js'
|
} from '../types.js'
|
||||||
|
|
||||||
import { createTableName } from '../../createTableName.js'
|
import { createTableName } from '../createTableName.js'
|
||||||
import { buildIndexName } from '../../utilities/buildIndexName.js'
|
import { buildIndexName } from '../utilities/buildIndexName.js'
|
||||||
import { hasLocalesTable } from '../../utilities/hasLocalesTable.js'
|
import { hasLocalesTable } from '../utilities/hasLocalesTable.js'
|
||||||
import { validateExistingBlockIsIdentical } from '../../utilities/validateExistingBlockIsIdentical.js'
|
import { validateExistingBlockIsIdentical } from '../utilities/validateExistingBlockIsIdentical.js'
|
||||||
import { buildTable } from './build.js'
|
import { buildTable } from './build.js'
|
||||||
import { createIndex } from './createIndex.js'
|
|
||||||
import { geometryColumn } from './geometryColumn.js'
|
|
||||||
import { idToUUID } from './idToUUID.js'
|
import { idToUUID } from './idToUUID.js'
|
||||||
import { parentIDColumnMap } from './parentIDColumnMap.js'
|
|
||||||
import { withDefault } from './withDefault.js'
|
import { withDefault } from './withDefault.js'
|
||||||
|
|
||||||
type Args = {
|
type Args = {
|
||||||
adapter: BasePostgresAdapter
|
adapter: DrizzleAdapter
|
||||||
columnPrefix?: string
|
columnPrefix?: string
|
||||||
columns: Record<string, PgColumnBuilder>
|
columns: Record<string, RawColumn>
|
||||||
disableNotNull: boolean
|
disableNotNull: boolean
|
||||||
disableRelsTableUnique?: boolean
|
disableRelsTableUnique?: boolean
|
||||||
disableUnique?: boolean
|
disableUnique?: boolean
|
||||||
fieldPrefix?: string
|
fieldPrefix?: string
|
||||||
fields: FlattenedField[]
|
fields: FlattenedField[]
|
||||||
forceLocalized?: boolean
|
forceLocalized?: boolean
|
||||||
indexes: Record<string, (cols: GenericColumns) => IndexBuilder>
|
indexes: Record<string, RawIndex>
|
||||||
localesColumns: Record<string, PgColumnBuilder>
|
localesColumns: Record<string, RawColumn>
|
||||||
localesIndexes: Record<string, (cols: GenericColumns) => IndexBuilder>
|
localesIndexes: Record<string, RawIndex>
|
||||||
newTableName: string
|
newTableName: string
|
||||||
parentTableName: string
|
parentTableName: string
|
||||||
relationships: Set<string>
|
relationships: Set<string>
|
||||||
relationsToBuild: RelationMap
|
relationsToBuild: RelationMap
|
||||||
rootRelationsToBuild?: RelationMap
|
rootRelationsToBuild?: RelationMap
|
||||||
rootTableIDColType: string
|
rootTableIDColType: IDType
|
||||||
rootTableName: string
|
rootTableName: string
|
||||||
|
setColumnID: SetColumnID
|
||||||
uniqueRelationships: Set<string>
|
uniqueRelationships: Set<string>
|
||||||
versions: boolean
|
versions: boolean
|
||||||
/**
|
/**
|
||||||
@@ -98,6 +82,7 @@ export const traverseFields = ({
|
|||||||
rootRelationsToBuild,
|
rootRelationsToBuild,
|
||||||
rootTableIDColType,
|
rootTableIDColType,
|
||||||
rootTableName,
|
rootTableName,
|
||||||
|
setColumnID,
|
||||||
uniqueRelationships,
|
uniqueRelationships,
|
||||||
versions,
|
versions,
|
||||||
withinLocalizedArrayOrBlock,
|
withinLocalizedArrayOrBlock,
|
||||||
@@ -111,14 +96,11 @@ export const traverseFields = ({
|
|||||||
let hasLocalizedManyNumberField = false
|
let hasLocalizedManyNumberField = false
|
||||||
|
|
||||||
let parentIDColType: IDType = 'integer'
|
let parentIDColType: IDType = 'integer'
|
||||||
if (columns.id instanceof PgUUIDBuilder) {
|
|
||||||
parentIDColType = 'uuid'
|
const idColumn = columns.id
|
||||||
}
|
|
||||||
if (columns.id instanceof PgNumericBuilder) {
|
if (idColumn && ['numeric', 'text', 'uuid', 'varchar'].includes(idColumn.type)) {
|
||||||
parentIDColType = 'numeric'
|
parentIDColType = idColumn.type as IDType
|
||||||
}
|
|
||||||
if (columns.id instanceof PgVarcharBuilder) {
|
|
||||||
parentIDColType = 'varchar'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fields.forEach((field) => {
|
fields.forEach((field) => {
|
||||||
@@ -168,11 +150,11 @@ export const traverseFields = ({
|
|||||||
|
|
||||||
const indexName = buildIndexName({ name: `${newTableName}_${columnName}`, adapter })
|
const indexName = buildIndexName({ name: `${newTableName}_${columnName}`, adapter })
|
||||||
|
|
||||||
targetIndexes[indexName] = createIndex({
|
targetIndexes[indexName] = {
|
||||||
name: field.localized ? [fieldName, '_locale'] : fieldName,
|
name: indexName,
|
||||||
indexName,
|
on: field.localized ? [fieldName, '_locale'] : fieldName,
|
||||||
unique,
|
unique,
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (field.type) {
|
switch (field.type) {
|
||||||
@@ -188,20 +170,42 @@ export const traverseFields = ({
|
|||||||
versionsCustomName: versions,
|
versionsCustomName: versions,
|
||||||
})
|
})
|
||||||
|
|
||||||
const baseColumns: Record<string, PgColumnBuilder> = {
|
const baseColumns: Record<string, RawColumn> = {
|
||||||
_order: integer('_order').notNull(),
|
_order: {
|
||||||
_parentID: parentIDColumnMap[parentIDColType]('_parent_id').notNull(),
|
name: '_order',
|
||||||
|
type: 'integer',
|
||||||
|
notNull: true,
|
||||||
|
},
|
||||||
|
_parentID: {
|
||||||
|
name: '_parent_id',
|
||||||
|
type: parentIDColType,
|
||||||
|
notNull: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseExtraConfig: BaseExtraConfig = {
|
const baseIndexes: Record<string, RawIndex> = {
|
||||||
_orderIdx: (cols) => index(`${arrayTableName}_order_idx`).on(cols._order),
|
_orderIdx: {
|
||||||
_parentIDFk: (cols) =>
|
name: `${arrayTableName}_order_idx`,
|
||||||
foreignKey({
|
on: ['_order'],
|
||||||
|
},
|
||||||
|
_parentIDIdx: {
|
||||||
|
name: `${arrayTableName}_parent_id_idx`,
|
||||||
|
on: '_parentID',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseForeignKeys: Record<string, RawForeignKey> = {
|
||||||
|
_parentIDFk: {
|
||||||
name: `${arrayTableName}_parent_id_fk`,
|
name: `${arrayTableName}_parent_id_fk`,
|
||||||
columns: [cols['_parentID']],
|
columns: ['_parentID'],
|
||||||
foreignColumns: [adapter.tables[parentTableName].id],
|
foreignColumns: [
|
||||||
}).onDelete('cascade'),
|
{
|
||||||
_parentIDIdx: (cols) => index(`${arrayTableName}_parent_id_idx`).on(cols._parentID),
|
name: 'id',
|
||||||
|
table: parentTableName,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onDelete: 'cascade',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLocalized =
|
const isLocalized =
|
||||||
@@ -210,9 +214,17 @@ export const traverseFields = ({
|
|||||||
forceLocalized
|
forceLocalized
|
||||||
|
|
||||||
if (isLocalized) {
|
if (isLocalized) {
|
||||||
baseColumns._locale = adapter.enums.enum__locales('_locale').notNull()
|
baseColumns._locale = {
|
||||||
baseExtraConfig._localeIdx = (cols) =>
|
name: '_locale',
|
||||||
index(`${arrayTableName}_locale_idx`).on(cols._locale)
|
type: 'enum',
|
||||||
|
locale: true,
|
||||||
|
notNull: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
baseIndexes._localeIdx = {
|
||||||
|
name: `${arrayTableName}_locale_idx`,
|
||||||
|
on: '_locale',
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -225,7 +237,8 @@ export const traverseFields = ({
|
|||||||
} = buildTable({
|
} = buildTable({
|
||||||
adapter,
|
adapter,
|
||||||
baseColumns,
|
baseColumns,
|
||||||
baseExtraConfig,
|
baseForeignKeys,
|
||||||
|
baseIndexes,
|
||||||
disableNotNull: disableNotNullFromHere,
|
disableNotNull: disableNotNullFromHere,
|
||||||
disableRelsTableUnique: true,
|
disableRelsTableUnique: true,
|
||||||
disableUnique,
|
disableUnique,
|
||||||
@@ -235,6 +248,7 @@ export const traverseFields = ({
|
|||||||
rootTableIDColType,
|
rootTableIDColType,
|
||||||
rootTableName,
|
rootTableName,
|
||||||
rootUniqueRelationships: uniqueRelationships,
|
rootUniqueRelationships: uniqueRelationships,
|
||||||
|
setColumnID,
|
||||||
tableName: arrayTableName,
|
tableName: arrayTableName,
|
||||||
versions,
|
versions,
|
||||||
withinLocalizedArrayOrBlock: isLocalized,
|
withinLocalizedArrayOrBlock: isLocalized,
|
||||||
@@ -270,21 +284,27 @@ export const traverseFields = ({
|
|||||||
target: arrayTableName,
|
target: arrayTableName,
|
||||||
})
|
})
|
||||||
|
|
||||||
adapter.relations[`relations_${arrayTableName}`] = relations(
|
const arrayRelations: Record<string, RawRelation> = {
|
||||||
adapter.tables[arrayTableName],
|
_parentID: {
|
||||||
({ many, one }) => {
|
type: 'one',
|
||||||
const result: Record<string, Relation<string>> = {
|
fields: [
|
||||||
_parentID: one(adapter.tables[parentTableName], {
|
{
|
||||||
fields: [adapter.tables[arrayTableName]._parentID],
|
name: '_parentID',
|
||||||
references: [adapter.tables[parentTableName].id],
|
table: arrayTableName,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
references: ['id'],
|
||||||
relationName: fieldName,
|
relationName: fieldName,
|
||||||
}),
|
to: parentTableName,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasLocalesTable(field.fields)) {
|
if (hasLocalesTable(field.fields)) {
|
||||||
result._locales = many(adapter.tables[`${arrayTableName}${adapter.localesSuffix}`], {
|
arrayRelations._locales = {
|
||||||
|
type: 'many',
|
||||||
relationName: '_locales',
|
relationName: '_locales',
|
||||||
})
|
to: `${arrayTableName}${adapter.localesSuffix}`,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
subRelationsToBuild.forEach(({ type, localized, target }, key) => {
|
subRelationsToBuild.forEach(({ type, localized, target }, key) => {
|
||||||
@@ -292,20 +312,31 @@ export const traverseFields = ({
|
|||||||
const arrayWithLocalized = localized
|
const arrayWithLocalized = localized
|
||||||
? `${arrayTableName}${adapter.localesSuffix}`
|
? `${arrayTableName}${adapter.localesSuffix}`
|
||||||
: arrayTableName
|
: arrayTableName
|
||||||
result[key] = one(adapter.tables[target], {
|
|
||||||
fields: [adapter.tables[arrayWithLocalized][key]],
|
arrayRelations[key] = {
|
||||||
references: [adapter.tables[target].id],
|
type: 'one',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: key,
|
||||||
|
table: arrayWithLocalized,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
references: ['id'],
|
||||||
relationName: key,
|
relationName: key,
|
||||||
})
|
to: target,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (type === 'many') {
|
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
|
break
|
||||||
}
|
}
|
||||||
@@ -321,23 +352,52 @@ export const traverseFields = ({
|
|||||||
throwValidationError,
|
throwValidationError,
|
||||||
versionsCustomName: versions,
|
versionsCustomName: versions,
|
||||||
})
|
})
|
||||||
if (!adapter.tables[blockTableName]) {
|
if (!adapter.rawTables[blockTableName]) {
|
||||||
const baseColumns: Record<string, PgColumnBuilder> = {
|
const baseColumns: Record<string, RawColumn> = {
|
||||||
_order: integer('_order').notNull(),
|
_order: {
|
||||||
_parentID: parentIDColumnMap[rootTableIDColType]('_parent_id').notNull(),
|
name: '_order',
|
||||||
_path: text('_path').notNull(),
|
type: 'integer',
|
||||||
|
notNull: true,
|
||||||
|
},
|
||||||
|
_parentID: {
|
||||||
|
name: '_parent_id',
|
||||||
|
type: rootTableIDColType,
|
||||||
|
notNull: true,
|
||||||
|
},
|
||||||
|
_path: {
|
||||||
|
name: '_path',
|
||||||
|
type: 'text',
|
||||||
|
notNull: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseExtraConfig: BaseExtraConfig = {
|
const baseIndexes: Record<string, RawIndex> = {
|
||||||
_orderIdx: (cols) => index(`${blockTableName}_order_idx`).on(cols._order),
|
_orderIdx: {
|
||||||
_parentIdFk: (cols) =>
|
name: `${blockTableName}_order_idx`,
|
||||||
foreignKey({
|
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`,
|
name: `${blockTableName}_parent_id_fk`,
|
||||||
columns: [cols._parentID],
|
columns: ['_parentID'],
|
||||||
foreignColumns: [adapter.tables[rootTableName].id],
|
foreignColumns: [
|
||||||
}).onDelete('cascade'),
|
{
|
||||||
_parentIDIdx: (cols) => index(`${blockTableName}_parent_id_idx`).on(cols._parentID),
|
name: 'id',
|
||||||
_pathIdx: (cols) => index(`${blockTableName}_path_idx`).on(cols._path),
|
table: rootTableName,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onDelete: 'cascade',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLocalized =
|
const isLocalized =
|
||||||
@@ -346,9 +406,17 @@ export const traverseFields = ({
|
|||||||
forceLocalized
|
forceLocalized
|
||||||
|
|
||||||
if (isLocalized) {
|
if (isLocalized) {
|
||||||
baseColumns._locale = adapter.enums.enum__locales('_locale').notNull()
|
baseColumns._locale = {
|
||||||
baseExtraConfig._localeIdx = (cols) =>
|
name: '_locale',
|
||||||
index(`${blockTableName}_locale_idx`).on(cols._locale)
|
type: 'enum',
|
||||||
|
locale: true,
|
||||||
|
notNull: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
baseIndexes._localeIdx = {
|
||||||
|
name: `${blockTableName}_locale_idx`,
|
||||||
|
on: '_locale',
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -361,7 +429,8 @@ export const traverseFields = ({
|
|||||||
} = buildTable({
|
} = buildTable({
|
||||||
adapter,
|
adapter,
|
||||||
baseColumns,
|
baseColumns,
|
||||||
baseExtraConfig,
|
baseForeignKeys,
|
||||||
|
baseIndexes,
|
||||||
disableNotNull: disableNotNullFromHere,
|
disableNotNull: disableNotNullFromHere,
|
||||||
disableRelsTableUnique: true,
|
disableRelsTableUnique: true,
|
||||||
disableUnique,
|
disableUnique,
|
||||||
@@ -371,6 +440,7 @@ export const traverseFields = ({
|
|||||||
rootTableIDColType,
|
rootTableIDColType,
|
||||||
rootTableName,
|
rootTableName,
|
||||||
rootUniqueRelationships: uniqueRelationships,
|
rootUniqueRelationships: uniqueRelationships,
|
||||||
|
setColumnID,
|
||||||
tableName: blockTableName,
|
tableName: blockTableName,
|
||||||
versions,
|
versions,
|
||||||
withinLocalizedArrayOrBlock: isLocalized,
|
withinLocalizedArrayOrBlock: isLocalized,
|
||||||
@@ -400,22 +470,27 @@ export const traverseFields = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
adapter.relations[`relations_${blockTableName}`] = relations(
|
const blockRelations: Record<string, RawRelation> = {
|
||||||
adapter.tables[blockTableName],
|
_parentID: {
|
||||||
({ many, one }) => {
|
type: 'one',
|
||||||
const result: Record<string, Relation<string>> = {
|
fields: [
|
||||||
_parentID: one(adapter.tables[rootTableName], {
|
{
|
||||||
fields: [adapter.tables[blockTableName]._parentID],
|
name: '_parentID',
|
||||||
references: [adapter.tables[rootTableName].id],
|
table: blockTableName,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
references: ['id'],
|
||||||
relationName: `_blocks_${block.slug}`,
|
relationName: `_blocks_${block.slug}`,
|
||||||
}),
|
to: rootTableName,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasLocalesTable(block.fields)) {
|
if (hasLocalesTable(block.fields)) {
|
||||||
result._locales = many(
|
blockRelations._locales = {
|
||||||
adapter.tables[`${blockTableName}${adapter.localesSuffix}`],
|
type: 'many',
|
||||||
{ relationName: '_locales' },
|
relationName: '_locales',
|
||||||
)
|
to: `${blockTableName}${adapter.localesSuffix}`,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
subRelationsToBuild.forEach(({ type, localized, target }, key) => {
|
subRelationsToBuild.forEach(({ type, localized, target }, key) => {
|
||||||
@@ -423,27 +498,38 @@ export const traverseFields = ({
|
|||||||
const blockWithLocalized = localized
|
const blockWithLocalized = localized
|
||||||
? `${blockTableName}${adapter.localesSuffix}`
|
? `${blockTableName}${adapter.localesSuffix}`
|
||||||
: blockTableName
|
: blockTableName
|
||||||
result[key] = one(adapter.tables[target], {
|
|
||||||
fields: [adapter.tables[blockWithLocalized][key]],
|
blockRelations[key] = {
|
||||||
references: [adapter.tables[target].id],
|
type: 'one',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: key,
|
||||||
|
table: blockWithLocalized,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
references: ['id'],
|
||||||
relationName: key,
|
relationName: key,
|
||||||
})
|
to: target,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (type === 'many') {
|
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) {
|
} else if (process.env.NODE_ENV !== 'production' && !versions) {
|
||||||
validateExistingBlockIsIdentical({
|
validateExistingBlockIsIdentical({
|
||||||
block,
|
block,
|
||||||
localized: field.localized,
|
localized: field.localized,
|
||||||
rootTableName,
|
rootTableName,
|
||||||
table: adapter.tables[blockTableName],
|
table: adapter.rawTables[blockTableName],
|
||||||
tableLocales: adapter.tables[`${blockTableName}${adapter.localesSuffix}`],
|
tableLocales: adapter.rawTables[`${blockTableName}${adapter.localesSuffix}`],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// blocks relationships are defined from the collection or globals table down to the block, bypassing any subBlocks
|
// 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
|
break
|
||||||
}
|
}
|
||||||
case 'checkbox': {
|
case 'checkbox': {
|
||||||
targetTable[fieldName] = withDefault(boolean(columnName), field)
|
targetTable[fieldName] = withDefault(
|
||||||
|
{
|
||||||
|
name: columnName,
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
|
field,
|
||||||
|
)
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'code':
|
case 'code':
|
||||||
case 'email':
|
case 'email':
|
||||||
case 'textarea': {
|
case 'textarea': {
|
||||||
targetTable[fieldName] = withDefault(varchar(columnName), field)
|
targetTable[fieldName] = withDefault(
|
||||||
|
{
|
||||||
|
name: columnName,
|
||||||
|
type: 'varchar',
|
||||||
|
},
|
||||||
|
field,
|
||||||
|
)
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'date': {
|
case 'date': {
|
||||||
targetTable[fieldName] = withDefault(
|
targetTable[fieldName] = withDefault(
|
||||||
timestamp(columnName, {
|
{
|
||||||
|
name: columnName,
|
||||||
|
type: 'timestamp',
|
||||||
mode: 'string',
|
mode: 'string',
|
||||||
precision: 3,
|
precision: 3,
|
||||||
withTimezone: true,
|
withTimezone: true,
|
||||||
}),
|
},
|
||||||
field,
|
field,
|
||||||
)
|
)
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -511,6 +614,7 @@ export const traverseFields = ({
|
|||||||
rootRelationsToBuild,
|
rootRelationsToBuild,
|
||||||
rootTableIDColType,
|
rootTableIDColType,
|
||||||
rootTableName,
|
rootTableName,
|
||||||
|
setColumnID,
|
||||||
uniqueRelationships,
|
uniqueRelationships,
|
||||||
versions,
|
versions,
|
||||||
withinLocalizedArrayOrBlock: withinLocalizedArrayOrBlock || field.localized,
|
withinLocalizedArrayOrBlock: withinLocalizedArrayOrBlock || field.localized,
|
||||||
@@ -539,7 +643,14 @@ export const traverseFields = ({
|
|||||||
|
|
||||||
case 'json':
|
case 'json':
|
||||||
case 'richText': {
|
case 'richText': {
|
||||||
targetTable[fieldName] = withDefault(jsonb(columnName), field)
|
targetTable[fieldName] = withDefault(
|
||||||
|
{
|
||||||
|
name: columnName,
|
||||||
|
type: 'jsonb',
|
||||||
|
},
|
||||||
|
field,
|
||||||
|
)
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -566,16 +677,27 @@ export const traverseFields = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
targetTable[fieldName] = withDefault(numeric(columnName), field)
|
targetTable[fieldName] = withDefault(
|
||||||
|
{
|
||||||
|
name: columnName,
|
||||||
|
type: 'numeric',
|
||||||
|
},
|
||||||
|
field,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'point': {
|
case 'point': {
|
||||||
targetTable[fieldName] = withDefault(geometryColumn(columnName), field)
|
targetTable[fieldName] = withDefault(
|
||||||
if (!adapter.extensions.postgis) {
|
{
|
||||||
adapter.extensions.postgis = true
|
name: columnName,
|
||||||
}
|
type: 'geometry',
|
||||||
|
},
|
||||||
|
field,
|
||||||
|
)
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -590,16 +712,13 @@ export const traverseFields = ({
|
|||||||
throwValidationError,
|
throwValidationError,
|
||||||
})
|
})
|
||||||
|
|
||||||
adapter.enums[enumName] = adapter.pgSchema.enum(
|
const options = field.options.map((option) => {
|
||||||
enumName,
|
|
||||||
field.options.map((option) => {
|
|
||||||
if (optionIsObject(option)) {
|
if (optionIsObject(option)) {
|
||||||
return option.value
|
return option.value
|
||||||
}
|
}
|
||||||
|
|
||||||
return option
|
return option
|
||||||
}) as [string, ...string[]],
|
})
|
||||||
)
|
|
||||||
|
|
||||||
if (field.type === 'select' && field.hasMany) {
|
if (field.type === 'select' && field.hasMany) {
|
||||||
const selectTableName = createTableName({
|
const selectTableName = createTableName({
|
||||||
@@ -610,21 +729,56 @@ export const traverseFields = ({
|
|||||||
throwValidationError,
|
throwValidationError,
|
||||||
versionsCustomName: versions,
|
versionsCustomName: versions,
|
||||||
})
|
})
|
||||||
const baseColumns: Record<string, PgColumnBuilder> = {
|
|
||||||
order: integer('order').notNull(),
|
const baseColumns: Record<string, RawColumn> = {
|
||||||
parent: parentIDColumnMap[parentIDColType]('parent_id').notNull(),
|
order: {
|
||||||
value: adapter.enums[enumName]('value'),
|
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 = {
|
const baseIndexes: Record<string, RawIndex> = {
|
||||||
orderIdx: (cols) => index(`${selectTableName}_order_idx`).on(cols.order),
|
orderIdx: {
|
||||||
parentFk: (cols) =>
|
name: `${selectTableName}_order_idx`,
|
||||||
foreignKey({
|
on: 'order',
|
||||||
|
},
|
||||||
|
parentIdx: {
|
||||||
|
name: `${selectTableName}_parent_idx`,
|
||||||
|
on: 'parent',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseForeignKeys: Record<string, RawForeignKey> = {
|
||||||
|
parentFk: {
|
||||||
name: `${selectTableName}_parent_fk`,
|
name: `${selectTableName}_parent_fk`,
|
||||||
columns: [cols.parent],
|
columns: ['parent'],
|
||||||
foreignColumns: [adapter.tables[parentTableName].id],
|
foreignColumns: [
|
||||||
}).onDelete('cascade'),
|
{
|
||||||
parentIdx: (cols) => index(`${selectTableName}_parent_idx`).on(cols.parent),
|
name: 'id',
|
||||||
|
table: parentTableName,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onDelete: 'cascade',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLocalized =
|
const isLocalized =
|
||||||
@@ -633,23 +787,36 @@ export const traverseFields = ({
|
|||||||
forceLocalized
|
forceLocalized
|
||||||
|
|
||||||
if (isLocalized) {
|
if (isLocalized) {
|
||||||
baseColumns.locale = adapter.enums.enum__locales('locale').notNull()
|
baseColumns.locale = {
|
||||||
baseExtraConfig.localeIdx = (cols) =>
|
name: 'locale',
|
||||||
index(`${selectTableName}_locale_idx`).on(cols.locale)
|
type: 'enum',
|
||||||
|
locale: true,
|
||||||
|
notNull: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
baseIndexes.localeIdx = {
|
||||||
|
name: `${selectTableName}_locale_idx`,
|
||||||
|
on: 'locale',
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (field.index) {
|
if (field.index) {
|
||||||
baseExtraConfig.value = (cols) => index(`${selectTableName}_value_idx`).on(cols.value)
|
baseIndexes.value = {
|
||||||
|
name: `${selectTableName}_value_idx`,
|
||||||
|
on: 'value',
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTable({
|
buildTable({
|
||||||
adapter,
|
adapter,
|
||||||
baseColumns,
|
baseColumns,
|
||||||
baseExtraConfig,
|
baseForeignKeys,
|
||||||
|
baseIndexes,
|
||||||
disableNotNull,
|
disableNotNull,
|
||||||
disableUnique,
|
disableUnique,
|
||||||
fields: [],
|
fields: [],
|
||||||
rootTableName,
|
rootTableName,
|
||||||
|
setColumnID,
|
||||||
tableName: selectTableName,
|
tableName: selectTableName,
|
||||||
versions,
|
versions,
|
||||||
})
|
})
|
||||||
@@ -661,18 +828,30 @@ export const traverseFields = ({
|
|||||||
target: selectTableName,
|
target: selectTableName,
|
||||||
})
|
})
|
||||||
|
|
||||||
adapter.relations[`relations_${selectTableName}`] = relations(
|
adapter.rawRelations[selectTableName] = {
|
||||||
adapter.tables[selectTableName],
|
parent: {
|
||||||
({ one }) => ({
|
type: 'one',
|
||||||
parent: one(adapter.tables[parentTableName], {
|
fields: [
|
||||||
fields: [adapter.tables[selectTableName].parent],
|
{
|
||||||
references: [adapter.tables[parentTableName].id],
|
name: 'parent',
|
||||||
|
table: selectTableName,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
references: ['id'],
|
||||||
relationName: fieldName,
|
relationName: fieldName,
|
||||||
}),
|
to: parentTableName,
|
||||||
}),
|
},
|
||||||
)
|
}
|
||||||
} else {
|
} else {
|
||||||
targetTable[fieldName] = withDefault(adapter.enums[enumName](columnName), field)
|
targetTable[fieldName] = withDefault(
|
||||||
|
{
|
||||||
|
name: columnName,
|
||||||
|
type: 'enum',
|
||||||
|
enumName,
|
||||||
|
options,
|
||||||
|
},
|
||||||
|
field,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -698,7 +877,7 @@ export const traverseFields = ({
|
|||||||
const tableName = adapter.tableNameMap.get(toSnakeCase(field.relationTo))
|
const tableName = adapter.tableNameMap.get(toSnakeCase(field.relationTo))
|
||||||
|
|
||||||
// get the id type of the related collection
|
// 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(
|
const relatedCollectionCustomID = relationshipConfig.fields.find(
|
||||||
(field) => fieldAffectsData(field) && field.name === 'id',
|
(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
|
// make the foreign key column for relationship using the correct id column type
|
||||||
targetTable[fieldName] = parentIDColumnMap[colType](`${columnName}_id`).references(
|
targetTable[fieldName] = {
|
||||||
() => adapter.tables[tableName].id,
|
name: `${columnName}_id`,
|
||||||
{ onDelete: 'set null' },
|
type: colType,
|
||||||
)
|
reference: {
|
||||||
|
name: 'id',
|
||||||
|
onDelete: 'set null',
|
||||||
|
table: tableName,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
// add relationship to table
|
// add relationship to table
|
||||||
relationsToBuild.set(fieldName, {
|
relationsToBuild.set(fieldName, {
|
||||||
@@ -724,7 +908,7 @@ export const traverseFields = ({
|
|||||||
|
|
||||||
// add notNull when not required
|
// add notNull when not required
|
||||||
if (!disableNotNull && field.required && !field.admin?.condition) {
|
if (!disableNotNull && field.required && !field.admin?.condition) {
|
||||||
targetTable[fieldName].notNull()
|
targetTable[fieldName].notNull = true
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -761,7 +945,13 @@ export const traverseFields = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
targetTable[fieldName] = withDefault(varchar(columnName), field)
|
targetTable[fieldName] = withDefault(
|
||||||
|
{
|
||||||
|
name: columnName,
|
||||||
|
type: 'varchar',
|
||||||
|
},
|
||||||
|
field,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -779,7 +969,7 @@ export const traverseFields = ({
|
|||||||
field.required &&
|
field.required &&
|
||||||
!condition
|
!condition
|
||||||
) {
|
) {
|
||||||
targetTable[fieldName].notNull()
|
targetTable[fieldName].notNull = true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
22
packages/drizzle/src/schema/withDefault.ts
Normal file
22
packages/drizzle/src/schema/withDefault.ts
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,10 +11,22 @@ import type {
|
|||||||
} from 'drizzle-orm'
|
} from 'drizzle-orm'
|
||||||
import type { LibSQLDatabase } from 'drizzle-orm/libsql'
|
import type { LibSQLDatabase } from 'drizzle-orm/libsql'
|
||||||
import type { NodePgDatabase, NodePgQueryResultHKT } from 'drizzle-orm/node-postgres'
|
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 { SQLiteColumn, SQLiteTable, SQLiteTransaction } from 'drizzle-orm/sqlite-core'
|
||||||
import type { Result } from 'drizzle-orm/sqlite-core/session'
|
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'
|
import type { BuildQueryJoinAliases } from './queries/buildQuery.js'
|
||||||
|
|
||||||
@@ -157,6 +169,129 @@ export type CreateJSONQueryArgs = {
|
|||||||
value: boolean | number | string
|
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 {
|
export interface DrizzleAdapter extends BaseDatabaseAdapter {
|
||||||
convertPathToJSONTraversal?: (incomingSegments: string[]) => string
|
convertPathToJSONTraversal?: (incomingSegments: string[]) => string
|
||||||
countDistinct: CountDistinct
|
countDistinct: CountDistinct
|
||||||
@@ -184,7 +319,10 @@ export interface DrizzleAdapter extends BaseDatabaseAdapter {
|
|||||||
logger: DrizzleConfig['logger']
|
logger: DrizzleConfig['logger']
|
||||||
operators: Operators
|
operators: Operators
|
||||||
push: boolean
|
push: boolean
|
||||||
|
rawRelations: Record<string, Record<string, RawRelation>>
|
||||||
|
rawTables: Record<string, RawTable>
|
||||||
rejectInitializing: () => void
|
rejectInitializing: () => void
|
||||||
|
|
||||||
relations: Record<string, GenericRelation>
|
relations: Record<string, GenericRelation>
|
||||||
relationshipsSuffix?: string
|
relationshipsSuffix?: string
|
||||||
requireDrizzleKit: RequireDrizzleKit
|
requireDrizzleKit: RequireDrizzleKit
|
||||||
@@ -204,3 +342,13 @@ export interface DrizzleAdapter extends BaseDatabaseAdapter {
|
|||||||
transactionOptions: unknown
|
transactionOptions: unknown
|
||||||
versionsSuffix?: string
|
versionsSuffix?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type RelationMap = Map<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
localized: boolean
|
||||||
|
relationName?: string
|
||||||
|
target: string
|
||||||
|
type: 'many' | 'one'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ type Args = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const executeSchemaHooks = async ({ type, adapter }: Args): Promise<void> => {
|
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({
|
const result = await hook({
|
||||||
adapter,
|
adapter: adapter as unknown as Adapter,
|
||||||
extendTable: extendDrizzleTable,
|
extendTable: extendDrizzleTable,
|
||||||
schema: {
|
schema: {
|
||||||
enums: adapter.enums,
|
enums: adapter.enums,
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ import type { Block, Field } from 'payload'
|
|||||||
import { InvalidConfiguration } from 'payload'
|
import { InvalidConfiguration } from 'payload'
|
||||||
import { fieldAffectsData, fieldHasSubFields, tabHasName } from 'payload/shared'
|
import { fieldAffectsData, fieldHasSubFields, tabHasName } from 'payload/shared'
|
||||||
|
|
||||||
|
import type { RawTable } from '../types.js'
|
||||||
|
|
||||||
type Args = {
|
type Args = {
|
||||||
block: Block
|
block: Block
|
||||||
localized: boolean
|
localized: boolean
|
||||||
rootTableName: string
|
rootTableName: string
|
||||||
table: Record<string, unknown>
|
table: RawTable
|
||||||
tableLocales?: Record<string, unknown>
|
tableLocales?: RawTable
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFlattenedFieldNames = (
|
const getFlattenedFieldNames = (
|
||||||
@@ -72,7 +74,7 @@ export const validateExistingBlockIsIdentical = ({
|
|||||||
// ensure every field from the config is in the matching table
|
// ensure every field from the config is in the matching table
|
||||||
fieldNames.find(({ name, localized }) => {
|
fieldNames.find(({ name, localized }) => {
|
||||||
const fieldTable = localized && tableLocales ? tableLocales : table
|
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
|
// ensure every table column is matched for every field from the config
|
||||||
Object.keys(table).find((fieldName) => {
|
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(
|
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.`,
|
`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.`,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user