feat(drizzle): customize schema with before / after init hooks (#8196)
Adds abillity to customize the generated Drizzle schema with
`beforeSchemaInit` and `afterSchemaInit`. Could be useful if you want to
preserve the existing database schema / override the generated one with
features that aren't supported from the Payload config.
## Docs:
### beforeSchemaInit
Runs before the schema is built. You can use this hook to extend your
database structure with tables that won't be managed by Payload.
```ts
import { postgresAdapter } from '@payloadcms/db-postgres'
import { integer, pgTable, serial } from 'drizzle-orm/pg-core'
postgresAdapter({
beforeSchemaInit: [
({ schema, adapter }) => {
return {
...schema,
tables: {
...schema.tables,
addedTable: pgTable('added_table', {
id: serial('id').notNull(),
}),
},
}
},
],
})
```
One use case is preserving your existing database structure when
migrating to Payload. By default, Payload drops the current database
schema, which may not be desirable in this scenario.
To quickly generate the Drizzle schema from your database you can use
[Drizzle
Introspection](https://orm.drizzle.team/kit-docs/commands#introspect--pull)
You should get the `schema.ts` file which may look like this:
```ts
import { pgTable, uniqueIndex, serial, varchar, text } from 'drizzle-orm/pg-core'
export const users = pgTable('users', {
id: serial('id').primaryKey(),
fullName: text('full_name'),
phone: varchar('phone', { length: 256 }),
})
export const countries = pgTable(
'countries',
{
id: serial('id').primaryKey(),
name: varchar('name', { length: 256 }),
},
(countries) => {
return {
nameIndex: uniqueIndex('name_idx').on(countries.name),
}
},
)
```
You can import them into your config and append to the schema with the
`beforeSchemaInit` hook like this:
```ts
import { postgresAdapter } from '@payloadcms/db-postgres'
import { users, countries } from '../drizzle/schema'
postgresAdapter({
beforeSchemaInit: [
({ schema, adapter }) => {
return {
...schema,
tables: {
...schema.tables,
users,
countries
},
}
},
],
})
```
Make sure Payload doesn't overlap table names with its collections. For
example, if you already have a collection with slug "users", you should
either change the slug or `dbName` to change the table name for this
collection.
### afterSchemaInit
Runs after the Drizzle schema is built. You can use this hook to modify
the schema with features that aren't supported by Payload, or if you
want to add a column that you don't want to be in the Payload config.
To extend a table, Payload exposes `extendTable` utillity to the args.
You can refer to the [Drizzle
documentation](https://orm.drizzle.team/docs/sql-schema-declaration).
The following example adds the `extra_integer_column` column and a
composite index on `country` and `city` columns.
```ts
import { postgresAdapter } from '@payloadcms/db-postgres'
import { index, integer } from 'drizzle-orm/pg-core'
import { buildConfig } from 'payload'
export default buildConfig({
collections: [
{
slug: 'places',
fields: [
{
name: 'country',
type: 'text',
},
{
name: 'city',
type: 'text',
},
],
},
],
db: postgresAdapter({
afterSchemaInit: [
({ schema, extendTable, adapter }) => {
extendTable({
table: schema.tables.places,
columns: {
extraIntegerColumn: integer('extra_integer_column'),
},
extraConfig: (table) => ({
country_city_composite_index: index('country_city_composite_index').on(
table.country,
table.city,
),
}),
})
return schema
},
],
}),
})
```
<!--
For external contributors, please include:
- A summary of the pull request and any related issues it fixes.
- Reasoning for the changes made or any additional context that may be
useful.
Ensure you have read and understand the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository.
-->
This commit is contained in:
@@ -63,6 +63,8 @@ export default buildConfig({
|
||||
| `localesSuffix` | A string appended to the end of table names for storing localized fields. Default is '_locales'. |
|
||||
| `relationshipsSuffix` | A string appended to the end of table names for storing relationships. Default is '_rels'. |
|
||||
| `versionsSuffix` | A string appended to the end of table names for storing versions. Defaults to '_v'. |
|
||||
| `beforeSchemaInit` | Drizzle schema hook. Runs before the schema is built. [More Details](#beforeschemainit) |
|
||||
| `afterSchemaInit` | Drizzle schema hook. Runs after the schema is built. [More Details](#afterschemainit) |
|
||||
|
||||
## Access to Drizzle
|
||||
|
||||
@@ -97,3 +99,134 @@ Alternatively, you can disable `push` and rely solely on migrations to keep your
|
||||
In Postgres, migrations are a fundamental aspect of working with Payload and you should become familiar with how they work.
|
||||
|
||||
For more information about migrations, [click here](/docs/beta/database/migrations#when-to-run-migrations).
|
||||
|
||||
## Drizzle schema hooks
|
||||
|
||||
### beforeSchemaInit
|
||||
|
||||
Runs before the schema is built. You can use this hook to extend your database structure with tables that won't be managed by Payload.
|
||||
|
||||
```ts
|
||||
import { postgresAdapter } from '@payloadcms/db-postgres'
|
||||
import { integer, pgTable, serial } from 'drizzle-orm/pg-core'
|
||||
|
||||
postgresAdapter({
|
||||
beforeSchemaInit: [
|
||||
({ schema, adapter }) => {
|
||||
return {
|
||||
...schema,
|
||||
tables: {
|
||||
...schema.tables,
|
||||
addedTable: pgTable('added_table', {
|
||||
id: serial('id').notNull(),
|
||||
}),
|
||||
},
|
||||
}
|
||||
},
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
One use case is preserving your existing database structure when migrating to Payload. By default, Payload drops the current database schema, which may not be desirable in this scenario.
|
||||
To quickly generate the Drizzle schema from your database you can use [Drizzle Introspection](https://orm.drizzle.team/kit-docs/commands#introspect--pull)
|
||||
You should get the `schema.ts` file which may look like this:
|
||||
|
||||
```ts
|
||||
import { pgTable, uniqueIndex, serial, varchar, text } from 'drizzle-orm/pg-core'
|
||||
|
||||
export const users = pgTable('users', {
|
||||
id: serial('id').primaryKey(),
|
||||
fullName: text('full_name'),
|
||||
phone: varchar('phone', { length: 256 }),
|
||||
})
|
||||
|
||||
export const countries = pgTable(
|
||||
'countries',
|
||||
{
|
||||
id: serial('id').primaryKey(),
|
||||
name: varchar('name', { length: 256 }),
|
||||
},
|
||||
(countries) => {
|
||||
return {
|
||||
nameIndex: uniqueIndex('name_idx').on(countries.name),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
You can import them into your config and append to the schema with the `beforeSchemaInit` hook like this:
|
||||
|
||||
```ts
|
||||
import { postgresAdapter } from '@payloadcms/db-postgres'
|
||||
import { users, countries } from '../drizzle/schema'
|
||||
|
||||
postgresAdapter({
|
||||
beforeSchemaInit: [
|
||||
({ schema, adapter }) => {
|
||||
return {
|
||||
...schema,
|
||||
tables: {
|
||||
...schema.tables,
|
||||
users,
|
||||
countries
|
||||
},
|
||||
}
|
||||
},
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
Make sure Payload doesn't overlap table names with its collections. For example, if you already have a collection with slug "users", you should either change the slug or `dbName` to change the table name for this collection.
|
||||
|
||||
|
||||
### afterSchemaInit
|
||||
|
||||
Runs after the Drizzle schema is built. You can use this hook to modify the schema with features that aren't supported by Payload, or if you want to add a column that you don't want to be in the Payload config.
|
||||
To extend a table, Payload exposes `extendTable` utillity to the args. You can refer to the [Drizzle documentation](https://orm.drizzle.team/docs/sql-schema-declaration).
|
||||
The following example adds the `extra_integer_column` column and a composite index on `country` and `city` columns.
|
||||
|
||||
```ts
|
||||
import { postgresAdapter } from '@payloadcms/db-postgres'
|
||||
import { index, integer } from 'drizzle-orm/pg-core'
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
export default buildConfig({
|
||||
collections: [
|
||||
{
|
||||
slug: 'places',
|
||||
fields: [
|
||||
{
|
||||
name: 'country',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'city',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
db: postgresAdapter({
|
||||
afterSchemaInit: [
|
||||
({ schema, extendTable, adapter }) => {
|
||||
extendTable({
|
||||
table: schema.tables.places,
|
||||
columns: {
|
||||
extraIntegerColumn: integer('extra_integer_column'),
|
||||
},
|
||||
extraConfig: (table) => ({
|
||||
country_city_composite_index: index('country_city_composite_index').on(
|
||||
table.country,
|
||||
table.city,
|
||||
),
|
||||
}),
|
||||
})
|
||||
|
||||
return schema
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
|
||||
```
|
||||
|
||||
@@ -35,7 +35,7 @@ export default buildConfig({
|
||||
## Options
|
||||
|
||||
| Option | Description |
|
||||
|-----------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `client` \* | [Client connection options](https://orm.drizzle.team/docs/get-started-sqlite#turso) that will be passed to `createClient` from `@libsql/client`. |
|
||||
| `push` | Disable Drizzle's [`db push`](https://orm.drizzle.team/kit-docs/overview#prototyping-with-db-push) in development mode. By default, `push` is enabled for development mode only. |
|
||||
| `migrationDir` | Customize the directory that migrations are stored. |
|
||||
@@ -44,8 +44,8 @@ export default buildConfig({
|
||||
| `localesSuffix` | A string appended to the end of table names for storing localized fields. Default is '_locales'. |
|
||||
| `relationshipsSuffix` | A string appended to the end of table names for storing relationships. Default is '_rels'. |
|
||||
| `versionsSuffix` | A string appended to the end of table names for storing versions. Defaults to '_v'. |
|
||||
|
||||
|
||||
| `beforeSchemaInit` | Drizzle schema hook. Runs before the schema is built. [More Details](#beforeschemainit) |
|
||||
| `afterSchemaInit` | Drizzle schema hook. Runs after the schema is built. [More Details](#afterschemainit) |
|
||||
|
||||
## Access to Drizzle
|
||||
|
||||
@@ -79,3 +79,134 @@ Alternatively, you can disable `push` and rely solely on migrations to keep your
|
||||
In SQLite, migrations are a fundamental aspect of working with Payload and you should become familiar with how they work.
|
||||
|
||||
For more information about migrations, [click here](/docs/beta/database/migrations#when-to-run-migrations).
|
||||
|
||||
## Drizzle schema hooks
|
||||
|
||||
### beforeSchemaInit
|
||||
|
||||
Runs before the schema is built. You can use this hook to extend your database structure with tables that won't be managed by Payload.
|
||||
|
||||
```ts
|
||||
import { sqliteAdapter } from '@payloadcms/db-sqlite'
|
||||
import { integer, sqliteTable } from 'drizzle-orm/sqlite-core'
|
||||
|
||||
sqliteAdapter({
|
||||
beforeSchemaInit: [
|
||||
({ schema, adapter }) => {
|
||||
return {
|
||||
...schema,
|
||||
tables: {
|
||||
...schema.tables,
|
||||
addedTable: sqliteTable('added_table', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
}),
|
||||
},
|
||||
}
|
||||
},
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
One use case is preserving your existing database structure when migrating to Payload. By default, Payload drops the current database schema, which may not be desirable in this scenario.
|
||||
To quickly generate the Drizzle schema from your database you can use [Drizzle Introspection](https://orm.drizzle.team/kit-docs/commands#introspect--pull)
|
||||
You should get the `schema.ts` file which may look like this:
|
||||
|
||||
```ts
|
||||
import { sqliteTable, text, uniqueIndex, integer } from 'drizzle-orm/sqlite-core'
|
||||
|
||||
export const users = sqliteTable('users', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
fullName: text('full_name'),
|
||||
phone: text('phone', {length: 256}),
|
||||
})
|
||||
|
||||
export const countries = sqliteTable(
|
||||
'countries',
|
||||
{
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
name: text('name', { length: 256 }),
|
||||
},
|
||||
(countries) => {
|
||||
return {
|
||||
nameIndex: uniqueIndex('name_idx').on(countries.name),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
You can import them into your config and append to the schema with the `beforeSchemaInit` hook like this:
|
||||
|
||||
```ts
|
||||
import { sqliteAdapter } from '@payloadcms/db-sqlite'
|
||||
import { users, countries } from '../drizzle/schema'
|
||||
|
||||
sqliteAdapter({
|
||||
beforeSchemaInit: [
|
||||
({ schema, adapter }) => {
|
||||
return {
|
||||
...schema,
|
||||
tables: {
|
||||
...schema.tables,
|
||||
users,
|
||||
countries
|
||||
},
|
||||
}
|
||||
},
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
Make sure Payload doesn't overlap table names with its collections. For example, if you already have a collection with slug "users", you should either change the slug or `dbName` to change the table name for this collection.
|
||||
|
||||
|
||||
### afterSchemaInit
|
||||
|
||||
Runs after the Drizzle schema is built. You can use this hook to modify the schema with features that aren't supported by Payload, or if you want to add a column that you don't want to be in the Payload config.
|
||||
To extend a table, Payload exposes `extendTable` utillity to the args. You can refer to the [Drizzle documentation](https://orm.drizzle.team/docs/sql-schema-declaration).
|
||||
The following example adds the `extra_integer_column` column and a composite index on `country` and `city` columns.
|
||||
|
||||
```ts
|
||||
import { sqliteAdapter } from '@payloadcms/db-sqlite'
|
||||
import { index, integer } from 'drizzle-orm/sqlite-core'
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
export default buildConfig({
|
||||
collections: [
|
||||
{
|
||||
slug: 'places',
|
||||
fields: [
|
||||
{
|
||||
name: 'country',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'city',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
db: sqliteAdapter({
|
||||
afterSchemaInit: [
|
||||
({ schema, extendTable, adapter }) => {
|
||||
extendTable({
|
||||
table: schema.tables.places,
|
||||
columns: {
|
||||
extraIntegerColumn: integer('extra_integer_column'),
|
||||
},
|
||||
extraConfig: (table) => ({
|
||||
country_city_composite_index: index('country_city_composite_index').on(
|
||||
table.country,
|
||||
table.city,
|
||||
),
|
||||
}),
|
||||
})
|
||||
|
||||
return schema
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
|
||||
```
|
||||
|
||||
@@ -76,6 +76,8 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
|
||||
|
||||
return createDatabaseAdapter<PostgresAdapter>({
|
||||
name: 'postgres',
|
||||
afterSchemaInit: args.afterSchemaInit ?? [],
|
||||
beforeSchemaInit: args.beforeSchemaInit ?? [],
|
||||
defaultDrizzleSnapshot,
|
||||
drizzle: undefined,
|
||||
enums: {},
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
MigrateDownArgs,
|
||||
MigrateUpArgs,
|
||||
PostgresDB,
|
||||
PostgresSchemaHook,
|
||||
} from '@payloadcms/drizzle/postgres'
|
||||
import type { DrizzleAdapter } from '@payloadcms/drizzle/types'
|
||||
import type { DrizzleConfig } from 'drizzle-orm'
|
||||
@@ -11,6 +12,18 @@ import type { PgSchema, PgTableFn, PgTransactionConfig } from 'drizzle-orm/pg-co
|
||||
import type { Pool, PoolConfig } from 'pg'
|
||||
|
||||
export type Args = {
|
||||
/**
|
||||
* Transform the schema after it's built.
|
||||
* You can use it to customize the schema with features that aren't supported by Payload.
|
||||
* Examples may include: composite indices, generated columns, vectors
|
||||
*/
|
||||
afterSchemaInit?: PostgresSchemaHook[]
|
||||
/**
|
||||
* Transform the schema before it's built.
|
||||
* You can use it to preserve an existing database schema and if there are any collissions Payload will override them.
|
||||
* To generate Drizzle schema from the database, see [Drizzle Kit introspection](https://orm.drizzle.team/kit-docs/commands#introspect--pull)
|
||||
*/
|
||||
beforeSchemaInit?: PostgresSchemaHook[]
|
||||
idType?: 'serial' | 'uuid'
|
||||
localesSuffix?: string
|
||||
logger?: DrizzleConfig['logger']
|
||||
@@ -41,6 +54,8 @@ declare module 'payload' {
|
||||
export interface DatabaseAdapter
|
||||
extends Omit<Args, 'idType' | 'logger' | 'migrationDir' | 'pool'>,
|
||||
DrizzleAdapter {
|
||||
afterSchemaInit: PostgresSchemaHook[]
|
||||
beforeSchemaInit: PostgresSchemaHook[]
|
||||
beginTransaction: (options?: PgTransactionConfig) => Promise<null | number | string>
|
||||
drizzle: PostgresDB
|
||||
enums: Record<string, GenericEnum>
|
||||
|
||||
@@ -79,6 +79,8 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
|
||||
|
||||
return createDatabaseAdapter<SQLiteAdapter>({
|
||||
name: 'sqlite',
|
||||
afterSchemaInit: args.afterSchemaInit ?? [],
|
||||
beforeSchemaInit: args.beforeSchemaInit ?? [],
|
||||
client: undefined,
|
||||
clientConfig: args.client,
|
||||
defaultDrizzleSnapshot,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { DrizzleAdapter } from '@payloadcms/drizzle/types'
|
||||
import type { Init, SanitizedCollectionConfig } from 'payload'
|
||||
|
||||
import { createTableName } from '@payloadcms/drizzle'
|
||||
import { createTableName, executeSchemaHooks } from '@payloadcms/drizzle'
|
||||
import { uniqueIndex } from 'drizzle-orm/sqlite-core'
|
||||
import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
@@ -11,8 +11,10 @@ import type { SQLiteAdapter } from './types.js'
|
||||
|
||||
import { buildTable } from './schema/build.js'
|
||||
|
||||
export const init: Init = function init(this: SQLiteAdapter) {
|
||||
export const init: Init = async function init(this: SQLiteAdapter) {
|
||||
let locales: [string, ...string[]] | undefined
|
||||
await executeSchemaHooks({ type: 'beforeSchemaInit', adapter: this })
|
||||
|
||||
if (this.payload.config.localization) {
|
||||
locales = this.payload.config.localization.locales.map(({ code }) => code) as [
|
||||
string,
|
||||
@@ -132,4 +134,6 @@ export const init: Init = function init(this: SQLiteAdapter) {
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
await executeSchemaHooks({ type: 'afterSchemaInit', adapter: this })
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Client, Config, ResultSet } from '@libsql/client'
|
||||
import type { Operators } from '@payloadcms/drizzle'
|
||||
import type { extendDrizzleTable, Operators } from '@payloadcms/drizzle'
|
||||
import type { BuildQueryJoinAliases, DrizzleAdapter } from '@payloadcms/drizzle/types'
|
||||
import type { DrizzleConfig, Relation, Relations, SQL } from 'drizzle-orm'
|
||||
import type { LibSQLDatabase } from 'drizzle-orm/libsql'
|
||||
@@ -12,7 +12,31 @@ import type {
|
||||
import type { SQLiteRaw } from 'drizzle-orm/sqlite-core/query-builders/raw'
|
||||
import type { Payload, PayloadRequest } from 'payload'
|
||||
|
||||
type SQLiteSchema = {
|
||||
relations: Record<string, GenericRelation>
|
||||
tables: Record<string, SQLiteTableWithColumns<any>>
|
||||
}
|
||||
|
||||
type SQLiteSchemaHookArgs = {
|
||||
extendTable: typeof extendDrizzleTable
|
||||
schema: SQLiteSchema
|
||||
}
|
||||
|
||||
export type SQLiteSchemaHook = (args: SQLiteSchemaHookArgs) => Promise<SQLiteSchema> | SQLiteSchema
|
||||
|
||||
export type Args = {
|
||||
/**
|
||||
* Transform the schema after it's built.
|
||||
* You can use it to customize the schema with features that aren't supported by Payload.
|
||||
* Examples may include: composite indices, generated columns, vectors
|
||||
*/
|
||||
afterSchemaInit?: SQLiteSchemaHook[]
|
||||
/**
|
||||
* Transform the schema before it's built.
|
||||
* You can use it to preserve an existing database schema and if there are any collissions Payload will override them.
|
||||
* To generate Drizzle schema from the database, see [Drizzle Kit introspection](https://orm.drizzle.team/kit-docs/commands#introspect--pull)
|
||||
*/
|
||||
beforeSchemaInit?: SQLiteSchemaHook[]
|
||||
client: Config
|
||||
idType?: 'serial' | 'uuid'
|
||||
localesSuffix?: string
|
||||
@@ -86,6 +110,8 @@ type SQLiteDrizzleAdapter = Omit<
|
||||
>
|
||||
|
||||
export type SQLiteAdapter = {
|
||||
afterSchemaInit: SQLiteSchemaHook[]
|
||||
beforeSchemaInit: SQLiteSchemaHook[]
|
||||
client: Client
|
||||
clientConfig: Args['client']
|
||||
countDistinct: CountDistinct
|
||||
|
||||
@@ -76,6 +76,8 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj<Verce
|
||||
|
||||
return createDatabaseAdapter<VercelPostgresAdapter>({
|
||||
name: 'postgres',
|
||||
afterSchemaInit: args.afterSchemaInit ?? [],
|
||||
beforeSchemaInit: args.beforeSchemaInit ?? [],
|
||||
defaultDrizzleSnapshot,
|
||||
drizzle: undefined,
|
||||
enums: {},
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
MigrateDownArgs,
|
||||
MigrateUpArgs,
|
||||
PostgresDB,
|
||||
PostgresSchemaHook,
|
||||
} from '@payloadcms/drizzle/postgres'
|
||||
import type { DrizzleAdapter } from '@payloadcms/drizzle/types'
|
||||
import type { VercelPool, VercelPostgresPoolConfig } from '@vercel/postgres'
|
||||
@@ -11,6 +12,18 @@ import type { DrizzleConfig } from 'drizzle-orm'
|
||||
import type { PgSchema, PgTableFn, PgTransactionConfig } from 'drizzle-orm/pg-core'
|
||||
|
||||
export type Args = {
|
||||
/**
|
||||
* Transform the schema after it's built.
|
||||
* You can use it to customize the schema with features that aren't supported by Payload.
|
||||
* Examples may include: composite indices, generated columns, vectors
|
||||
*/
|
||||
afterSchemaInit?: PostgresSchemaHook[]
|
||||
/**
|
||||
* Transform the schema before it's built.
|
||||
* You can use it to preserve an existing database schema and if there are any collissions Payload will override them.
|
||||
* To generate Drizzle schema from the database, see [Drizzle Kit introspection](https://orm.drizzle.team/kit-docs/commands#introspect--pull)
|
||||
*/
|
||||
beforeSchemaInit?: PostgresSchemaHook[]
|
||||
connectionString?: string
|
||||
idType?: 'serial' | 'uuid'
|
||||
localesSuffix?: string
|
||||
@@ -46,6 +59,8 @@ declare module 'payload' {
|
||||
export interface DatabaseAdapter
|
||||
extends Omit<Args, 'idType' | 'logger' | 'migrationDir' | 'pool'>,
|
||||
DrizzleAdapter {
|
||||
afterSchemaInit: PostgresSchemaHook[]
|
||||
beforeSchemaInit: PostgresSchemaHook[]
|
||||
beginTransaction: (options?: PgTransactionConfig) => Promise<null | number | string>
|
||||
drizzle: PostgresDB
|
||||
enums: Record<string, GenericEnum>
|
||||
|
||||
@@ -32,6 +32,8 @@ export { updateGlobal } from './updateGlobal.js'
|
||||
export { updateGlobalVersion } from './updateGlobalVersion.js'
|
||||
export { updateVersion } from './updateVersion.js'
|
||||
export { upsertRow } from './upsertRow/index.js'
|
||||
export { executeSchemaHooks } from './utilities/executeSchemaHooks.js'
|
||||
export { extendDrizzleTable } from './utilities/extendDrizzleTable.js'
|
||||
export { hasLocalesTable } from './utilities/hasLocalesTable.js'
|
||||
export { pushDevSchema } from './utilities/pushDevSchema.js'
|
||||
export { validateExistingBlockIsIdentical } from './utilities/validateExistingBlockIsIdentical.js'
|
||||
|
||||
@@ -7,9 +7,12 @@ import toSnakeCase from 'to-snake-case'
|
||||
import type { BaseExtraConfig, BasePostgresAdapter } from './types.js'
|
||||
|
||||
import { createTableName } from '../createTableName.js'
|
||||
import { executeSchemaHooks } from '../utilities/executeSchemaHooks.js'
|
||||
import { buildTable } from './schema/build.js'
|
||||
|
||||
export const init: Init = function init(this: BasePostgresAdapter) {
|
||||
export const init: Init = async function init(this: BasePostgresAdapter) {
|
||||
await executeSchemaHooks({ type: 'beforeSchemaInit', adapter: this })
|
||||
|
||||
if (this.payload.config.localization) {
|
||||
this.enums.enum__locales = this.pgSchema.enum(
|
||||
'_locales',
|
||||
@@ -110,4 +113,6 @@ export const init: Init = function init(this: BasePostgresAdapter) {
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
await executeSchemaHooks({ type: 'afterSchemaInit', adapter: this })
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import type { PgTableFn } from 'drizzle-orm/pg-core/table'
|
||||
import type { Payload, PayloadRequest } from 'payload'
|
||||
import type { QueryResult } from 'pg'
|
||||
|
||||
import type { Operators } from '../index.js'
|
||||
import type { extendDrizzleTable, Operators } from '../index.js'
|
||||
import type { BuildQueryJoinAliases, DrizzleAdapter, TransactionPg } from '../types.js'
|
||||
|
||||
export type BaseExtraConfig = Record<
|
||||
@@ -99,7 +99,25 @@ type Schema =
|
||||
}
|
||||
| PgSchema
|
||||
|
||||
type PostgresSchema = {
|
||||
enums: Record<string, GenericEnum>
|
||||
relations: Record<string, GenericRelation>
|
||||
tables: Record<string, PgTableWithColumns<any>>
|
||||
}
|
||||
|
||||
type PostgresSchemaHookArgs = {
|
||||
adapter: PostgresDrizzleAdapter
|
||||
extendTable: typeof extendDrizzleTable
|
||||
schema: PostgresSchema
|
||||
}
|
||||
|
||||
export type PostgresSchemaHook = (
|
||||
args: PostgresSchemaHookArgs,
|
||||
) => PostgresSchema | Promise<PostgresSchema>
|
||||
|
||||
export type BasePostgresAdapter = {
|
||||
afterSchemaInit: PostgresSchemaHook[]
|
||||
beforeSchemaInit: PostgresSchemaHook[]
|
||||
countDistinct: CountDistinct
|
||||
defaultDrizzleSnapshot: DrizzleSnapshotJSON
|
||||
deleteWhere: DeleteWhere
|
||||
|
||||
47
packages/drizzle/src/utilities/executeSchemaHooks.ts
Normal file
47
packages/drizzle/src/utilities/executeSchemaHooks.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { DrizzleAdapter } from '../types.js'
|
||||
|
||||
import { extendDrizzleTable } from './extendDrizzleTable.js'
|
||||
|
||||
type DatabaseSchema = {
|
||||
enums?: DrizzleAdapter['enums']
|
||||
relations: Record<string, any>
|
||||
tables: DrizzleAdapter['tables']
|
||||
}
|
||||
|
||||
type Adapter = {
|
||||
afterSchemaInit: DatabaseSchemaHook[]
|
||||
beforeSchemaInit: DatabaseSchemaHook[]
|
||||
} & DatabaseSchema
|
||||
|
||||
type DatabaseSchemaHookArgs = {
|
||||
adapter: Record<string, unknown>
|
||||
extendTable: typeof extendDrizzleTable
|
||||
schema: DatabaseSchema
|
||||
}
|
||||
|
||||
type DatabaseSchemaHook = (args: DatabaseSchemaHookArgs) => DatabaseSchema | Promise<DatabaseSchema>
|
||||
|
||||
type Args = {
|
||||
adapter: Adapter
|
||||
type: 'afterSchemaInit' | 'beforeSchemaInit'
|
||||
}
|
||||
|
||||
export const executeSchemaHooks = async ({ type, adapter }: Args): Promise<void> => {
|
||||
for (const hook of adapter[type]) {
|
||||
const result = await hook({
|
||||
adapter,
|
||||
extendTable: extendDrizzleTable,
|
||||
schema: {
|
||||
enums: adapter.enums,
|
||||
relations: adapter.relations,
|
||||
tables: adapter.tables,
|
||||
},
|
||||
})
|
||||
if (result.enums) {
|
||||
adapter.enums = result.enums
|
||||
}
|
||||
|
||||
adapter.tables = result.tables
|
||||
adapter.relations = result.relations
|
||||
}
|
||||
}
|
||||
63
packages/drizzle/src/utilities/extendDrizzleTable.ts
Normal file
63
packages/drizzle/src/utilities/extendDrizzleTable.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Implemented from:
|
||||
* https://github.com/drizzle-team/drizzle-orm/blob/main/drizzle-orm/src/pg-core/table.ts#L73
|
||||
* Drizzle uses @internal JSDoc to remove their internal methods from types, for example
|
||||
* Table.Symbol, columnBuilder.build - but they actually exist.
|
||||
*/
|
||||
import type { ColumnBuilderBase } from 'drizzle-orm'
|
||||
|
||||
import { Table } from 'drizzle-orm'
|
||||
import { APIError } from 'payload'
|
||||
|
||||
const { Symbol: DrizzleSymbol } = Table as unknown as {
|
||||
Symbol: {
|
||||
Columns: symbol
|
||||
ExtraConfigBuilder: symbol
|
||||
ExtraConfigColumns: symbol
|
||||
}
|
||||
}
|
||||
|
||||
type Args = {
|
||||
columns?: Record<string, ColumnBuilderBase<any>>
|
||||
extraConfig?: (self: Record<string, any>) => object
|
||||
table: Table
|
||||
}
|
||||
|
||||
/**
|
||||
* Extends the passed table with additional columns / extra config
|
||||
*/
|
||||
export const extendDrizzleTable = ({ columns, extraConfig, table }: Args): void => {
|
||||
const InlineForeignKeys = Object.getOwnPropertySymbols(table).find((symbol) => {
|
||||
return symbol.description?.includes('InlineForeignKeys')
|
||||
})
|
||||
|
||||
if (!InlineForeignKeys) {
|
||||
throw new APIError(`Error when finding InlineForeignKeys Symbol`, 500)
|
||||
}
|
||||
|
||||
if (columns) {
|
||||
for (const [name, columnBuilder] of Object.entries(columns) as [string, any][]) {
|
||||
const column = columnBuilder.build(table)
|
||||
|
||||
table[name] = column
|
||||
table[InlineForeignKeys].push(...columnBuilder.buildForeignKeys(column, table))
|
||||
table[DrizzleSymbol.Columns][name] = column
|
||||
|
||||
table[DrizzleSymbol.ExtraConfigColumns][name] =
|
||||
'buildExtraConfigColumn' in columnBuilder
|
||||
? columnBuilder.buildExtraConfigColumn(table)
|
||||
: column
|
||||
}
|
||||
}
|
||||
|
||||
if (extraConfig) {
|
||||
const originalExtraConfigBuilder = table[DrizzleSymbol.ExtraConfigBuilder]
|
||||
|
||||
table[DrizzleSymbol.ExtraConfigBuilder] = (t) => {
|
||||
return {
|
||||
...originalExtraConfigBuilder(t),
|
||||
...extraConfig(t),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -275,6 +275,19 @@ export default buildConfigWithDefaults({
|
||||
drafts: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: 'places',
|
||||
fields: [
|
||||
{
|
||||
name: 'country',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'city',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'fields-persistance',
|
||||
fields: [
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import type { MongooseAdapter } from '@payloadcms/db-mongodb'
|
||||
import type { PostgresAdapter } from '@payloadcms/db-postgres/types'
|
||||
import type { Table } from 'drizzle-orm'
|
||||
import type { NextRESTClient } from 'helpers/NextRESTClient.js'
|
||||
import type { Payload, PayloadRequest, TypeWithID } from 'payload'
|
||||
|
||||
import * as drizzlePg from 'drizzle-orm/pg-core'
|
||||
import * as drizzleSqlite from 'drizzle-orm/sqlite-core'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { commitTransaction, initTransaction, QueryError } from 'payload'
|
||||
@@ -477,6 +480,232 @@ describe('database', () => {
|
||||
expect(result.select).toStrictEqual('default')
|
||||
})
|
||||
})
|
||||
describe('drizzle: schema hooks', () => {
|
||||
it('should add tables with hooks', async () => {
|
||||
// eslint-disable-next-line jest/no-conditional-in-test
|
||||
if (payload.db.name === 'mongoose') {
|
||||
return
|
||||
}
|
||||
|
||||
let added_table_before: Table
|
||||
let added_table_after: Table
|
||||
|
||||
// eslint-disable-next-line jest/no-conditional-in-test
|
||||
if (payload.db.name.includes('postgres')) {
|
||||
added_table_before = drizzlePg.pgTable('added_table_before', {
|
||||
id: drizzlePg.serial('id').primaryKey(),
|
||||
text: drizzlePg.text('text'),
|
||||
})
|
||||
|
||||
added_table_after = drizzlePg.pgTable('added_table_after', {
|
||||
id: drizzlePg.serial('id').primaryKey(),
|
||||
text: drizzlePg.text('text'),
|
||||
})
|
||||
}
|
||||
|
||||
// eslint-disable-next-line jest/no-conditional-in-test
|
||||
if (payload.db.name.includes('sqlite')) {
|
||||
added_table_before = drizzleSqlite.sqliteTable('added_table_before', {
|
||||
id: drizzleSqlite.integer('id').primaryKey(),
|
||||
text: drizzleSqlite.text('text'),
|
||||
})
|
||||
|
||||
added_table_after = drizzleSqlite.sqliteTable('added_table_after', {
|
||||
id: drizzleSqlite.integer('id').primaryKey(),
|
||||
text: drizzleSqlite.text('text'),
|
||||
})
|
||||
}
|
||||
|
||||
payload.db.beforeSchemaInit = [
|
||||
({ schema }) => ({
|
||||
...schema,
|
||||
tables: {
|
||||
...schema.tables,
|
||||
added_table_before,
|
||||
},
|
||||
}),
|
||||
]
|
||||
|
||||
payload.db.afterSchemaInit = [
|
||||
({ schema }) => {
|
||||
return {
|
||||
...schema,
|
||||
tables: {
|
||||
...schema.tables,
|
||||
added_table_after,
|
||||
},
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
delete payload.db.pool
|
||||
await payload.db.init()
|
||||
|
||||
expect(payload.db.tables.added_table_before).toBeDefined()
|
||||
|
||||
await payload.db.connect()
|
||||
|
||||
await payload.db.execute({
|
||||
drizzle: payload.db.drizzle,
|
||||
raw: `INSERT into added_table_before (text) VALUES ('some-text')`,
|
||||
})
|
||||
|
||||
const res_before = await payload.db.execute({
|
||||
drizzle: payload.db.drizzle,
|
||||
raw: 'SELECT * from added_table_before',
|
||||
})
|
||||
expect(res_before.rows[0].text).toBe('some-text')
|
||||
|
||||
await payload.db.execute({
|
||||
drizzle: payload.db.drizzle,
|
||||
raw: `INSERT into added_table_after (text) VALUES ('some-text')`,
|
||||
})
|
||||
|
||||
const res_after = await payload.db.execute({
|
||||
drizzle: payload.db.drizzle,
|
||||
raw: `SELECT * from added_table_after`,
|
||||
})
|
||||
|
||||
expect(res_after.rows[0].text).toBe('some-text')
|
||||
})
|
||||
|
||||
it('should extend the existing table with extra column and modify the existing column with enforcing DB level length', async () => {
|
||||
// eslint-disable-next-line jest/no-conditional-in-test
|
||||
if (payload.db.name === 'mongoose') {
|
||||
return
|
||||
}
|
||||
|
||||
const isSQLite = payload.db.name === 'sqlite'
|
||||
|
||||
payload.db.afterSchemaInit = [
|
||||
({ schema, extendTable }) => {
|
||||
extendTable({
|
||||
table: schema.tables.places,
|
||||
columns: {
|
||||
// SQLite doesn't have DB length enforcement
|
||||
// eslint-disable-next-line jest/no-conditional-in-test
|
||||
...(payload.db.name === 'postgres' && {
|
||||
city: drizzlePg.varchar('city', { length: 10 }),
|
||||
}),
|
||||
// eslint-disable-next-line jest/no-conditional-in-test
|
||||
extraColumn: isSQLite
|
||||
? drizzleSqlite.integer('extra_column')
|
||||
: drizzlePg.integer('extra_column'),
|
||||
},
|
||||
})
|
||||
|
||||
return schema
|
||||
},
|
||||
]
|
||||
|
||||
delete payload.db.pool
|
||||
await payload.db.init()
|
||||
await payload.db.connect()
|
||||
|
||||
expect(payload.db.tables.places.extraColumn).toBeDefined()
|
||||
|
||||
await payload.create({
|
||||
collection: 'places',
|
||||
data: {
|
||||
city: 'Berlin',
|
||||
country: 'Germany',
|
||||
},
|
||||
})
|
||||
|
||||
// eslint-disable-next-line jest/no-conditional-in-test
|
||||
const tableName = payload.db.schemaName ? `"${payload.db.schemaName}"."places"` : 'places'
|
||||
|
||||
await payload.db.execute({
|
||||
drizzle: payload.db.drizzle,
|
||||
raw: `UPDATE ${tableName} SET extra_column = 10`,
|
||||
})
|
||||
|
||||
const res_with_extra_col = await payload.db.execute({
|
||||
drizzle: payload.db.drizzle,
|
||||
raw: `SELECT * from ${tableName}`,
|
||||
})
|
||||
|
||||
expect(res_with_extra_col.rows[0].extra_column).toBe(10)
|
||||
|
||||
// SQLite doesn't have DB length enforcement
|
||||
// eslint-disable-next-line jest/no-conditional-in-test
|
||||
if (payload.db.name === 'postgres') {
|
||||
await expect(
|
||||
payload.db.execute({
|
||||
drizzle: payload.db.drizzle,
|
||||
raw: `UPDATE ${tableName} SET city = 'MoreThan10Chars'`,
|
||||
}),
|
||||
).rejects.toBeTruthy()
|
||||
}
|
||||
})
|
||||
|
||||
it('should extend the existing table with composite unique and throw ValidationError on it', async () => {
|
||||
// eslint-disable-next-line jest/no-conditional-in-test
|
||||
if (payload.db.name === 'mongoose') {
|
||||
return
|
||||
}
|
||||
|
||||
const isSQLite = payload.db.name === 'sqlite'
|
||||
|
||||
payload.db.afterSchemaInit = [
|
||||
({ schema, extendTable }) => {
|
||||
extendTable({
|
||||
table: schema.tables.places,
|
||||
extraConfig: (t) => ({
|
||||
// eslint-disable-next-line jest/no-conditional-in-test
|
||||
uniqueOnCityAndCountry: (isSQLite ? drizzleSqlite : drizzlePg)
|
||||
.unique()
|
||||
.on(t.city, t.country),
|
||||
}),
|
||||
})
|
||||
|
||||
return schema
|
||||
},
|
||||
]
|
||||
|
||||
delete payload.db.pool
|
||||
await payload.db.init()
|
||||
await payload.db.connect()
|
||||
|
||||
await payload.create({
|
||||
collection: 'places',
|
||||
data: {
|
||||
city: 'A',
|
||||
country: 'B',
|
||||
},
|
||||
})
|
||||
|
||||
await expect(
|
||||
payload.create({
|
||||
collection: 'places',
|
||||
data: {
|
||||
city: 'C',
|
||||
country: 'B',
|
||||
},
|
||||
}),
|
||||
).resolves.toBeTruthy()
|
||||
|
||||
await expect(
|
||||
payload.create({
|
||||
collection: 'places',
|
||||
data: {
|
||||
city: 'A',
|
||||
country: 'D',
|
||||
},
|
||||
}),
|
||||
).resolves.toBeTruthy()
|
||||
|
||||
await expect(
|
||||
payload.create({
|
||||
collection: 'places',
|
||||
data: {
|
||||
city: 'A',
|
||||
country: 'B',
|
||||
},
|
||||
}),
|
||||
).rejects.toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('virtual fields', () => {
|
||||
it('should not save a field with `virtual: true` to the db', async () => {
|
||||
|
||||
@@ -17,8 +17,10 @@ export interface Config {
|
||||
'relation-b': RelationB;
|
||||
'pg-migrations': PgMigration;
|
||||
'custom-schema': CustomSchema;
|
||||
places: Place;
|
||||
'fields-persistance': FieldsPersistance;
|
||||
users: User;
|
||||
'payload-locked-documents': PayloadLockedDocument;
|
||||
'payload-preferences': PayloadPreference;
|
||||
'payload-migrations': PayloadMigration;
|
||||
};
|
||||
@@ -199,6 +201,17 @@ export interface CustomSchema {
|
||||
createdAt: string;
|
||||
_status?: ('draft' | 'published') | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "places".
|
||||
*/
|
||||
export interface Place {
|
||||
id: string;
|
||||
country?: string | null;
|
||||
city?: string | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "fields-persistance".
|
||||
@@ -232,6 +245,61 @@ export interface User {
|
||||
lockUntil?: string | null;
|
||||
password?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents".
|
||||
*/
|
||||
export interface PayloadLockedDocument {
|
||||
id: string;
|
||||
document?:
|
||||
| ({
|
||||
relationTo: 'posts';
|
||||
value: string | Post;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'default-values';
|
||||
value: string | DefaultValue;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'relation-a';
|
||||
value: string | RelationA;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'relation-b';
|
||||
value: string | RelationB;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'pg-migrations';
|
||||
value: string | PgMigration;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'custom-schema';
|
||||
value: string | CustomSchema;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'places';
|
||||
value: string | Place;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'fields-persistance';
|
||||
value: string | FieldsPersistance;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
} | null);
|
||||
globalSlug?: string | null;
|
||||
_lastEdited: {
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
};
|
||||
editedAt?: string | null;
|
||||
};
|
||||
isLocked?: boolean | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences".
|
||||
|
||||
Reference in New Issue
Block a user