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',
|
||||
payload,
|
||||
queryDrafts,
|
||||
rawRelations: {},
|
||||
rawTables: {},
|
||||
rejectInitializing,
|
||||
requireDrizzleKit,
|
||||
resolveInitializing,
|
||||
|
||||
@@ -100,6 +100,8 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
|
||||
operators,
|
||||
prodMigrations: args.prodMigrations,
|
||||
push: args.push,
|
||||
rawRelations: {},
|
||||
rawTables: {},
|
||||
relations: {},
|
||||
relationshipsSuffix: args.relationshipsSuffix || '_rels',
|
||||
schema: {},
|
||||
|
||||
@@ -1,138 +1,38 @@
|
||||
import type { DrizzleAdapter } from '@payloadcms/drizzle/types'
|
||||
import type { Init, SanitizedCollectionConfig } from 'payload'
|
||||
import type { Init } from 'payload'
|
||||
|
||||
import { createTableName, executeSchemaHooks } from '@payloadcms/drizzle'
|
||||
import { uniqueIndex } from 'drizzle-orm/sqlite-core'
|
||||
import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
import { buildDrizzleRelations, buildRawSchema, executeSchemaHooks } from '@payloadcms/drizzle'
|
||||
|
||||
import type { BaseExtraConfig } from './schema/build.js'
|
||||
import type { SQLiteAdapter } from './types.js'
|
||||
|
||||
import { buildTable } from './schema/build.js'
|
||||
import { buildDrizzleTable } from './schema/buildDrizzleTable.js'
|
||||
import { setColumnID } from './schema/setColumnID.js'
|
||||
|
||||
export const init: Init = async function init(this: SQLiteAdapter) {
|
||||
let locales: [string, ...string[]] | undefined
|
||||
await executeSchemaHooks({ type: 'beforeSchemaInit', adapter: this })
|
||||
let locales: string[] | undefined
|
||||
|
||||
this.rawRelations = {}
|
||||
this.rawTables = {}
|
||||
|
||||
if (this.payload.config.localization) {
|
||||
locales = this.payload.config.localization.locales.map(({ code }) => code) as [
|
||||
string,
|
||||
...string[],
|
||||
]
|
||||
locales = this.payload.config.localization.locales.map(({ code }) => code)
|
||||
}
|
||||
|
||||
this.payload.config.collections.forEach((collection: SanitizedCollectionConfig) => {
|
||||
createTableName({
|
||||
adapter: this as unknown as DrizzleAdapter,
|
||||
config: collection,
|
||||
})
|
||||
const adapter = this as unknown as DrizzleAdapter
|
||||
|
||||
if (collection.versions) {
|
||||
createTableName({
|
||||
adapter: this as unknown as DrizzleAdapter,
|
||||
config: collection,
|
||||
versions: true,
|
||||
versionsCustomName: true,
|
||||
})
|
||||
}
|
||||
})
|
||||
this.payload.config.collections.forEach((collection: SanitizedCollectionConfig) => {
|
||||
const tableName = this.tableNameMap.get(toSnakeCase(collection.slug))
|
||||
const config = this.payload.config
|
||||
|
||||
const baseExtraConfig: BaseExtraConfig = {}
|
||||
|
||||
if (collection.upload.filenameCompoundIndex) {
|
||||
const indexName = `${tableName}_filename_compound_idx`
|
||||
|
||||
baseExtraConfig.filename_compound_index = (cols) => {
|
||||
const colsConstraint = collection.upload.filenameCompoundIndex.map((f) => {
|
||||
return cols[f]
|
||||
})
|
||||
return uniqueIndex(indexName).on(colsConstraint[0], ...colsConstraint.slice(1))
|
||||
}
|
||||
}
|
||||
|
||||
if (collection.upload.filenameCompoundIndex) {
|
||||
const indexName = `${tableName}_filename_compound_idx`
|
||||
|
||||
baseExtraConfig.filename_compound_index = (cols) => {
|
||||
const colsConstraint = collection.upload.filenameCompoundIndex.map((f) => {
|
||||
return cols[f]
|
||||
})
|
||||
return uniqueIndex(indexName).on(colsConstraint[0], ...colsConstraint.slice(1))
|
||||
}
|
||||
}
|
||||
|
||||
buildTable({
|
||||
adapter: this,
|
||||
disableNotNull: !!collection?.versions?.drafts,
|
||||
disableUnique: false,
|
||||
fields: collection.flattenedFields,
|
||||
locales,
|
||||
tableName,
|
||||
timestamps: collection.timestamps,
|
||||
versions: false,
|
||||
})
|
||||
|
||||
if (collection.versions) {
|
||||
const versionsTableName = this.tableNameMap.get(
|
||||
`_${toSnakeCase(collection.slug)}${this.versionsSuffix}`,
|
||||
)
|
||||
const versionFields = buildVersionCollectionFields(config, collection, true)
|
||||
|
||||
buildTable({
|
||||
adapter: this,
|
||||
disableNotNull: !!collection.versions?.drafts,
|
||||
disableUnique: true,
|
||||
fields: versionFields,
|
||||
locales,
|
||||
tableName: versionsTableName,
|
||||
timestamps: true,
|
||||
versions: true,
|
||||
})
|
||||
}
|
||||
buildRawSchema({
|
||||
adapter,
|
||||
setColumnID,
|
||||
})
|
||||
|
||||
this.payload.config.globals.forEach((global) => {
|
||||
const tableName = createTableName({
|
||||
adapter: this as unknown as DrizzleAdapter,
|
||||
config: global,
|
||||
})
|
||||
await executeSchemaHooks({ type: 'beforeSchemaInit', adapter: this })
|
||||
|
||||
buildTable({
|
||||
adapter: this,
|
||||
disableNotNull: !!global?.versions?.drafts,
|
||||
disableUnique: false,
|
||||
fields: global.flattenedFields,
|
||||
locales,
|
||||
tableName,
|
||||
timestamps: false,
|
||||
versions: false,
|
||||
})
|
||||
for (const tableName in this.rawTables) {
|
||||
buildDrizzleTable({ adapter, locales, rawTable: this.rawTables[tableName] })
|
||||
}
|
||||
|
||||
if (global.versions) {
|
||||
const versionsTableName = createTableName({
|
||||
adapter: this as unknown as DrizzleAdapter,
|
||||
config: global,
|
||||
versions: true,
|
||||
versionsCustomName: true,
|
||||
})
|
||||
const config = this.payload.config
|
||||
const versionFields = buildVersionGlobalFields(config, global, true)
|
||||
|
||||
buildTable({
|
||||
adapter: this,
|
||||
disableNotNull: !!global.versions?.drafts,
|
||||
disableUnique: true,
|
||||
fields: versionFields,
|
||||
locales,
|
||||
tableName: versionsTableName,
|
||||
timestamps: true,
|
||||
versions: true,
|
||||
})
|
||||
}
|
||||
buildDrizzleRelations({
|
||||
adapter,
|
||||
})
|
||||
|
||||
await executeSchemaHooks({ type: 'afterSchemaInit', adapter: this })
|
||||
|
||||
@@ -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 { FlattenedField } from 'payload'
|
||||
import type { SetColumnID } from '@payloadcms/drizzle/types'
|
||||
|
||||
import { integer, numeric, text } from 'drizzle-orm/sqlite-core'
|
||||
|
||||
import type { IDType } from '../types.js'
|
||||
|
||||
type Args = {
|
||||
columns: Record<string, SQLiteColumnBuilder>
|
||||
fields: FlattenedField[]
|
||||
}
|
||||
export const setColumnID = ({ columns, fields }: Args): IDType => {
|
||||
export const setColumnID: SetColumnID = ({ columns, fields }) => {
|
||||
const idField = fields.find((field) => field.name === 'id')
|
||||
if (idField) {
|
||||
if (idField.type === 'number') {
|
||||
columns.id = numeric('id').primaryKey()
|
||||
columns.id = {
|
||||
name: 'id',
|
||||
type: 'numeric',
|
||||
primaryKey: true,
|
||||
}
|
||||
return 'numeric'
|
||||
}
|
||||
|
||||
if (idField.type === 'text') {
|
||||
columns.id = text('id').primaryKey()
|
||||
columns.id = {
|
||||
name: 'id',
|
||||
type: 'text',
|
||||
primaryKey: true,
|
||||
}
|
||||
return 'text'
|
||||
}
|
||||
}
|
||||
|
||||
columns.id = integer('id').primaryKey()
|
||||
columns.id = {
|
||||
name: 'id',
|
||||
type: 'integer',
|
||||
primaryKey: true,
|
||||
}
|
||||
|
||||
return 'integer'
|
||||
}
|
||||
|
||||
@@ -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[]
|
||||
client: Config
|
||||
/** Generated schema from payload generate:db-schema file path */
|
||||
generateSchemaOutputFile?: string
|
||||
idType?: 'serial' | 'uuid'
|
||||
localesSuffix?: string
|
||||
logger?: DrizzleConfig['logger']
|
||||
@@ -109,6 +111,16 @@ type SQLiteDrizzleAdapter = Omit<
|
||||
| 'relations'
|
||||
>
|
||||
|
||||
export interface GeneratedDatabaseSchema {
|
||||
schemaUntyped: Record<string, unknown>
|
||||
}
|
||||
|
||||
type ResolveSchemaType<T> = 'schema' extends keyof T
|
||||
? T['schema']
|
||||
: GeneratedDatabaseSchema['schemaUntyped']
|
||||
|
||||
type Drizzle = { $client: Client } & LibSQLDatabase<ResolveSchemaType<GeneratedDatabaseSchema>>
|
||||
|
||||
export type SQLiteAdapter = {
|
||||
afterSchemaInit: SQLiteSchemaHook[]
|
||||
beforeSchemaInit: SQLiteSchemaHook[]
|
||||
@@ -117,9 +129,7 @@ export type SQLiteAdapter = {
|
||||
countDistinct: CountDistinct
|
||||
defaultDrizzleSnapshot: any
|
||||
deleteWhere: DeleteWhere
|
||||
drizzle: { $client: Client } & LibSQLDatabase<
|
||||
Record<string, GenericRelation | GenericTable> & Record<string, unknown>
|
||||
>
|
||||
drizzle: Drizzle
|
||||
dropDatabase: DropDatabase
|
||||
execute: Execute<unknown>
|
||||
/**
|
||||
@@ -165,7 +175,7 @@ export type MigrateUpArgs = {
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
db: LibSQLDatabase
|
||||
db: Drizzle
|
||||
/**
|
||||
* The Payload instance that you can use to execute Local API methods
|
||||
* To use the current transaction you must pass `req` to arguments
|
||||
@@ -196,7 +206,7 @@ export type MigrateDownArgs = {
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
db: LibSQLDatabase
|
||||
db: Drizzle
|
||||
/**
|
||||
* The Payload instance that you can use to execute Local API methods
|
||||
* To use the current transaction you must pass `req` to arguments
|
||||
@@ -221,7 +231,7 @@ declare module 'payload' {
|
||||
extends Omit<Args, 'idType' | 'logger' | 'migrationDir' | 'pool'>,
|
||||
DrizzleAdapter {
|
||||
beginTransaction: (options?: SQLiteTransactionConfig) => Promise<null | number | string>
|
||||
drizzle: LibSQLDatabase
|
||||
drizzle: Drizzle
|
||||
/**
|
||||
* An object keyed on each table, with a key value pair where the constraint name is the key, followed by the dot-notation field name
|
||||
* Used for returning properly formed errors from unique fields
|
||||
|
||||
@@ -110,6 +110,8 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj<Verce
|
||||
poolOptions: args.pool,
|
||||
prodMigrations: args.prodMigrations,
|
||||
push: args.push,
|
||||
rawRelations: {},
|
||||
rawTables: {},
|
||||
relations: {},
|
||||
relationshipsSuffix: args.relationshipsSuffix || '_rels',
|
||||
schema: {},
|
||||
|
||||
@@ -26,6 +26,8 @@ export { migrateStatus } from './migrateStatus.js'
|
||||
export { operatorMap } from './queries/operatorMap.js'
|
||||
export type { Operators } from './queries/operatorMap.js'
|
||||
export { queryDrafts } from './queryDrafts.js'
|
||||
export { buildDrizzleRelations } from './schema/buildDrizzleRelations.js'
|
||||
export { buildRawSchema } from './schema/buildRawSchema.js'
|
||||
export { beginTransaction } from './transactions/beginTransaction.js'
|
||||
export { commitTransaction } from './transactions/commitTransaction.js'
|
||||
export { rollbackTransaction } from './transactions/rollbackTransaction.js'
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
import type { Init, SanitizedCollectionConfig } from 'payload'
|
||||
import type { Init } from 'payload'
|
||||
|
||||
import { uniqueIndex } from 'drizzle-orm/pg-core'
|
||||
import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
import type { BasePostgresAdapter } from './types.js'
|
||||
|
||||
import type { BaseExtraConfig, BasePostgresAdapter } from './types.js'
|
||||
|
||||
import { createTableName } from '../createTableName.js'
|
||||
import { buildDrizzleRelations } from '../schema/buildDrizzleRelations.js'
|
||||
import { buildRawSchema } from '../schema/buildRawSchema.js'
|
||||
import { executeSchemaHooks } from '../utilities/executeSchemaHooks.js'
|
||||
import { buildTable } from './schema/build.js'
|
||||
import { buildDrizzleTable } from './schema/buildDrizzleTable.js'
|
||||
import { setColumnID } from './schema/setColumnID.js'
|
||||
|
||||
export const init: Init = async function init(this: BasePostgresAdapter) {
|
||||
this.rawRelations = {}
|
||||
this.rawTables = {}
|
||||
|
||||
buildRawSchema({
|
||||
adapter: this,
|
||||
setColumnID,
|
||||
})
|
||||
|
||||
await executeSchemaHooks({ type: 'beforeSchemaInit', adapter: this })
|
||||
|
||||
if (this.payload.config.localization) {
|
||||
@@ -20,98 +26,12 @@ export const init: Init = async function init(this: BasePostgresAdapter) {
|
||||
)
|
||||
}
|
||||
|
||||
this.payload.config.collections.forEach((collection: SanitizedCollectionConfig) => {
|
||||
createTableName({
|
||||
adapter: this,
|
||||
config: collection,
|
||||
})
|
||||
for (const tableName in this.rawTables) {
|
||||
buildDrizzleTable({ adapter: this, rawTable: this.rawTables[tableName] })
|
||||
}
|
||||
|
||||
if (collection.versions) {
|
||||
createTableName({
|
||||
adapter: this,
|
||||
config: collection,
|
||||
versions: true,
|
||||
versionsCustomName: true,
|
||||
})
|
||||
}
|
||||
})
|
||||
this.payload.config.collections.forEach((collection: SanitizedCollectionConfig) => {
|
||||
const tableName = this.tableNameMap.get(toSnakeCase(collection.slug))
|
||||
|
||||
const baseExtraConfig: BaseExtraConfig = {}
|
||||
|
||||
if (collection.upload.filenameCompoundIndex) {
|
||||
const indexName = `${tableName}_filename_compound_idx`
|
||||
|
||||
baseExtraConfig.filename_compound_index = (cols) => {
|
||||
const colsConstraint = collection.upload.filenameCompoundIndex.map((f) => {
|
||||
return cols[f]
|
||||
})
|
||||
return uniqueIndex(indexName).on(colsConstraint[0], ...colsConstraint.slice(1))
|
||||
}
|
||||
}
|
||||
|
||||
buildTable({
|
||||
adapter: this,
|
||||
baseExtraConfig,
|
||||
disableNotNull: !!collection?.versions?.drafts,
|
||||
disableUnique: false,
|
||||
fields: collection.flattenedFields,
|
||||
tableName,
|
||||
timestamps: collection.timestamps,
|
||||
versions: false,
|
||||
})
|
||||
|
||||
if (collection.versions) {
|
||||
const versionsTableName = this.tableNameMap.get(
|
||||
`_${toSnakeCase(collection.slug)}${this.versionsSuffix}`,
|
||||
)
|
||||
const versionFields = buildVersionCollectionFields(this.payload.config, collection, true)
|
||||
|
||||
buildTable({
|
||||
adapter: this,
|
||||
disableNotNull: !!collection.versions?.drafts,
|
||||
disableUnique: true,
|
||||
fields: versionFields,
|
||||
tableName: versionsTableName,
|
||||
timestamps: true,
|
||||
versions: true,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
this.payload.config.globals.forEach((global) => {
|
||||
const tableName = createTableName({ adapter: this, config: global })
|
||||
|
||||
buildTable({
|
||||
adapter: this,
|
||||
disableNotNull: !!global?.versions?.drafts,
|
||||
disableUnique: false,
|
||||
fields: global.flattenedFields,
|
||||
tableName,
|
||||
timestamps: false,
|
||||
versions: false,
|
||||
})
|
||||
|
||||
if (global.versions) {
|
||||
const versionsTableName = createTableName({
|
||||
adapter: this,
|
||||
config: global,
|
||||
versions: true,
|
||||
versionsCustomName: true,
|
||||
})
|
||||
const versionFields = buildVersionGlobalFields(this.payload.config, global, true)
|
||||
|
||||
buildTable({
|
||||
adapter: this,
|
||||
disableNotNull: !!global.versions?.drafts,
|
||||
disableUnique: true,
|
||||
fields: versionFields,
|
||||
tableName: versionsTableName,
|
||||
timestamps: true,
|
||||
versions: true,
|
||||
})
|
||||
}
|
||||
buildDrizzleRelations({
|
||||
adapter: this,
|
||||
})
|
||||
|
||||
await executeSchemaHooks({ type: 'afterSchemaInit', adapter: this })
|
||||
|
||||
@@ -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 { FlattenedField } from 'payload'
|
||||
import type { SetColumnID } from '../../types.js'
|
||||
|
||||
import { numeric, serial, uuid, varchar } from 'drizzle-orm/pg-core'
|
||||
|
||||
import type { BasePostgresAdapter, IDType } from '../types.js'
|
||||
|
||||
type Args = {
|
||||
adapter: BasePostgresAdapter
|
||||
columns: Record<string, PgColumnBuilder>
|
||||
fields: FlattenedField[]
|
||||
}
|
||||
export const setColumnID = ({ adapter, columns, fields }: Args): IDType => {
|
||||
export const setColumnID: SetColumnID = ({ adapter, columns, fields }) => {
|
||||
const idField = fields.find((field) => field.name === 'id')
|
||||
if (idField) {
|
||||
if (idField.type === 'number') {
|
||||
columns.id = numeric('id').primaryKey()
|
||||
columns.id = {
|
||||
name: 'id',
|
||||
type: 'numeric',
|
||||
primaryKey: true,
|
||||
}
|
||||
|
||||
return 'numeric'
|
||||
}
|
||||
|
||||
if (idField.type === 'text') {
|
||||
columns.id = varchar('id').primaryKey()
|
||||
columns.id = {
|
||||
name: 'id',
|
||||
type: 'varchar',
|
||||
primaryKey: true,
|
||||
}
|
||||
return 'varchar'
|
||||
}
|
||||
}
|
||||
|
||||
if (adapter.idType === 'uuid') {
|
||||
columns.id = uuid('id').defaultRandom().primaryKey()
|
||||
columns.id = {
|
||||
name: 'id',
|
||||
type: 'uuid',
|
||||
defaultRandom: true,
|
||||
primaryKey: true,
|
||||
}
|
||||
|
||||
return 'uuid'
|
||||
}
|
||||
|
||||
columns.id = serial('id').primaryKey()
|
||||
columns.id = {
|
||||
name: 'id',
|
||||
type: 'serial',
|
||||
primaryKey: true,
|
||||
}
|
||||
|
||||
return 'integer'
|
||||
}
|
||||
|
||||
@@ -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 { relations } from 'drizzle-orm'
|
||||
import {
|
||||
boolean,
|
||||
foreignKey,
|
||||
index,
|
||||
integer,
|
||||
jsonb,
|
||||
numeric,
|
||||
PgNumericBuilder,
|
||||
PgUUIDBuilder,
|
||||
PgVarcharBuilder,
|
||||
text,
|
||||
timestamp,
|
||||
varchar,
|
||||
} from 'drizzle-orm/pg-core'
|
||||
import { InvalidConfiguration } from 'payload'
|
||||
import { fieldAffectsData, fieldIsVirtual, optionIsObject } from 'payload/shared'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type {
|
||||
BaseExtraConfig,
|
||||
BasePostgresAdapter,
|
||||
GenericColumns,
|
||||
DrizzleAdapter,
|
||||
IDType,
|
||||
RawColumn,
|
||||
RawForeignKey,
|
||||
RawIndex,
|
||||
RawRelation,
|
||||
RelationMap,
|
||||
SetColumnID,
|
||||
} from '../types.js'
|
||||
|
||||
import { createTableName } from '../../createTableName.js'
|
||||
import { buildIndexName } from '../../utilities/buildIndexName.js'
|
||||
import { hasLocalesTable } from '../../utilities/hasLocalesTable.js'
|
||||
import { validateExistingBlockIsIdentical } from '../../utilities/validateExistingBlockIsIdentical.js'
|
||||
import { createTableName } from '../createTableName.js'
|
||||
import { buildIndexName } from '../utilities/buildIndexName.js'
|
||||
import { hasLocalesTable } from '../utilities/hasLocalesTable.js'
|
||||
import { validateExistingBlockIsIdentical } from '../utilities/validateExistingBlockIsIdentical.js'
|
||||
import { buildTable } from './build.js'
|
||||
import { createIndex } from './createIndex.js'
|
||||
import { geometryColumn } from './geometryColumn.js'
|
||||
import { idToUUID } from './idToUUID.js'
|
||||
import { parentIDColumnMap } from './parentIDColumnMap.js'
|
||||
import { withDefault } from './withDefault.js'
|
||||
|
||||
type Args = {
|
||||
adapter: BasePostgresAdapter
|
||||
adapter: DrizzleAdapter
|
||||
columnPrefix?: string
|
||||
columns: Record<string, PgColumnBuilder>
|
||||
columns: Record<string, RawColumn>
|
||||
disableNotNull: boolean
|
||||
disableRelsTableUnique?: boolean
|
||||
disableUnique?: boolean
|
||||
fieldPrefix?: string
|
||||
fields: FlattenedField[]
|
||||
forceLocalized?: boolean
|
||||
indexes: Record<string, (cols: GenericColumns) => IndexBuilder>
|
||||
localesColumns: Record<string, PgColumnBuilder>
|
||||
localesIndexes: Record<string, (cols: GenericColumns) => IndexBuilder>
|
||||
indexes: Record<string, RawIndex>
|
||||
localesColumns: Record<string, RawColumn>
|
||||
localesIndexes: Record<string, RawIndex>
|
||||
newTableName: string
|
||||
parentTableName: string
|
||||
relationships: Set<string>
|
||||
relationsToBuild: RelationMap
|
||||
rootRelationsToBuild?: RelationMap
|
||||
rootTableIDColType: string
|
||||
rootTableIDColType: IDType
|
||||
rootTableName: string
|
||||
setColumnID: SetColumnID
|
||||
uniqueRelationships: Set<string>
|
||||
versions: boolean
|
||||
/**
|
||||
@@ -98,6 +82,7 @@ export const traverseFields = ({
|
||||
rootRelationsToBuild,
|
||||
rootTableIDColType,
|
||||
rootTableName,
|
||||
setColumnID,
|
||||
uniqueRelationships,
|
||||
versions,
|
||||
withinLocalizedArrayOrBlock,
|
||||
@@ -111,14 +96,11 @@ export const traverseFields = ({
|
||||
let hasLocalizedManyNumberField = false
|
||||
|
||||
let parentIDColType: IDType = 'integer'
|
||||
if (columns.id instanceof PgUUIDBuilder) {
|
||||
parentIDColType = 'uuid'
|
||||
}
|
||||
if (columns.id instanceof PgNumericBuilder) {
|
||||
parentIDColType = 'numeric'
|
||||
}
|
||||
if (columns.id instanceof PgVarcharBuilder) {
|
||||
parentIDColType = 'varchar'
|
||||
|
||||
const idColumn = columns.id
|
||||
|
||||
if (idColumn && ['numeric', 'text', 'uuid', 'varchar'].includes(idColumn.type)) {
|
||||
parentIDColType = idColumn.type as IDType
|
||||
}
|
||||
|
||||
fields.forEach((field) => {
|
||||
@@ -168,11 +150,11 @@ export const traverseFields = ({
|
||||
|
||||
const indexName = buildIndexName({ name: `${newTableName}_${columnName}`, adapter })
|
||||
|
||||
targetIndexes[indexName] = createIndex({
|
||||
name: field.localized ? [fieldName, '_locale'] : fieldName,
|
||||
indexName,
|
||||
targetIndexes[indexName] = {
|
||||
name: indexName,
|
||||
on: field.localized ? [fieldName, '_locale'] : fieldName,
|
||||
unique,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
switch (field.type) {
|
||||
@@ -188,20 +170,42 @@ export const traverseFields = ({
|
||||
versionsCustomName: versions,
|
||||
})
|
||||
|
||||
const baseColumns: Record<string, PgColumnBuilder> = {
|
||||
_order: integer('_order').notNull(),
|
||||
_parentID: parentIDColumnMap[parentIDColType]('_parent_id').notNull(),
|
||||
const baseColumns: Record<string, RawColumn> = {
|
||||
_order: {
|
||||
name: '_order',
|
||||
type: 'integer',
|
||||
notNull: true,
|
||||
},
|
||||
_parentID: {
|
||||
name: '_parent_id',
|
||||
type: parentIDColType,
|
||||
notNull: true,
|
||||
},
|
||||
}
|
||||
|
||||
const baseExtraConfig: BaseExtraConfig = {
|
||||
_orderIdx: (cols) => index(`${arrayTableName}_order_idx`).on(cols._order),
|
||||
_parentIDFk: (cols) =>
|
||||
foreignKey({
|
||||
name: `${arrayTableName}_parent_id_fk`,
|
||||
columns: [cols['_parentID']],
|
||||
foreignColumns: [adapter.tables[parentTableName].id],
|
||||
}).onDelete('cascade'),
|
||||
_parentIDIdx: (cols) => index(`${arrayTableName}_parent_id_idx`).on(cols._parentID),
|
||||
const baseIndexes: Record<string, RawIndex> = {
|
||||
_orderIdx: {
|
||||
name: `${arrayTableName}_order_idx`,
|
||||
on: ['_order'],
|
||||
},
|
||||
_parentIDIdx: {
|
||||
name: `${arrayTableName}_parent_id_idx`,
|
||||
on: '_parentID',
|
||||
},
|
||||
}
|
||||
|
||||
const baseForeignKeys: Record<string, RawForeignKey> = {
|
||||
_parentIDFk: {
|
||||
name: `${arrayTableName}_parent_id_fk`,
|
||||
columns: ['_parentID'],
|
||||
foreignColumns: [
|
||||
{
|
||||
name: 'id',
|
||||
table: parentTableName,
|
||||
},
|
||||
],
|
||||
onDelete: 'cascade',
|
||||
},
|
||||
}
|
||||
|
||||
const isLocalized =
|
||||
@@ -210,9 +214,17 @@ export const traverseFields = ({
|
||||
forceLocalized
|
||||
|
||||
if (isLocalized) {
|
||||
baseColumns._locale = adapter.enums.enum__locales('_locale').notNull()
|
||||
baseExtraConfig._localeIdx = (cols) =>
|
||||
index(`${arrayTableName}_locale_idx`).on(cols._locale)
|
||||
baseColumns._locale = {
|
||||
name: '_locale',
|
||||
type: 'enum',
|
||||
locale: true,
|
||||
notNull: true,
|
||||
}
|
||||
|
||||
baseIndexes._localeIdx = {
|
||||
name: `${arrayTableName}_locale_idx`,
|
||||
on: '_locale',
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
@@ -225,7 +237,8 @@ export const traverseFields = ({
|
||||
} = buildTable({
|
||||
adapter,
|
||||
baseColumns,
|
||||
baseExtraConfig,
|
||||
baseForeignKeys,
|
||||
baseIndexes,
|
||||
disableNotNull: disableNotNullFromHere,
|
||||
disableRelsTableUnique: true,
|
||||
disableUnique,
|
||||
@@ -235,6 +248,7 @@ export const traverseFields = ({
|
||||
rootTableIDColType,
|
||||
rootTableName,
|
||||
rootUniqueRelationships: uniqueRelationships,
|
||||
setColumnID,
|
||||
tableName: arrayTableName,
|
||||
versions,
|
||||
withinLocalizedArrayOrBlock: isLocalized,
|
||||
@@ -270,42 +284,59 @@ export const traverseFields = ({
|
||||
target: arrayTableName,
|
||||
})
|
||||
|
||||
adapter.relations[`relations_${arrayTableName}`] = relations(
|
||||
adapter.tables[arrayTableName],
|
||||
({ many, one }) => {
|
||||
const result: Record<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
|
||||
const arrayRelations: Record<string, RawRelation> = {
|
||||
_parentID: {
|
||||
type: 'one',
|
||||
fields: [
|
||||
{
|
||||
name: '_parentID',
|
||||
table: arrayTableName,
|
||||
},
|
||||
],
|
||||
references: ['id'],
|
||||
relationName: fieldName,
|
||||
to: parentTableName,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (hasLocalesTable(field.fields)) {
|
||||
arrayRelations._locales = {
|
||||
type: 'many',
|
||||
relationName: '_locales',
|
||||
to: `${arrayTableName}${adapter.localesSuffix}`,
|
||||
}
|
||||
}
|
||||
|
||||
subRelationsToBuild.forEach(({ type, localized, target }, key) => {
|
||||
if (type === 'one') {
|
||||
const arrayWithLocalized = localized
|
||||
? `${arrayTableName}${adapter.localesSuffix}`
|
||||
: arrayTableName
|
||||
|
||||
arrayRelations[key] = {
|
||||
type: 'one',
|
||||
fields: [
|
||||
{
|
||||
name: key,
|
||||
table: arrayWithLocalized,
|
||||
},
|
||||
],
|
||||
references: ['id'],
|
||||
relationName: key,
|
||||
to: target,
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'many') {
|
||||
arrayRelations[key] = {
|
||||
type: 'many',
|
||||
relationName: key,
|
||||
to: target,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
adapter.rawRelations[arrayTableName] = arrayRelations
|
||||
|
||||
break
|
||||
}
|
||||
@@ -321,23 +352,52 @@ export const traverseFields = ({
|
||||
throwValidationError,
|
||||
versionsCustomName: versions,
|
||||
})
|
||||
if (!adapter.tables[blockTableName]) {
|
||||
const baseColumns: Record<string, PgColumnBuilder> = {
|
||||
_order: integer('_order').notNull(),
|
||||
_parentID: parentIDColumnMap[rootTableIDColType]('_parent_id').notNull(),
|
||||
_path: text('_path').notNull(),
|
||||
if (!adapter.rawTables[blockTableName]) {
|
||||
const baseColumns: Record<string, RawColumn> = {
|
||||
_order: {
|
||||
name: '_order',
|
||||
type: 'integer',
|
||||
notNull: true,
|
||||
},
|
||||
_parentID: {
|
||||
name: '_parent_id',
|
||||
type: rootTableIDColType,
|
||||
notNull: true,
|
||||
},
|
||||
_path: {
|
||||
name: '_path',
|
||||
type: 'text',
|
||||
notNull: true,
|
||||
},
|
||||
}
|
||||
|
||||
const baseExtraConfig: BaseExtraConfig = {
|
||||
_orderIdx: (cols) => index(`${blockTableName}_order_idx`).on(cols._order),
|
||||
_parentIdFk: (cols) =>
|
||||
foreignKey({
|
||||
name: `${blockTableName}_parent_id_fk`,
|
||||
columns: [cols._parentID],
|
||||
foreignColumns: [adapter.tables[rootTableName].id],
|
||||
}).onDelete('cascade'),
|
||||
_parentIDIdx: (cols) => index(`${blockTableName}_parent_id_idx`).on(cols._parentID),
|
||||
_pathIdx: (cols) => index(`${blockTableName}_path_idx`).on(cols._path),
|
||||
const baseIndexes: Record<string, RawIndex> = {
|
||||
_orderIdx: {
|
||||
name: `${blockTableName}_order_idx`,
|
||||
on: '_order',
|
||||
},
|
||||
_parentIDIdx: {
|
||||
name: `${blockTableName}_parent_id_idx`,
|
||||
on: ['_parentID'],
|
||||
},
|
||||
_pathIdx: {
|
||||
name: `${blockTableName}_path_idx`,
|
||||
on: '_path',
|
||||
},
|
||||
}
|
||||
|
||||
const baseForeignKeys: Record<string, RawForeignKey> = {
|
||||
_parentIdFk: {
|
||||
name: `${blockTableName}_parent_id_fk`,
|
||||
columns: ['_parentID'],
|
||||
foreignColumns: [
|
||||
{
|
||||
name: 'id',
|
||||
table: rootTableName,
|
||||
},
|
||||
],
|
||||
onDelete: 'cascade',
|
||||
},
|
||||
}
|
||||
|
||||
const isLocalized =
|
||||
@@ -346,9 +406,17 @@ export const traverseFields = ({
|
||||
forceLocalized
|
||||
|
||||
if (isLocalized) {
|
||||
baseColumns._locale = adapter.enums.enum__locales('_locale').notNull()
|
||||
baseExtraConfig._localeIdx = (cols) =>
|
||||
index(`${blockTableName}_locale_idx`).on(cols._locale)
|
||||
baseColumns._locale = {
|
||||
name: '_locale',
|
||||
type: 'enum',
|
||||
locale: true,
|
||||
notNull: true,
|
||||
}
|
||||
|
||||
baseIndexes._localeIdx = {
|
||||
name: `${blockTableName}_locale_idx`,
|
||||
on: '_locale',
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
@@ -361,7 +429,8 @@ export const traverseFields = ({
|
||||
} = buildTable({
|
||||
adapter,
|
||||
baseColumns,
|
||||
baseExtraConfig,
|
||||
baseForeignKeys,
|
||||
baseIndexes,
|
||||
disableNotNull: disableNotNullFromHere,
|
||||
disableRelsTableUnique: true,
|
||||
disableUnique,
|
||||
@@ -371,6 +440,7 @@ export const traverseFields = ({
|
||||
rootTableIDColType,
|
||||
rootTableName,
|
||||
rootUniqueRelationships: uniqueRelationships,
|
||||
setColumnID,
|
||||
tableName: blockTableName,
|
||||
versions,
|
||||
withinLocalizedArrayOrBlock: isLocalized,
|
||||
@@ -400,50 +470,66 @@ export const traverseFields = ({
|
||||
}
|
||||
}
|
||||
|
||||
adapter.relations[`relations_${blockTableName}`] = relations(
|
||||
adapter.tables[blockTableName],
|
||||
({ many, one }) => {
|
||||
const result: Record<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
|
||||
const blockRelations: Record<string, RawRelation> = {
|
||||
_parentID: {
|
||||
type: 'one',
|
||||
fields: [
|
||||
{
|
||||
name: '_parentID',
|
||||
table: blockTableName,
|
||||
},
|
||||
],
|
||||
references: ['id'],
|
||||
relationName: `_blocks_${block.slug}`,
|
||||
to: rootTableName,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (hasLocalesTable(block.fields)) {
|
||||
blockRelations._locales = {
|
||||
type: 'many',
|
||||
relationName: '_locales',
|
||||
to: `${blockTableName}${adapter.localesSuffix}`,
|
||||
}
|
||||
}
|
||||
|
||||
subRelationsToBuild.forEach(({ type, localized, target }, key) => {
|
||||
if (type === 'one') {
|
||||
const blockWithLocalized = localized
|
||||
? `${blockTableName}${adapter.localesSuffix}`
|
||||
: blockTableName
|
||||
|
||||
blockRelations[key] = {
|
||||
type: 'one',
|
||||
fields: [
|
||||
{
|
||||
name: key,
|
||||
table: blockWithLocalized,
|
||||
},
|
||||
],
|
||||
references: ['id'],
|
||||
relationName: key,
|
||||
to: target,
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'many') {
|
||||
blockRelations[key] = {
|
||||
type: 'many',
|
||||
relationName: key,
|
||||
to: target,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
adapter.rawRelations[blockTableName] = blockRelations
|
||||
} else if (process.env.NODE_ENV !== 'production' && !versions) {
|
||||
validateExistingBlockIsIdentical({
|
||||
block,
|
||||
localized: field.localized,
|
||||
rootTableName,
|
||||
table: adapter.tables[blockTableName],
|
||||
tableLocales: adapter.tables[`${blockTableName}${adapter.localesSuffix}`],
|
||||
table: adapter.rawTables[blockTableName],
|
||||
tableLocales: adapter.rawTables[`${blockTableName}${adapter.localesSuffix}`],
|
||||
})
|
||||
}
|
||||
// blocks relationships are defined from the collection or globals table down to the block, bypassing any subBlocks
|
||||
@@ -458,26 +544,43 @@ export const traverseFields = ({
|
||||
break
|
||||
}
|
||||
case 'checkbox': {
|
||||
targetTable[fieldName] = withDefault(boolean(columnName), field)
|
||||
targetTable[fieldName] = withDefault(
|
||||
{
|
||||
name: columnName,
|
||||
type: 'boolean',
|
||||
},
|
||||
field,
|
||||
)
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'code':
|
||||
case 'email':
|
||||
case 'textarea': {
|
||||
targetTable[fieldName] = withDefault(varchar(columnName), field)
|
||||
targetTable[fieldName] = withDefault(
|
||||
{
|
||||
name: columnName,
|
||||
type: 'varchar',
|
||||
},
|
||||
field,
|
||||
)
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'date': {
|
||||
targetTable[fieldName] = withDefault(
|
||||
timestamp(columnName, {
|
||||
{
|
||||
name: columnName,
|
||||
type: 'timestamp',
|
||||
mode: 'string',
|
||||
precision: 3,
|
||||
withTimezone: true,
|
||||
}),
|
||||
},
|
||||
field,
|
||||
)
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
@@ -511,6 +614,7 @@ export const traverseFields = ({
|
||||
rootRelationsToBuild,
|
||||
rootTableIDColType,
|
||||
rootTableName,
|
||||
setColumnID,
|
||||
uniqueRelationships,
|
||||
versions,
|
||||
withinLocalizedArrayOrBlock: withinLocalizedArrayOrBlock || field.localized,
|
||||
@@ -539,7 +643,14 @@ export const traverseFields = ({
|
||||
|
||||
case 'json':
|
||||
case 'richText': {
|
||||
targetTable[fieldName] = withDefault(jsonb(columnName), field)
|
||||
targetTable[fieldName] = withDefault(
|
||||
{
|
||||
name: columnName,
|
||||
type: 'jsonb',
|
||||
},
|
||||
field,
|
||||
)
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
@@ -566,16 +677,27 @@ export const traverseFields = ({
|
||||
)
|
||||
}
|
||||
} else {
|
||||
targetTable[fieldName] = withDefault(numeric(columnName), field)
|
||||
targetTable[fieldName] = withDefault(
|
||||
{
|
||||
name: columnName,
|
||||
type: 'numeric',
|
||||
},
|
||||
field,
|
||||
)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'point': {
|
||||
targetTable[fieldName] = withDefault(geometryColumn(columnName), field)
|
||||
if (!adapter.extensions.postgis) {
|
||||
adapter.extensions.postgis = true
|
||||
}
|
||||
targetTable[fieldName] = withDefault(
|
||||
{
|
||||
name: columnName,
|
||||
type: 'geometry',
|
||||
},
|
||||
field,
|
||||
)
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
@@ -590,16 +712,13 @@ export const traverseFields = ({
|
||||
throwValidationError,
|
||||
})
|
||||
|
||||
adapter.enums[enumName] = adapter.pgSchema.enum(
|
||||
enumName,
|
||||
field.options.map((option) => {
|
||||
if (optionIsObject(option)) {
|
||||
return option.value
|
||||
}
|
||||
const options = field.options.map((option) => {
|
||||
if (optionIsObject(option)) {
|
||||
return option.value
|
||||
}
|
||||
|
||||
return option
|
||||
}) as [string, ...string[]],
|
||||
)
|
||||
return option
|
||||
})
|
||||
|
||||
if (field.type === 'select' && field.hasMany) {
|
||||
const selectTableName = createTableName({
|
||||
@@ -610,21 +729,56 @@ export const traverseFields = ({
|
||||
throwValidationError,
|
||||
versionsCustomName: versions,
|
||||
})
|
||||
const baseColumns: Record<string, PgColumnBuilder> = {
|
||||
order: integer('order').notNull(),
|
||||
parent: parentIDColumnMap[parentIDColType]('parent_id').notNull(),
|
||||
value: adapter.enums[enumName]('value'),
|
||||
|
||||
const baseColumns: Record<string, RawColumn> = {
|
||||
order: {
|
||||
name: 'order',
|
||||
type: 'integer',
|
||||
notNull: true,
|
||||
},
|
||||
parent: {
|
||||
name: 'parent_id',
|
||||
type: parentIDColType,
|
||||
notNull: true,
|
||||
},
|
||||
value: {
|
||||
name: 'value',
|
||||
type: 'enum',
|
||||
enumName: createTableName({
|
||||
adapter,
|
||||
config: field,
|
||||
parentTableName: newTableName,
|
||||
prefix: `enum_${newTableName}_`,
|
||||
target: 'enumName',
|
||||
throwValidationError,
|
||||
}),
|
||||
options,
|
||||
},
|
||||
}
|
||||
|
||||
const baseExtraConfig: BaseExtraConfig = {
|
||||
orderIdx: (cols) => index(`${selectTableName}_order_idx`).on(cols.order),
|
||||
parentFk: (cols) =>
|
||||
foreignKey({
|
||||
name: `${selectTableName}_parent_fk`,
|
||||
columns: [cols.parent],
|
||||
foreignColumns: [adapter.tables[parentTableName].id],
|
||||
}).onDelete('cascade'),
|
||||
parentIdx: (cols) => index(`${selectTableName}_parent_idx`).on(cols.parent),
|
||||
const baseIndexes: Record<string, RawIndex> = {
|
||||
orderIdx: {
|
||||
name: `${selectTableName}_order_idx`,
|
||||
on: 'order',
|
||||
},
|
||||
parentIdx: {
|
||||
name: `${selectTableName}_parent_idx`,
|
||||
on: 'parent',
|
||||
},
|
||||
}
|
||||
|
||||
const baseForeignKeys: Record<string, RawForeignKey> = {
|
||||
parentFk: {
|
||||
name: `${selectTableName}_parent_fk`,
|
||||
columns: ['parent'],
|
||||
foreignColumns: [
|
||||
{
|
||||
name: 'id',
|
||||
table: parentTableName,
|
||||
},
|
||||
],
|
||||
onDelete: 'cascade',
|
||||
},
|
||||
}
|
||||
|
||||
const isLocalized =
|
||||
@@ -633,23 +787,36 @@ export const traverseFields = ({
|
||||
forceLocalized
|
||||
|
||||
if (isLocalized) {
|
||||
baseColumns.locale = adapter.enums.enum__locales('locale').notNull()
|
||||
baseExtraConfig.localeIdx = (cols) =>
|
||||
index(`${selectTableName}_locale_idx`).on(cols.locale)
|
||||
baseColumns.locale = {
|
||||
name: 'locale',
|
||||
type: 'enum',
|
||||
locale: true,
|
||||
notNull: true,
|
||||
}
|
||||
|
||||
baseIndexes.localeIdx = {
|
||||
name: `${selectTableName}_locale_idx`,
|
||||
on: 'locale',
|
||||
}
|
||||
}
|
||||
|
||||
if (field.index) {
|
||||
baseExtraConfig.value = (cols) => index(`${selectTableName}_value_idx`).on(cols.value)
|
||||
baseIndexes.value = {
|
||||
name: `${selectTableName}_value_idx`,
|
||||
on: 'value',
|
||||
}
|
||||
}
|
||||
|
||||
buildTable({
|
||||
adapter,
|
||||
baseColumns,
|
||||
baseExtraConfig,
|
||||
baseForeignKeys,
|
||||
baseIndexes,
|
||||
disableNotNull,
|
||||
disableUnique,
|
||||
fields: [],
|
||||
rootTableName,
|
||||
setColumnID,
|
||||
tableName: selectTableName,
|
||||
versions,
|
||||
})
|
||||
@@ -661,18 +828,30 @@ export const traverseFields = ({
|
||||
target: selectTableName,
|
||||
})
|
||||
|
||||
adapter.relations[`relations_${selectTableName}`] = relations(
|
||||
adapter.tables[selectTableName],
|
||||
({ one }) => ({
|
||||
parent: one(adapter.tables[parentTableName], {
|
||||
fields: [adapter.tables[selectTableName].parent],
|
||||
references: [adapter.tables[parentTableName].id],
|
||||
relationName: fieldName,
|
||||
}),
|
||||
}),
|
||||
)
|
||||
adapter.rawRelations[selectTableName] = {
|
||||
parent: {
|
||||
type: 'one',
|
||||
fields: [
|
||||
{
|
||||
name: 'parent',
|
||||
table: selectTableName,
|
||||
},
|
||||
],
|
||||
references: ['id'],
|
||||
relationName: fieldName,
|
||||
to: parentTableName,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
targetTable[fieldName] = withDefault(adapter.enums[enumName](columnName), field)
|
||||
targetTable[fieldName] = withDefault(
|
||||
{
|
||||
name: columnName,
|
||||
type: 'enum',
|
||||
enumName,
|
||||
options,
|
||||
},
|
||||
field,
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -698,7 +877,7 @@ export const traverseFields = ({
|
||||
const tableName = adapter.tableNameMap.get(toSnakeCase(field.relationTo))
|
||||
|
||||
// get the id type of the related collection
|
||||
let colType = adapter.idType === 'uuid' ? 'uuid' : 'integer'
|
||||
let colType: IDType = adapter.idType === 'uuid' ? 'uuid' : 'integer'
|
||||
const relatedCollectionCustomID = relationshipConfig.fields.find(
|
||||
(field) => fieldAffectsData(field) && field.name === 'id',
|
||||
)
|
||||
@@ -710,10 +889,15 @@ export const traverseFields = ({
|
||||
}
|
||||
|
||||
// make the foreign key column for relationship using the correct id column type
|
||||
targetTable[fieldName] = parentIDColumnMap[colType](`${columnName}_id`).references(
|
||||
() => adapter.tables[tableName].id,
|
||||
{ onDelete: 'set null' },
|
||||
)
|
||||
targetTable[fieldName] = {
|
||||
name: `${columnName}_id`,
|
||||
type: colType,
|
||||
reference: {
|
||||
name: 'id',
|
||||
onDelete: 'set null',
|
||||
table: tableName,
|
||||
},
|
||||
}
|
||||
|
||||
// add relationship to table
|
||||
relationsToBuild.set(fieldName, {
|
||||
@@ -724,7 +908,7 @@ export const traverseFields = ({
|
||||
|
||||
// add notNull when not required
|
||||
if (!disableNotNull && field.required && !field.admin?.condition) {
|
||||
targetTable[fieldName].notNull()
|
||||
targetTable[fieldName].notNull = true
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -761,7 +945,13 @@ export const traverseFields = ({
|
||||
)
|
||||
}
|
||||
} else {
|
||||
targetTable[fieldName] = withDefault(varchar(columnName), field)
|
||||
targetTable[fieldName] = withDefault(
|
||||
{
|
||||
name: columnName,
|
||||
type: 'varchar',
|
||||
},
|
||||
field,
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -779,7 +969,7 @@ export const traverseFields = ({
|
||||
field.required &&
|
||||
!condition
|
||||
) {
|
||||
targetTable[fieldName].notNull()
|
||||
targetTable[fieldName].notNull = true
|
||||
}
|
||||
})
|
||||
|
||||
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'
|
||||
import type { LibSQLDatabase } from 'drizzle-orm/libsql'
|
||||
import type { NodePgDatabase, NodePgQueryResultHKT } from 'drizzle-orm/node-postgres'
|
||||
import type { PgColumn, PgTable, PgTransaction } from 'drizzle-orm/pg-core'
|
||||
import type {
|
||||
PgColumn,
|
||||
PgTable,
|
||||
PgTransaction,
|
||||
Precision,
|
||||
UpdateDeleteAction,
|
||||
} from 'drizzle-orm/pg-core'
|
||||
import type { SQLiteColumn, SQLiteTable, SQLiteTransaction } from 'drizzle-orm/sqlite-core'
|
||||
import type { Result } from 'drizzle-orm/sqlite-core/session'
|
||||
import type { BaseDatabaseAdapter, MigrationData, Payload, PayloadRequest } from 'payload'
|
||||
import type {
|
||||
BaseDatabaseAdapter,
|
||||
FlattenedField,
|
||||
MigrationData,
|
||||
Payload,
|
||||
PayloadRequest,
|
||||
} from 'payload'
|
||||
|
||||
import type { BuildQueryJoinAliases } from './queries/buildQuery.js'
|
||||
|
||||
@@ -157,6 +169,129 @@ export type CreateJSONQueryArgs = {
|
||||
value: boolean | number | string
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract relation link
|
||||
*/
|
||||
export type RawRelation =
|
||||
| {
|
||||
fields: { name: string; table: string }[]
|
||||
references: string[]
|
||||
relationName?: string
|
||||
to: string
|
||||
type: 'one'
|
||||
}
|
||||
| {
|
||||
relationName?: string
|
||||
to: string
|
||||
type: 'many'
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract SQL table that later gets converted by database specific implementation to Drizzle
|
||||
*/
|
||||
export type RawTable = {
|
||||
columns: Record<string, RawColumn>
|
||||
foreignKeys?: Record<string, RawForeignKey>
|
||||
indexes?: Record<string, RawIndex>
|
||||
name: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract SQL foreign key that later gets converted by database specific implementation to Drizzle
|
||||
*/
|
||||
export type RawForeignKey = {
|
||||
columns: string[]
|
||||
foreignColumns: { name: string; table: string }[]
|
||||
name: string
|
||||
onDelete?: UpdateDeleteAction
|
||||
onUpdate?: UpdateDeleteAction
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract SQL index that later gets converted by database specific implementation to Drizzle
|
||||
*/
|
||||
export type RawIndex = {
|
||||
name: string
|
||||
on: string | string[]
|
||||
unique?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract SQL column that later gets converted by database specific implementation to Drizzle
|
||||
*/
|
||||
export type BaseRawColumn = {
|
||||
default?: any
|
||||
name: string
|
||||
notNull?: boolean
|
||||
primaryKey?: boolean
|
||||
reference?: {
|
||||
name: string
|
||||
onDelete: UpdateDeleteAction
|
||||
table: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Postgres: native timestamp type
|
||||
* SQLite: text column, defaultNow achieved through strftime('%Y-%m-%dT%H:%M:%fZ', 'now'). withTimezone/precision have no any effect.
|
||||
*/
|
||||
export type TimestampRawColumn = {
|
||||
defaultNow?: boolean
|
||||
mode: 'date' | 'string'
|
||||
precision: Precision
|
||||
type: 'timestamp'
|
||||
withTimezone?: boolean
|
||||
} & BaseRawColumn
|
||||
|
||||
/**
|
||||
* Postgres: native UUID type and db lavel defaultRandom
|
||||
* SQLite: text type and defaultRandom in the app level
|
||||
*/
|
||||
export type UUIDRawColumn = {
|
||||
defaultRandom?: boolean
|
||||
type: 'uuid'
|
||||
} & BaseRawColumn
|
||||
|
||||
/**
|
||||
* Accepts either `locale: true` to have options from locales or `options` string array
|
||||
* Postgres: native enums
|
||||
* SQLite: text column with checks.
|
||||
*/
|
||||
export type EnumRawColumn = (
|
||||
| {
|
||||
enumName: string
|
||||
options: string[]
|
||||
type: 'enum'
|
||||
}
|
||||
| {
|
||||
locale: true
|
||||
type: 'enum'
|
||||
}
|
||||
) &
|
||||
BaseRawColumn
|
||||
|
||||
export type RawColumn =
|
||||
| ({
|
||||
type: 'boolean' | 'geometry' | 'integer' | 'jsonb' | 'numeric' | 'serial' | 'text' | 'varchar'
|
||||
} & BaseRawColumn)
|
||||
| EnumRawColumn
|
||||
| TimestampRawColumn
|
||||
| UUIDRawColumn
|
||||
|
||||
export type IDType = 'integer' | 'numeric' | 'text' | 'uuid' | 'varchar'
|
||||
|
||||
export type SetColumnID = (args: {
|
||||
adapter: DrizzleAdapter
|
||||
columns: Record<string, RawColumn>
|
||||
fields: FlattenedField[]
|
||||
}) => IDType
|
||||
|
||||
export type BuildDrizzleTable<T extends DrizzleAdapter = DrizzleAdapter> = (args: {
|
||||
adapter: T
|
||||
locales: string[]
|
||||
rawTable: RawTable
|
||||
}) => void
|
||||
|
||||
export interface DrizzleAdapter extends BaseDatabaseAdapter {
|
||||
convertPathToJSONTraversal?: (incomingSegments: string[]) => string
|
||||
countDistinct: CountDistinct
|
||||
@@ -184,7 +319,10 @@ export interface DrizzleAdapter extends BaseDatabaseAdapter {
|
||||
logger: DrizzleConfig['logger']
|
||||
operators: Operators
|
||||
push: boolean
|
||||
rawRelations: Record<string, Record<string, RawRelation>>
|
||||
rawTables: Record<string, RawTable>
|
||||
rejectInitializing: () => void
|
||||
|
||||
relations: Record<string, GenericRelation>
|
||||
relationshipsSuffix?: string
|
||||
requireDrizzleKit: RequireDrizzleKit
|
||||
@@ -204,3 +342,13 @@ export interface DrizzleAdapter extends BaseDatabaseAdapter {
|
||||
transactionOptions: unknown
|
||||
versionsSuffix?: string
|
||||
}
|
||||
|
||||
export type RelationMap = Map<
|
||||
string,
|
||||
{
|
||||
localized: boolean
|
||||
relationName?: string
|
||||
target: string
|
||||
type: 'many' | 'one'
|
||||
}
|
||||
>
|
||||
|
||||
@@ -27,9 +27,9 @@ type Args = {
|
||||
}
|
||||
|
||||
export const executeSchemaHooks = async ({ type, adapter }: Args): Promise<void> => {
|
||||
for (const hook of adapter[type]) {
|
||||
for (const hook of (adapter as unknown as Adapter)[type]) {
|
||||
const result = await hook({
|
||||
adapter,
|
||||
adapter: adapter as unknown as Adapter,
|
||||
extendTable: extendDrizzleTable,
|
||||
schema: {
|
||||
enums: adapter.enums,
|
||||
|
||||
@@ -3,12 +3,14 @@ import type { Block, Field } from 'payload'
|
||||
import { InvalidConfiguration } from 'payload'
|
||||
import { fieldAffectsData, fieldHasSubFields, tabHasName } from 'payload/shared'
|
||||
|
||||
import type { RawTable } from '../types.js'
|
||||
|
||||
type Args = {
|
||||
block: Block
|
||||
localized: boolean
|
||||
rootTableName: string
|
||||
table: Record<string, unknown>
|
||||
tableLocales?: Record<string, unknown>
|
||||
table: RawTable
|
||||
tableLocales?: RawTable
|
||||
}
|
||||
|
||||
const getFlattenedFieldNames = (
|
||||
@@ -72,7 +74,7 @@ export const validateExistingBlockIsIdentical = ({
|
||||
// ensure every field from the config is in the matching table
|
||||
fieldNames.find(({ name, localized }) => {
|
||||
const fieldTable = localized && tableLocales ? tableLocales : table
|
||||
return Object.keys(fieldTable).indexOf(name) === -1
|
||||
return Object.keys(fieldTable.columns).indexOf(name) === -1
|
||||
}) ||
|
||||
// ensure every table column is matched for every field from the config
|
||||
Object.keys(table).find((fieldName) => {
|
||||
@@ -91,7 +93,7 @@ export const validateExistingBlockIsIdentical = ({
|
||||
)
|
||||
}
|
||||
|
||||
if (Boolean(localized) !== Boolean(table._locale)) {
|
||||
if (Boolean(localized) !== Boolean(table.columns._locale)) {
|
||||
throw new InvalidConfiguration(
|
||||
`The table ${rootTableName} has multiple blocks with slug ${block.slug}, but the schemas do not match. One is localized, but another is not. Block schemas of the same name must match exactly.`,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user