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:
Sasha
2024-09-25 22:14:03 +03:00
committed by GitHub
parent b10f61cb25
commit 8acbda078e
18 changed files with 784 additions and 9 deletions

View File

@@ -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
},
],
}),
})
```

View File

@@ -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
},
],
}),
})
```

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View 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),
}
}
}
}

View File

@@ -275,6 +275,19 @@ export default buildConfigWithDefaults({
drafts: true,
},
},
{
slug: 'places',
fields: [
{
name: 'country',
type: 'text',
},
{
name: 'city',
type: 'text',
},
],
},
{
slug: 'fields-persistance',
fields: [

View File

@@ -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 () => {

View File

@@ -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".