feat(drizzle): abstract shared sql code to new package (#7320)

- Abstract shared sql code to a new drizzle package
- Adds sqlite package, not ready to publish until drizzle patches some
issues
- Add `transactionOptions` to allow customizing or disabling db
transactions
- Adds "experimental" label to the `schemaName` property until drizzle
patches an issue
This commit is contained in:
Dan Ribbens
2024-07-24 12:43:29 -04:00
committed by GitHub
parent c129c10f0f
commit 09ad6e4280
166 changed files with 5243 additions and 1939 deletions

4
.gitignore vendored
View File

@@ -22,6 +22,10 @@ meta_shared.json
# Ignore test directory media folder/files # Ignore test directory media folder/files
/media /media
test/media test/media
*payloadtests.db
*payloadtests.db-journal
*payloadtests.db-shm
*payloadtests.db-wal
/versions /versions
# Created by https://www.toptal.com/developers/gitignore/api/node,macos,windows,webstorm,sublimetext,visualstudiocode # Created by https://www.toptal.com/developers/gitignore/api/node,macos,windows,webstorm,sublimetext,visualstudiocode

4
.idea/payload.iml generated
View File

@@ -75,6 +75,10 @@
<excludeFolder url="file://$MODULE_DIR$/packages/ui/.swc" /> <excludeFolder url="file://$MODULE_DIR$/packages/ui/.swc" />
<excludeFolder url="file://$MODULE_DIR$/packages/ui/.turbo" /> <excludeFolder url="file://$MODULE_DIR$/packages/ui/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/ui/dist" /> <excludeFolder url="file://$MODULE_DIR$/packages/ui/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/drizzle/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/drizzle/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/db-sqlite/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/db-sqlite/dist" />
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />

View File

@@ -34,18 +34,18 @@ export default buildConfig({
## Options ## Options
| Option | Description | | Option | Description |
|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |-----------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `pool` \* | [Pool connection options](https://orm.drizzle.team/docs/quick-postgresql/node-postgres) that will be passed to Drizzle and `node-postgres`. | | `pool` \* | [Pool connection options](https://orm.drizzle.team/docs/quick-postgresql/node-postgres) that will be passed to Drizzle and `node-postgres`. |
| `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. | | `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. | | `migrationDir` | Customize the directory that migrations are stored. |
| `logger` | The instance of the logger to be passed to drizzle. By default Payload's will be used. | | `logger` | The instance of the logger to be passed to drizzle. By default Payload's will be used. |
| `schemaName` | A string for the postgres schema to use, defaults to 'public'. | | `schemaName` (experimental) | A string for the postgres schema to use, defaults to 'public'. |
| `idType` | A string of 'serial', or 'uuid' that is used for the data type given to id columns. |
| `transactionOptions` | A PgTransactionConfig object for transactions, or set to `false` to disable using transactions. [More details](https://orm.drizzle.team/docs/transactions) |
| `localesSuffix` | A string appended to the end of table names for storing localized fields. Default is '_locales'. | | `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'. | | `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'. | | `versionsSuffix` | A string appended to the end of table names for storing versions. Defaults to '_v'. |
## Access to Drizzle ## Access to Drizzle
After Payload is initialized, this adapter will expose the full power of Drizzle to you for use if you need it. After Payload is initialized, this adapter will expose the full power of Drizzle to you for use if you need it.

81
docs/database/sqlite.mdx Normal file
View File

@@ -0,0 +1,81 @@
---
title: SQLite
label: SQLite
order: 60
desc: Payload supports SQLite through an officially supported Drizzle Database Adapter.
keywords: SQLite, documentation, typescript, Content Management System, cms, headless, javascript, node, react, nextjs
---
To use Payload with SQLite, install the package `@payloadcms/db-sqlite`. It leverages Drizzle ORM and `libSQL` to interact with a SQLite database that you provide.
It automatically manages changes to your database for you in development mode, and exposes a full suite of migration controls for you to leverage in order to keep other database environments in sync with your schema. DDL transformations are automatically generated.
To configure Payload to use SQLite, pass the `sqliteAdapter` to your Payload Config as follows:
```ts
import { sqliteAdapter } from '@payloadcms/db-sqlite'
export default buildConfig({
// Your config goes here
collections: [
// Collections go here
],
// Configure the SQLite adapter here
db: sqliteAdapter({
// SQLite-specific arguments go here.
// `client.url` is required.
client: {
url: process.env.DATABASE_URL,
authToken: process.env.DATABASE_AUTH_TOKEN,
}
}),
})
```
## 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. |
| `logger` | The instance of the logger to be passed to drizzle. By default Payload's will be used. |
| `transactionOptions` | A SQLiteTransactionConfig object for transactions, or set to `false` to disable using transactions. [More details](https://orm.drizzle.team/docs/transactions) |
| `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'. |
## Access to Drizzle
After Payload is initialized, this adapter will expose the full power of Drizzle to you for use if you need it.
You can access Drizzle as follows:
```text
payload.db.drizzle
```
## Tables and relations
In addition to exposing Drizzle directly, all of the tables and Drizzle relations are exposed for you via the `payload.db` property as well.
- Tables - `payload.db.tables`
- Relations - `payload.db.relations`
## Prototyping in development mode
Drizzle exposes two ways to work locally in development mode.
The first is [`db push`](https://orm.drizzle.team/kit-docs/overview#prototyping-with-db-push), which automatically pushes changes you make to your Payload Config (and therefore, Drizzle schema) to your database so you don't have to manually migrate every time you change your Payload Config. This only works in development mode, and should not be mixed with manually running [`migrate`](/docs/database/migrations) commands.
You will be warned if any changes that you make will entail data loss while in development mode. Push is enabled by default, but you can opt out if you'd like.
Alternatively, you can disable `push` and rely solely on migrations to keep your local database in sync with your Payload Config.
## Migration workflows
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).

View File

@@ -68,3 +68,7 @@ The following functions can be used for managing transactions:
`payload.db.beginTransaction` - Starts a new session and returns a transaction ID for use in other Payload Local API calls. `payload.db.beginTransaction` - Starts a new session and returns a transaction ID for use in other Payload Local API calls.
`payload.db.commitTransaction` - Takes the identifier for the transaction, finalizes any changes. `payload.db.commitTransaction` - Takes the identifier for the transaction, finalizes any changes.
`payload.db.rollbackTransaction` - Takes the identifier for the transaction, discards any changes. `payload.db.rollbackTransaction` - Takes the identifier for the transaction, discards any changes.
## Disabling Transactions
If you wish to disable transactions entirely, you can do so by passing `false` as the `transactionOptions` in your database adapter configuration. All the official Payload database adapters support this option.

View File

@@ -15,6 +15,8 @@
"build:create-payload-app": "turbo build --filter create-payload-app", "build:create-payload-app": "turbo build --filter create-payload-app",
"build:db-mongodb": "turbo build --filter db-mongodb", "build:db-mongodb": "turbo build --filter db-mongodb",
"build:db-postgres": "turbo build --filter db-postgres", "build:db-postgres": "turbo build --filter db-postgres",
"build:db-sqlite": "turbo build --filter db-sqlite",
"build:drizzle": "turbo build --filter drizzle",
"build:email-nodemailer": "turbo build --filter email-nodemailer", "build:email-nodemailer": "turbo build --filter email-nodemailer",
"build:email-resend": "turbo build --filter email-resend", "build:email-resend": "turbo build --filter email-resend",
"build:eslint-config": "turbo build --filter eslint-config", "build:eslint-config": "turbo build --filter eslint-config",
@@ -93,6 +95,7 @@
}, },
"devDependencies": { "devDependencies": {
"@jest/globals": "29.7.0", "@jest/globals": "29.7.0",
"@libsql/client": "0.6.2",
"@next/bundle-analyzer": "15.0.0-canary.53", "@next/bundle-analyzer": "15.0.0-canary.53",
"@payloadcms/eslint-config": "workspace:*", "@payloadcms/eslint-config": "workspace:*",
"@payloadcms/eslint-plugin": "workspace:*", "@payloadcms/eslint-plugin": "workspace:*",
@@ -116,7 +119,6 @@
"create-payload-app": "workspace:*", "create-payload-app": "workspace:*",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"dotenv": "16.4.5", "dotenv": "16.4.5",
"drizzle-kit": "0.20.14-1f2c838",
"drizzle-orm": "0.29.4", "drizzle-orm": "0.29.4",
"escape-html": "^1.0.3", "escape-html": "^1.0.3",
"execa": "5.1.1", "execa": "5.1.1",

View File

@@ -5,7 +5,7 @@ import { fileURLToPath } from 'url'
const filename = fileURLToPath(import.meta.url) const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename) const dirname = path.dirname(filename)
import { commonjs } from '@hyrious/esbuild-plugin-commonjs' import { commonjs } from '@hyrious/esbuild-plugin-commonjs'
throw new Error('asfdadsf')
async function build() { async function build() {
const resultServer = await esbuild.build({ const resultServer = await esbuild.build({
entryPoints: ['src/index.ts'], entryPoints: ['src/index.ts'],
@@ -18,9 +18,9 @@ async function build() {
'*.scss', '*.scss',
'*.css', '*.css',
'drizzle-kit', 'drizzle-kit',
'libsql',
'pg', 'pg',
'@payloadcms/translations', '@payloadcms/translations',
'@payloadcms/drizzle',
'payload', 'payload',
'payload/*', 'payload/*',
], ],

View File

@@ -40,11 +40,12 @@
"build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths", "build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
"build:types": "tsc --emitDeclarationOnly --outDir dist", "build:types": "tsc --emitDeclarationOnly --outDir dist",
"clean": "rimraf {dist,*.tsbuildinfo}", "clean": "rimraf {dist,*.tsbuildinfo}",
"prepack": "pnpm clean && pnpm turbo build",
"prepublishOnly": "pnpm clean && pnpm turbo build", "prepublishOnly": "pnpm clean && pnpm turbo build",
"renamePredefinedMigrations": "tsx ./scripts/renamePredefinedMigrations.ts" "renamePredefinedMigrations": "tsx ./scripts/renamePredefinedMigrations.ts"
}, },
"dependencies": { "dependencies": {
"@libsql/client": "^0.5.2", "@payloadcms/drizzle": "workspace:*",
"console-table-printer": "2.11.2", "console-table-printer": "2.11.2",
"drizzle-kit": "0.20.14-1f2c838", "drizzle-kit": "0.20.14-1f2c838",
"drizzle-orm": "0.29.4", "drizzle-orm": "0.29.4",

View File

@@ -1,13 +1,12 @@
import type { DrizzleAdapter } from '@payloadcms/drizzle/types'
import type { Connect, Payload } from 'payload' import type { Connect, Payload } from 'payload'
import { sql } from 'drizzle-orm' import { pushDevSchema } from '@payloadcms/drizzle'
import { drizzle } from 'drizzle-orm/node-postgres' import { drizzle } from 'drizzle-orm/node-postgres'
import pg from 'pg' import pg from 'pg'
import type { PostgresAdapter } from './types.js' import type { PostgresAdapter } from './types.js'
import { pushDevSchema } from './utilities/pushDevSchema.js'
const connectWithReconnect = async function ({ const connectWithReconnect = async function ({
adapter, adapter,
payload, payload,
@@ -71,12 +70,7 @@ export const connect: Connect = async function connect(
if (!hotReload) { if (!hotReload) {
if (process.env.PAYLOAD_DROP_DATABASE === 'true') { if (process.env.PAYLOAD_DROP_DATABASE === 'true') {
this.payload.logger.info(`---- DROPPING TABLES SCHEMA(${this.schemaName || 'public'}) ----`) this.payload.logger.info(`---- DROPPING TABLES SCHEMA(${this.schemaName || 'public'}) ----`)
await this.drizzle.execute( await this.dropDatabase({ adapter: this })
sql.raw(`
drop schema if exists ${this.schemaName || 'public'} cascade;
create schema ${this.schemaName || 'public'};
`),
)
this.payload.logger.info('---- DROPPED TABLES ----') this.payload.logger.info('---- DROPPED TABLES ----')
} }
} }
@@ -92,7 +86,7 @@ export const connect: Connect = async function connect(
process.env.PAYLOAD_MIGRATING !== 'true' && process.env.PAYLOAD_MIGRATING !== 'true' &&
this.push !== false this.push !== false
) { ) {
await pushDevSchema(this) await pushDevSchema(this as unknown as DrizzleAdapter)
} }
if (typeof this.resolveInitializing === 'function') this.resolveInitializing() if (typeof this.resolveInitializing === 'function') this.resolveInitializing()

View File

@@ -1,54 +0,0 @@
import type { Count, SanitizedCollectionConfig } from 'payload'
import { sql } from 'drizzle-orm'
import toSnakeCase from 'to-snake-case'
import type { ChainedMethods } from './find/chainMethods.js'
import type { PostgresAdapter } from './types.js'
import { chainMethods } from './find/chainMethods.js'
import buildQuery from './queries/buildQuery.js'
export const count: Count = async function count(
this: PostgresAdapter,
{ collection, locale, req, where: whereArg },
) {
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const tableName = this.tableNameMap.get(toSnakeCase(collectionConfig.slug))
const db = this.sessions[await req.transactionID]?.db || this.drizzle
const table = this.tables[tableName]
const { joins, where } = await buildQuery({
adapter: this,
fields: collectionConfig.fields,
locale,
tableName,
where: whereArg,
})
const selectCountMethods: ChainedMethods = []
Object.entries(joins).forEach(([joinTable, condition]) => {
if (joinTable) {
selectCountMethods.push({
args: [this.tables[joinTable], condition],
method: 'leftJoin',
})
}
})
const countResult = await chainMethods({
methods: selectCountMethods,
query: db
.select({
count: sql<number>`count
(DISTINCT ${this.tables[tableName].id})`,
})
.from(table)
.where(where),
})
return { totalDocs: Number(countResult[0].count) }
}

View File

@@ -0,0 +1,33 @@
import type { ChainedMethods, TransactionPg } from '@payloadcms/drizzle/types'
import { chainMethods } from '@payloadcms/drizzle'
import { sql } from 'drizzle-orm'
import type { CountDistinct, PostgresAdapter } from './types.js'
export const countDistinct: CountDistinct = async function countDistinct(
this: PostgresAdapter,
{ db, joins, tableName, where },
) {
const chainedMethods: ChainedMethods = []
joins.forEach(({ condition, table }) => {
chainedMethods.push({
args: [table, condition],
method: 'leftJoin',
})
})
const countResult = await chainMethods({
methods: chainedMethods,
query: (db as TransactionPg)
.select({
count: sql<string>`count
(DISTINCT ${this.tables[tableName].id})`,
})
.from(this.tables[tableName])
.where(where),
})
return Number(countResult[0].count)
}

View File

@@ -1,6 +1,5 @@
/* eslint-disable no-restricted-syntax, no-await-in-loop */
import type { DrizzleSnapshotJSON } from 'drizzle-kit/payload' import type { DrizzleSnapshotJSON } from 'drizzle-kit/payload'
import type { CreateMigration, MigrationTemplateArgs } from 'payload' import type { CreateMigration } from 'payload'
import fs from 'fs' import fs from 'fs'
import { createRequire } from 'module' import { createRequire } from 'module'
@@ -11,38 +10,11 @@ import { fileURLToPath } from 'url'
import type { PostgresAdapter } from './types.js' import type { PostgresAdapter } from './types.js'
import { defaultDrizzleSnapshot } from './defaultSnapshot.js'
import { getMigrationTemplate } from './getMigrationTemplate.js'
const require = createRequire(import.meta.url) const require = createRequire(import.meta.url)
const migrationTemplate = ({
downSQL,
imports,
upSQL,
}: MigrationTemplateArgs): string => `import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
${imports ? `${imports}\n` : ''}
export async function up({ payload, req }: MigrateUpArgs): Promise<void> {
${upSQL}
};
export async function down({ payload, req }: MigrateDownArgs): Promise<void> {
${downSQL}
};
`
const getDefaultDrizzleSnapshot = (): DrizzleSnapshotJSON => ({
id: '00000000-0000-0000-0000-000000000000',
_meta: {
columns: {},
schemas: {},
tables: {},
},
dialect: 'pg',
enums: {},
prevId: '00000000-0000-0000-0000-00000000000',
schemas: {},
tables: {},
version: '5',
})
export const createMigration: CreateMigration = async function createMigration( export const createMigration: CreateMigration = async function createMigration(
this: PostgresAdapter, this: PostgresAdapter,
{ file, forceAcceptWarning, migrationName, payload }, { file, forceAcceptWarning, migrationName, payload },
@@ -75,7 +47,7 @@ export const createMigration: CreateMigration = async function createMigration(
const filePath = `${dir}/${fileName}` const filePath = `${dir}/${fileName}`
let drizzleJsonBefore = getDefaultDrizzleSnapshot() let drizzleJsonBefore = defaultDrizzleSnapshot
if (!upSQL) { if (!upSQL) {
// Get latest migration snapshot // Get latest migration snapshot
@@ -93,7 +65,7 @@ export const createMigration: CreateMigration = async function createMigration(
const sqlStatementsUp = await generateMigration(drizzleJsonBefore, drizzleJsonAfter) const sqlStatementsUp = await generateMigration(drizzleJsonBefore, drizzleJsonAfter)
const sqlStatementsDown = await generateMigration(drizzleJsonAfter, drizzleJsonBefore) const sqlStatementsDown = await generateMigration(drizzleJsonAfter, drizzleJsonBefore)
const sqlExecute = 'await payload.db.drizzle.execute(sql`' const sqlExecute = 'await db.execute(sql`'
if (sqlStatementsUp?.length) { if (sqlStatementsUp?.length) {
upSQL = `${sqlExecute}\n ${sqlStatementsUp?.join('\n')}\`)` upSQL = `${sqlExecute}\n ${sqlStatementsUp?.join('\n')}\`)`
@@ -121,15 +93,15 @@ export const createMigration: CreateMigration = async function createMigration(
process.exit(0) process.exit(0)
} }
} }
}
// write schema // write schema
fs.writeFileSync(`${filePath}.json`, JSON.stringify(drizzleJsonAfter, null, 2)) fs.writeFileSync(`${filePath}.json`, JSON.stringify(drizzleJsonAfter, null, 2))
}
// write migration // write migration
fs.writeFileSync( fs.writeFileSync(
`${filePath}.ts`, `${filePath}.ts`,
migrationTemplate({ getMigrationTemplate({
downSQL: downSQL || ` // Migration code`, downSQL: downSQL || ` // Migration code`,
imports, imports,
upSQL: upSQL || ` // Migration code`, upSQL: upSQL || ` // Migration code`,

View File

@@ -0,0 +1,16 @@
import type { DrizzleSnapshotJSON } from 'drizzle-kit/payload'
export const defaultDrizzleSnapshot: DrizzleSnapshotJSON = {
id: '00000000-0000-0000-0000-000000000000',
_meta: {
columns: {},
schemas: {},
tables: {},
},
dialect: 'pg',
enums: {},
prevId: '00000000-0000-0000-0000-00000000000',
schemas: {},
tables: {},
version: '5',
}

View File

@@ -0,0 +1,8 @@
import type { TransactionPg } from '@payloadcms/drizzle/types'
import type { DeleteWhere } from './types.js'
export const deleteWhere: DeleteWhere = async function deleteWhere({ db, tableName, where }) {
const table = this.tables[tableName]
await (db as TransactionPg).delete(table).where(where)
}

View File

@@ -0,0 +1,9 @@
import type { DropDatabase } from './types.js'
export const dropDatabase: DropDatabase = async function dropDatabase({ adapter }) {
await adapter.execute({
drizzle: adapter.drizzle,
raw: `drop schema if exists ${this.schemaName || 'public'} cascade;
create schema ${this.schemaName || 'public'};`,
})
}

View File

@@ -0,0 +1,13 @@
import { sql } from 'drizzle-orm'
import type { Execute } from './types.js'
export const execute: Execute<any> = function execute({ db, drizzle, raw, sql: statement }) {
const executeFrom = db ?? drizzle
if (raw) {
return executeFrom.execute(sql.raw(raw))
} else {
return executeFrom.execute(sql`${statement}`)
}
}

View File

@@ -0,0 +1,16 @@
import type { MigrationTemplateArgs } from 'payload'
export const getMigrationTemplate = ({
downSQL,
imports,
upSQL,
}: MigrationTemplateArgs): string => `import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
${imports ? `${imports}\n` : ''}
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
${upSQL}
}
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
${downSQL}
}
`

View File

@@ -1,42 +1,54 @@
import type { DatabaseAdapterObj, Payload } from 'payload' import type { DatabaseAdapterObj, Payload } from 'payload'
import fs from 'fs' import {
import path from 'path' beginTransaction,
commitTransaction,
count,
create,
createGlobal,
createGlobalVersion,
createVersion,
deleteMany,
deleteOne,
deleteVersions,
destroy,
find,
findGlobal,
findGlobalVersions,
findMigrationDir,
findOne,
findVersions,
migrate,
migrateDown,
migrateFresh,
migrateRefresh,
migrateReset,
migrateStatus,
operatorMap,
queryDrafts,
rollbackTransaction,
updateGlobal,
updateGlobalVersion,
updateOne,
updateVersion,
} from '@payloadcms/drizzle'
import { createDatabaseAdapter } from 'payload' import { createDatabaseAdapter } from 'payload'
import type { Args, PostgresAdapter } from './types.js' import type { Args, PostgresAdapter } from './types.js'
import { connect } from './connect.js' import { connect } from './connect.js'
import { count } from './count.js' import { countDistinct } from './countDistinct.js'
import { create } from './create.js' import { convertPathToJSONTraversal } from './createJSONQuery/convertPathToJSONTraversal.js'
import { createGlobal } from './createGlobal.js' import { createJSONQuery } from './createJSONQuery/index.js'
import { createGlobalVersion } from './createGlobalVersion.js'
import { createMigration } from './createMigration.js' import { createMigration } from './createMigration.js'
import { createVersion } from './createVersion.js' import { defaultDrizzleSnapshot } from './defaultSnapshot.js'
import { deleteMany } from './deleteMany.js' import { deleteWhere } from './deleteWhere.js'
import { deleteOne } from './deleteOne.js' import { dropDatabase } from './dropDatabase.js'
import { deleteVersions } from './deleteVersions.js' import { execute } from './execute.js'
import { destroy } from './destroy.js' import { getMigrationTemplate } from './getMigrationTemplate.js'
import { find } from './find.js'
import { findGlobal } from './findGlobal.js'
import { findGlobalVersions } from './findGlobalVersions.js'
import { findOne } from './findOne.js'
import { findVersions } from './findVersions.js'
import { init } from './init.js' import { init } from './init.js'
import { migrate } from './migrate.js' import { insert } from './insert.js'
import { migrateDown } from './migrateDown.js' import { requireDrizzleKit } from './requireDrizzleKit.js'
import { migrateFresh } from './migrateFresh.js'
import { migrateRefresh } from './migrateRefresh.js'
import { migrateReset } from './migrateReset.js'
import { migrateStatus } from './migrateStatus.js'
import { queryDrafts } from './queryDrafts.js'
import { beginTransaction } from './transactions/beginTransaction.js'
import { commitTransaction } from './transactions/commitTransaction.js'
import { rollbackTransaction } from './transactions/rollbackTransaction.js'
import { updateOne } from './update.js'
import { updateGlobal } from './updateGlobal.js'
import { updateGlobalVersion } from './updateGlobalVersion.js'
import { updateVersion } from './updateVersion.js'
export type { MigrateDownArgs, MigrateUpArgs } from './types.js' export type { MigrateDownArgs, MigrateUpArgs } from './types.js'
@@ -58,13 +70,19 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
return createDatabaseAdapter<PostgresAdapter>({ return createDatabaseAdapter<PostgresAdapter>({
name: 'postgres', name: 'postgres',
defaultDrizzleSnapshot,
drizzle: undefined, drizzle: undefined,
enums: {}, enums: {},
features: {
json: true,
},
fieldConstraints: {}, fieldConstraints: {},
getMigrationTemplate,
idType: postgresIDType, idType: postgresIDType,
initializing, initializing,
localesSuffix: args.localesSuffix || '_locales', localesSuffix: args.localesSuffix || '_locales',
logger: args.logger, logger: args.logger,
operators: operatorMap,
pgSchema: undefined, pgSchema: undefined,
pool: undefined, pool: undefined,
poolOptions: args.pool, poolOptions: args.pool,
@@ -76,29 +94,37 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
sessions: {}, sessions: {},
tableNameMap: new Map<string, string>(), tableNameMap: new Map<string, string>(),
tables: {}, tables: {},
transactionOptions: args.transactionOptions || undefined,
versionsSuffix: args.versionsSuffix || '_v', versionsSuffix: args.versionsSuffix || '_v',
// DatabaseAdapter // DatabaseAdapter
beginTransaction, beginTransaction: args.transactionOptions === false ? undefined : beginTransaction,
commitTransaction, commitTransaction,
connect, connect,
convertPathToJSONTraversal,
count, count,
countDistinct,
create, create,
createGlobal, createGlobal,
createGlobalVersion, createGlobalVersion,
createJSONQuery,
createMigration, createMigration,
createVersion, createVersion,
defaultIDType: payloadIDType, defaultIDType: payloadIDType,
deleteMany, deleteMany,
deleteOne, deleteOne,
deleteVersions, deleteVersions,
deleteWhere,
destroy, destroy,
dropDatabase,
execute,
find, find,
findGlobal, findGlobal,
findGlobalVersions, findGlobalVersions,
findOne, findOne,
findVersions, findVersions,
init, init,
insert,
migrate, migrate,
migrateDown, migrateDown,
migrateFresh, migrateFresh,
@@ -109,6 +135,7 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
payload, payload,
queryDrafts, queryDrafts,
rejectInitializing, rejectInitializing,
requireDrizzleKit,
resolveInitializing, resolveInitializing,
rollbackTransaction, rollbackTransaction,
updateGlobal, updateGlobal,
@@ -123,42 +150,3 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
init: adapter, init: adapter,
} }
} }
/**
* Attempt to find migrations directory.
*
* Checks for the following directories in order:
* - `migrationDir` argument from Payload config
* - `src/migrations`
* - `dist/migrations`
* - `migrations`
*
* Defaults to `src/migrations`
*
* @param migrationDir
* @returns
*/
function findMigrationDir(migrationDir?: string): string {
const cwd = process.cwd()
const srcDir = path.resolve(cwd, 'src/migrations')
const distDir = path.resolve(cwd, 'dist/migrations')
const relativeMigrations = path.resolve(cwd, 'migrations')
// Use arg if provided
if (migrationDir) return migrationDir
// Check other common locations
if (fs.existsSync(srcDir)) {
return srcDir
}
if (fs.existsSync(distDir)) {
return distDir
}
if (fs.existsSync(relativeMigrations)) {
return relativeMigrations
}
return srcDir
}

View File

@@ -8,8 +8,8 @@ import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js' import type { PostgresAdapter } from './types.js'
import { createTableName } from '../../drizzle/src/createTableName.js'
import { buildTable } from './schema/build.js' import { buildTable } from './schema/build.js'
import { createTableName } from './schema/createTableName.js'
export const init: Init = function init(this: PostgresAdapter) { export const init: Init = function init(this: PostgresAdapter) {
if (this.schemaName) { if (this.schemaName) {
@@ -17,7 +17,6 @@ export const init: Init = function init(this: PostgresAdapter) {
} else { } else {
this.pgSchema = { table: pgTable } this.pgSchema = { table: pgTable }
} }
if (this.payload.config.localization) { if (this.payload.config.localization) {
this.enums.enum__locales = pgEnum( this.enums.enum__locales = pgEnum(
'_locales', '_locales',

View File

@@ -0,0 +1,25 @@
import type { TransactionPg } from '@payloadcms/drizzle/types'
import type { Insert } from './types.js'
export const insert: Insert = async function insert({
db,
onConflictDoUpdate,
tableName,
values,
}): Promise<Record<string, unknown>[]> {
const table = this.tables[tableName]
let result
if (onConflictDoUpdate) {
result = await (db as TransactionPg)
.insert(table)
.values(values)
.onConflictDoUpdate(onConflictDoUpdate)
.returning()
} else {
result = await (db as TransactionPg).insert(table).values(values).returning()
}
return result
}

View File

@@ -1,15 +1,17 @@
import type { TransactionPg } from '@payloadcms/drizzle/types'
import type { Field, Payload, PayloadRequest } from 'payload' import type { Field, Payload, PayloadRequest } from 'payload'
import type { DrizzleTransaction, PostgresAdapter } from '../../../types.js' import { upsertRow } from '@payloadcms/drizzle'
import type { PostgresAdapter } from '../../../types.js'
import type { DocsToResave } from '../types.js' import type { DocsToResave } from '../types.js'
import { upsertRow } from '../../../upsertRow/index.js'
import { traverseFields } from './traverseFields.js' import { traverseFields } from './traverseFields.js'
type Args = { type Args = {
adapter: PostgresAdapter adapter: PostgresAdapter
collectionSlug?: string collectionSlug?: string
db: DrizzleTransaction db: TransactionPg
debug: boolean debug: boolean
docsToResave: DocsToResave docsToResave: DocsToResave
fields: Field[] fields: Field[]

View File

@@ -1,3 +1,4 @@
import type { TransactionPg } from '@payloadcms/drizzle/types'
import type { DrizzleSnapshotJSON } from 'drizzle-kit/payload' import type { DrizzleSnapshotJSON } from 'drizzle-kit/payload'
import type { Payload, PayloadRequest } from 'payload' import type { Payload, PayloadRequest } from 'payload'
@@ -37,8 +38,8 @@ type Args = {
* @param req * @param req
*/ */
export const migratePostgresV2toV3 = async ({ debug, payload, req }: Args) => { export const migratePostgresV2toV3 = async ({ debug, payload, req }: Args) => {
const adapter = payload.db as PostgresAdapter const adapter = payload.db as unknown as PostgresAdapter
const db = adapter.sessions[await req.transactionID]?.db const db = adapter.sessions[await req.transactionID].db as TransactionPg
const dir = payload.db.migrationDir const dir = payload.db.migrationDir
// get the drizzle migrateUpSQL from drizzle using the last schema // get the drizzle migrateUpSQL from drizzle using the last schema

View File

@@ -1,8 +1,9 @@
import type { TransactionPg } from '@payloadcms/drizzle/types'
import type { Field, Payload, PayloadRequest } from 'payload' import type { Field, Payload, PayloadRequest } from 'payload'
import { sql } from 'drizzle-orm' import { sql } from 'drizzle-orm'
import type { DrizzleTransaction, PostgresAdapter } from '../../types.js' import type { PostgresAdapter } from '../../types.js'
import type { DocsToResave, PathsToQuery } from './types.js' import type { DocsToResave, PathsToQuery } from './types.js'
import { fetchAndResave } from './fetchAndResave/index.js' import { fetchAndResave } from './fetchAndResave/index.js'
@@ -10,7 +11,7 @@ import { fetchAndResave } from './fetchAndResave/index.js'
type Args = { type Args = {
adapter: PostgresAdapter adapter: PostgresAdapter
collectionSlug?: string collectionSlug?: string
db: DrizzleTransaction db: TransactionPg
debug: boolean debug: boolean
fields: Field[] fields: Field[]
globalSlug?: string globalSlug?: string

View File

@@ -1,16 +1,17 @@
import type { TransactionPg } from '@payloadcms/drizzle/types'
import type { Field, Payload } from 'payload' import type { Field, Payload } from 'payload'
import { tabHasName } from 'payload/shared' import { tabHasName } from 'payload/shared'
import toSnakeCase from 'to-snake-case' import toSnakeCase from 'to-snake-case'
import type { DrizzleTransaction, PostgresAdapter } from '../../types.js' import type { PostgresAdapter } from '../../types.js'
import type { PathsToQuery } from './types.js' import type { PathsToQuery } from './types.js'
type Args = { type Args = {
adapter: PostgresAdapter adapter: PostgresAdapter
collectionSlug?: string collectionSlug?: string
columnPrefix: string columnPrefix: string
db: DrizzleTransaction db: TransactionPg
disableNotNull: boolean disableNotNull: boolean
fields: Field[] fields: Field[]
globalSlug?: string globalSlug?: string

View File

@@ -0,0 +1,5 @@
import type { RequireDrizzleKit } from '@payloadcms/drizzle/types'
import { createRequire } from 'module'
const require = createRequire(import.meta.url)
export const requireDrizzleKit: RequireDrizzleKit = () => require('drizzle-kit/payload')

View File

@@ -9,6 +9,7 @@ import type {
} from 'drizzle-orm/pg-core' } from 'drizzle-orm/pg-core'
import type { Field } from 'payload' import type { Field } from 'payload'
import { createTableName } from '@payloadcms/drizzle'
import { relations } from 'drizzle-orm' import { relations } from 'drizzle-orm'
import { import {
foreignKey, foreignKey,
@@ -24,7 +25,6 @@ import toSnakeCase from 'to-snake-case'
import type { GenericColumns, GenericTable, IDType, PostgresAdapter } from '../types.js' import type { GenericColumns, GenericTable, IDType, PostgresAdapter } from '../types.js'
import { createTableName } from './createTableName.js'
import { parentIDColumnMap } from './parentIDColumnMap.js' import { parentIDColumnMap } from './parentIDColumnMap.js'
import { setColumnID } from './setColumnID.js' import { setColumnID } from './setColumnID.js'
import { traverseFields } from './traverseFields.js' import { traverseFields } from './traverseFields.js'

View File

@@ -3,6 +3,11 @@ import type { Relation } from 'drizzle-orm'
import type { IndexBuilder, PgColumnBuilder } from 'drizzle-orm/pg-core' import type { IndexBuilder, PgColumnBuilder } from 'drizzle-orm/pg-core'
import type { Field, TabAsField } from 'payload' import type { Field, TabAsField } from 'payload'
import {
createTableName,
hasLocalesTable,
validateExistingBlockIsIdentical,
} from '@payloadcms/drizzle'
import { relations } from 'drizzle-orm' import { relations } from 'drizzle-orm'
import { import {
PgNumericBuilder, PgNumericBuilder,
@@ -26,13 +31,10 @@ import toSnakeCase from 'to-snake-case'
import type { GenericColumns, IDType, PostgresAdapter } from '../types.js' import type { GenericColumns, IDType, PostgresAdapter } from '../types.js'
import type { BaseExtraConfig, RelationMap } from './build.js' import type { BaseExtraConfig, RelationMap } from './build.js'
import { hasLocalesTable } from '../utilities/hasLocalesTable.js'
import { buildTable } from './build.js' import { buildTable } from './build.js'
import { createIndex } from './createIndex.js' import { createIndex } from './createIndex.js'
import { createTableName } from './createTableName.js'
import { idToUUID } from './idToUUID.js' import { idToUUID } from './idToUUID.js'
import { parentIDColumnMap } from './parentIDColumnMap.js' import { parentIDColumnMap } from './parentIDColumnMap.js'
import { validateExistingBlockIsIdentical } from './validateExistingBlockIsIdentical.js'
type Args = { type Args = {
adapter: PostgresAdapter adapter: PostgresAdapter

View File

@@ -1,24 +1,30 @@
import type { Operators } from '@payloadcms/drizzle'
import type {
BuildQueryJoinAliases,
DrizzleAdapter,
TransactionPg,
} from '@payloadcms/drizzle/types'
import type { DrizzleSnapshotJSON } from 'drizzle-kit/payload'
import type { import type {
ColumnBaseConfig, ColumnBaseConfig,
ColumnDataType, ColumnDataType,
DrizzleConfig, DrizzleConfig,
ExtractTablesWithRelations,
Relation, Relation,
Relations, Relations,
SQL,
} from 'drizzle-orm' } from 'drizzle-orm'
import type { NodePgDatabase, NodePgQueryResultHKT } from 'drizzle-orm/node-postgres' import type { NodePgDatabase } from 'drizzle-orm/node-postgres'
import type { import type {
PgColumn, PgColumn,
PgEnum, PgEnum,
PgInsertOnConflictDoUpdateConfig,
PgSchema, PgSchema,
PgTableWithColumns, PgTableWithColumns,
PgTransaction, PgTransactionConfig,
} from 'drizzle-orm/pg-core' } from 'drizzle-orm/pg-core'
import type { PgTableFn } from 'drizzle-orm/pg-core/table' import type { PgTableFn } from 'drizzle-orm/pg-core/table'
import type { BaseDatabaseAdapter, Payload, PayloadRequest } from 'payload' import type { Payload, PayloadRequest } from 'payload'
import type { Pool, PoolConfig } from 'pg' import type { Pool, PoolConfig, QueryResult } from 'pg'
export type DrizzleDB = NodePgDatabase<Record<string, unknown>>
export type Args = { export type Args = {
idType?: 'serial' | 'uuid' idType?: 'serial' | 'uuid'
@@ -28,7 +34,12 @@ export type Args = {
pool: PoolConfig pool: PoolConfig
push?: boolean push?: boolean
relationshipsSuffix?: string relationshipsSuffix?: string
/**
* The schema name to use for the database
* @experimental This only works when there are not other tables or enums of the same name in the database under a different schema. Awaiting fix from Drizzle.
*/
schemaName?: string schemaName?: string
transactionOptions?: PgTransactionConfig | false
versionsSuffix?: string versionsSuffix?: string
} }
@@ -45,21 +56,106 @@ export type GenericTable = PgTableWithColumns<{
columns: GenericColumns columns: GenericColumns
dialect: string dialect: string
name: string name: string
schema: undefined schema: string
}> }>
export type GenericEnum = PgEnum<[string, ...string[]]> export type GenericEnum = PgEnum<[string, ...string[]]>
export type GenericRelation = Relations<string, Record<string, Relation<string>>> export type GenericRelation = Relations<string, Record<string, Relation<string>>>
export type DrizzleTransaction = PgTransaction< export type PostgresDB = NodePgDatabase<Record<string, unknown>>
NodePgQueryResultHKT,
Record<string, unknown>, export type CountDistinct = (args: {
ExtractTablesWithRelations<Record<string, unknown>> db: PostgresDB | TransactionPg
joins: BuildQueryJoinAliases
tableName: string
where: SQL
}) => Promise<number>
export type DeleteWhere = (args: {
db: PostgresDB | TransactionPg
tableName: string
where: SQL
}) => Promise<void>
export type DropDatabase = (args: { adapter: PostgresAdapter }) => Promise<void>
export type Execute<T> = (args: {
db?: PostgresDB | TransactionPg
drizzle?: PostgresDB
raw?: string
sql?: SQL<unknown>
}) => Promise<QueryResult<Record<string, T>>>
export type Insert = (args: {
db: PostgresDB | TransactionPg
onConflictDoUpdate?: PgInsertOnConflictDoUpdateConfig<any>
tableName: string
values: Record<string, unknown> | Record<string, unknown>[]
}) => Promise<Record<string, unknown>[]>
type PostgresDrizzleAdapter = Omit<
DrizzleAdapter,
| 'countDistinct'
| 'deleteWhere'
| 'drizzle'
| 'dropDatabase'
| 'execute'
| 'insert'
| 'operators'
| 'relations'
> >
export type PostgresAdapter = BaseDatabaseAdapter & { export type PostgresAdapter = {
drizzle: DrizzleDB countDistinct: CountDistinct
defaultDrizzleSnapshot: DrizzleSnapshotJSON
deleteWhere: DeleteWhere
drizzle: PostgresDB
dropDatabase: DropDatabase
enums: Record<string, GenericEnum>
execute: Execute<unknown>
/**
* 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
*/
fieldConstraints: Record<string, Record<string, string>>
idType: Args['idType']
initializing: Promise<void>
insert: Insert
localesSuffix?: string
logger: DrizzleConfig['logger']
operators: Operators
pgSchema?: { table: PgTableFn } | PgSchema
pool: Pool
poolOptions: Args['pool']
push: boolean
rejectInitializing: () => void
relations: Record<string, GenericRelation>
relationshipsSuffix?: string
resolveInitializing: () => void
schema: Record<string, GenericEnum | GenericRelation | GenericTable>
schemaName?: Args['schemaName']
sessions: {
[id: string]: {
db: PostgresDB | TransactionPg
reject: () => Promise<void>
resolve: () => Promise<void>
}
}
tableNameMap: Map<string, string>
tables: Record<string, GenericTable>
versionsSuffix?: string
} & PostgresDrizzleAdapter
export type IDType = 'integer' | 'numeric' | 'uuid' | 'varchar'
export type MigrateUpArgs = { payload: Payload; req?: Partial<PayloadRequest> }
export type MigrateDownArgs = { payload: Payload; req?: Partial<PayloadRequest> }
declare module 'payload' {
export interface DatabaseAdapter
extends Omit<Args, 'idType' | 'logger' | 'migrationDir' | 'pool'>,
DrizzleAdapter {
enums: Record<string, GenericEnum> enums: Record<string, GenericEnum>
/** /**
* An object keyed on each table, with a key value pair where the constraint name is the key, followed by the dot-notation field name * An object keyed on each table, with a key value pair where the constraint name is the key, followed by the dot-notation field name
@@ -75,51 +171,11 @@ export type PostgresAdapter = BaseDatabaseAdapter & {
poolOptions: Args['pool'] poolOptions: Args['pool']
push: boolean push: boolean
rejectInitializing: () => void rejectInitializing: () => void
relations: Record<string, GenericRelation>
relationshipsSuffix?: string relationshipsSuffix?: string
resolveInitializing: () => void resolveInitializing: () => void
schema: Record<string, GenericEnum | GenericRelation | GenericTable> schema: Record<string, GenericEnum | GenericRelation | GenericTable>
schemaName?: Args['schemaName'] schemaName?: Args['schemaName']
sessions: {
[id: string]: {
db: DrizzleTransaction
reject: () => Promise<void>
resolve: () => Promise<void>
}
}
tableNameMap: Map<string, string> tableNameMap: Map<string, string>
tables: Record<string, GenericTable | PgTableWithColumns<any>>
versionsSuffix?: string
}
export type IDType = 'integer' | 'numeric' | 'uuid' | 'varchar'
export type PostgresAdapterResult = (args: { payload: Payload }) => PostgresAdapter
export type MigrateUpArgs = { payload: Payload; req?: Partial<PayloadRequest> }
export type MigrateDownArgs = { payload: Payload; req?: Partial<PayloadRequest> }
declare module 'payload' {
export interface DatabaseAdapter
extends Omit<Args, 'migrationDir' | 'pool'>,
BaseDatabaseAdapter {
drizzle: DrizzleDB
enums: Record<string, GenericEnum>
fieldConstraints: Record<string, Record<string, string>>
localeSuffix?: string
pool: Pool
push: boolean
relations: Record<string, GenericRelation>
relationshipsSuffix?: string
schema: Record<string, GenericEnum | GenericRelation | GenericTable>
sessions: {
[id: string]: {
db: DrizzleTransaction
reject: () => Promise<void>
resolve: () => Promise<void>
}
}
tables: Record<string, GenericTable>
versionsSuffix?: string versionsSuffix?: string
} }
} }

View File

@@ -1,17 +0,0 @@
import { sql } from 'drizzle-orm'
import type { PostgresAdapter } from '../types.js'
export const createMigrationTable = async (adapter: PostgresAdapter): Promise<void> => {
const prependSchema = adapter.schemaName ? `"${adapter.schemaName}".` : ''
await adapter.drizzle.execute(
sql.raw(`CREATE TABLE IF NOT EXISTS ${prependSchema}"payload_migrations" (
"id" serial PRIMARY KEY NOT NULL,
"name" varchar,
"batch" numeric,
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
);`),
)
}

View File

@@ -1,11 +0,0 @@
import { sql } from 'drizzle-orm'
import type { DrizzleDB } from '../types.js'
export const migrationTableExists = async (db: DrizzleDB): Promise<boolean> => {
const queryRes = await db.execute(sql`SELECT to_regclass('public.payload_migrations');`)
// Returns table name 'payload_migrations' or null
const exists = queryRes.rows?.[0]?.to_regclass === 'payload_migrations'
return exists
}

View File

@@ -1,80 +0,0 @@
import { eq } from 'drizzle-orm'
import { numeric, timestamp, varchar } from 'drizzle-orm/pg-core'
import { createRequire } from 'module'
import prompts from 'prompts'
import type { PostgresAdapter } from '../types.js'
const require = createRequire(import.meta.url)
/**
* Pushes the development schema to the database using Drizzle.
*
* @param {PostgresAdapter} db - The PostgresAdapter instance connected to the database.
* @returns {Promise<void>} - A promise that resolves once the schema push is complete.
*/
export const pushDevSchema = async (db: PostgresAdapter) => {
const { pushSchema } = require('drizzle-kit/payload')
// This will prompt if clarifications are needed for Drizzle to push new schema
const { apply, hasDataLoss, warnings } = await pushSchema(db.schema, db.drizzle)
if (warnings.length) {
let message = `Warnings detected during schema push: \n\n${warnings.join('\n')}\n\n`
if (hasDataLoss) {
message += `DATA LOSS WARNING: Possible data loss detected if schema is pushed.\n\n`
}
message += `Accept warnings and push schema to database?`
const { confirm: acceptWarnings } = await prompts(
{
name: 'confirm',
type: 'confirm',
initial: false,
message,
},
{
onCancel: () => {
process.exit(0)
},
},
)
// Exit if user does not accept warnings.
// Q: Is this the right type of exit for this interaction?
if (!acceptWarnings) {
process.exit(0)
}
}
await apply()
// Migration table def in order to use query using drizzle
const migrationsSchema = db.pgSchema.table('payload_migrations', {
name: varchar('name'),
batch: numeric('batch'),
created_at: timestamp('created_at'),
updated_at: timestamp('updated_at'),
})
const devPush = await db.drizzle
.select()
.from(migrationsSchema)
.where(eq(migrationsSchema.batch, '-1'))
if (!devPush.length) {
await db.drizzle.insert(migrationsSchema).values({
name: 'dev',
batch: '-1',
})
} else {
await db.drizzle
.update(migrationsSchema)
.set({
updated_at: new Date(),
})
.where(eq(migrationsSchema.batch, '-1'))
}
}

View File

@@ -1,11 +1,15 @@
{ {
"extends": "../../tsconfig.json", "extends": "../../tsconfig.json",
"compilerOptions": { "compilerOptions": {
"composite": true, // Make sure typescript knows that this module depends on their references // Make sure typescript knows that this module depends on their references
"noEmit": false /* Do not emit outputs. */, "composite": true,
/* Do not emit outputs. */
"noEmit": false,
"emitDeclarationOnly": true, "emitDeclarationOnly": true,
"outDir": "./dist" /* Specify an output folder for all emitted files. */, /* Specify an output folder for all emitted files. */
"rootDir": "./src" /* Specify the root folder within your source files. */ "outDir": "./dist",
/* Specify the root folder within your source files. */
"rootDir": "./src"
}, },
"exclude": [ "exclude": [
"dist", "dist",
@@ -19,6 +23,19 @@
"src/**/*.spec.ts", "src/**/*.spec.ts",
"src/**/*.spec.tsx" "src/**/*.spec.tsx"
], ],
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts", "src/**/*.json"], "include": [
"references": [{ "path": "../payload" }, { "path": "../translations" }] "src",
"src/**/*.ts",
],
"references": [
{
"path": "../payload"
},
{
"path": "../translations"
},
{
"path": "../drizzle"
}
]
} }

View File

@@ -0,0 +1,10 @@
.tmp
**/.git
**/.hg
**/.pnp.*
**/.svn
**/.yarn/**
**/build
**/dist/**
**/node_modules
**/temp

View File

@@ -0,0 +1,7 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
parserOptions: {
project: ['./tsconfig.json'],
tsconfigRootDir: __dirname,
},
}

1
packages/db-sqlite/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/migrations

View File

@@ -0,0 +1,10 @@
.tmp
**/.git
**/.hg
**/.pnp.*
**/.svn
**/.yarn/**
**/build
**/dist/**
**/node_modules
**/temp

15
packages/db-sqlite/.swcrc Normal file
View File

@@ -0,0 +1,15 @@
{
"$schema": "https://json.schemastore.org/swcrc",
"sourceMaps": true,
"jsc": {
"target": "esnext",
"parser": {
"syntax": "typescript",
"tsx": true,
"dts": true
}
},
"module": {
"type": "es6"
}
}

View File

@@ -0,0 +1,30 @@
# Payload Postgres Adapter
Official SQLite adapter for [Payload](https://payloadcms.com).
- [Main Repository](https://github.com/payloadcms/payload)
- [Payload Docs](https://payloadcms.com/docs)
## Installation
```bash
npm install @payloadcms/db-sqlite
```
## Usage
```ts
import { buildConfig } from 'payload/config'
import { sqliteAdapter } from '@payloadcms/db-sqlite'
export default buildConfig({
db: sqliteAdapter({
client: {
url: process.env.DATABASE_URI,
},
}),
// ...rest of config
})
```
More detailed usage can be found in the [Payload Docs](https://payloadcms.com/docs/configuration/overview).

View File

@@ -0,0 +1,38 @@
import * as esbuild from 'esbuild'
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
import { commonjs } from '@hyrious/esbuild-plugin-commonjs'
async function build() {
const resultServer = await esbuild.build({
entryPoints: ['src/index.ts'],
bundle: true,
platform: 'node',
format: 'esm',
outfile: 'dist/index.js',
splitting: false,
external: [
'*.scss',
'*.css',
'drizzle-kit',
'libsql',
'pg',
'@payloadcms/translations',
'@payloadcms/drizzle',
'payload',
'payload/*',
],
minify: true,
metafile: true,
tsconfig: path.resolve(dirname, './tsconfig.json'),
plugins: [commonjs()],
sourcemap: true,
})
console.log('db-sqlite bundled successfully')
fs.writeFileSync('meta_server.json', JSON.stringify(resultServer.metafile))
}
await build()

View File

@@ -0,0 +1,85 @@
{
"name": "@payloadcms/db-sqlite",
"version": "3.0.0-beta.36",
"description": "The officially supported SQLite database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",
"url": "https://github.com/payloadcms/payload.git",
"directory": "packages/db-sqlite"
},
"license": "MIT",
"author": "Payload <dev@payloadcms.com> (https://payloadcms.com)",
"type": "module",
"exports": {
".": {
"import": "./src/index.ts",
"require": "./src/index.ts",
"types": "./src/index.ts"
},
"./types": {
"import": "./src/types.ts",
"require": "./src/types.ts",
"types": "./src/types.ts"
},
"./migration-utils": {
"import": "./src/exports/migration-utils.ts",
"require": "./src/exports/migration-utils.ts",
"types": "./src/exports/migration-utils.ts"
}
},
"main": "./src/index.ts",
"types": "./src/types.ts",
"files": [
"dist",
"mock.js"
],
"scripts": {
"build": "pnpm build:swc && pnpm build:types",
"build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
"build:types": "tsc --emitDeclarationOnly --outDir dist",
"clean": "rimraf {dist,*.tsbuildinfo}",
"prepack": "pnpm clean && pnpm turbo build",
"prepublishOnly": "pnpm clean && pnpm turbo build"
},
"dependencies": {
"@libsql/client": "^0.6.2",
"@payloadcms/drizzle": "workspace:*",
"console-table-printer": "2.11.2",
"drizzle-kit": "0.20.14-1f2c838",
"drizzle-orm": "0.29.4",
"prompts": "2.4.2",
"to-snake-case": "1.0.0",
"uuid": "9.0.0"
},
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",
"@types/pg": "8.10.2",
"@types/to-snake-case": "1.0.0",
"payload": "workspace:*"
},
"peerDependencies": {
"payload": "workspace:*"
},
"publishConfig": {
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./types": {
"import": "./dist/types.js",
"require": "./dist/types.js",
"types": "./dist/types.d.ts"
},
"./migration-utils": {
"import": "./dist/exports/migration-utils.js",
"require": "./dist/exports/migration-utils.js",
"types": "./dist/exports/migration-utils.d.ts"
}
},
"main": "./dist/index.js",
"types": "./dist/index.d.ts"
}
}

View File

@@ -0,0 +1,55 @@
import type { DrizzleAdapter } from '@payloadcms/drizzle/types'
import type { LibSQLDatabase } from 'drizzle-orm/libsql'
import type { Connect } from 'payload'
import { createClient } from '@libsql/client'
import { pushDevSchema } from '@payloadcms/drizzle'
import { drizzle } from 'drizzle-orm/libsql'
import type { SQLiteAdapter } from './types.js'
export const connect: Connect = async function connect(
this: SQLiteAdapter,
options = {
hotReload: false,
},
) {
const { hotReload } = options
this.schema = {
...this.tables,
...this.relations,
}
try {
if (!this.client) {
this.client = createClient(this.clientConfig)
}
const logger = this.logger || false
this.drizzle = drizzle(this.client, { logger, schema: this.schema }) as LibSQLDatabase
if (!hotReload) {
if (process.env.PAYLOAD_DROP_DATABASE === 'true') {
this.payload.logger.info(`---- DROPPING TABLES ----`)
await this.dropDatabase({ adapter: this })
this.payload.logger.info('---- DROPPED TABLES ----')
}
}
} catch (err) {
this.payload.logger.error(`Error: cannot connect to SQLite. Details: ${err.message}`, err)
if (typeof this.rejectInitializing === 'function') this.rejectInitializing()
process.exit(1)
}
// Only push schema if not in production
if (
process.env.NODE_ENV !== 'production' &&
process.env.PAYLOAD_MIGRATING !== 'true' &&
this.push !== false
) {
await pushDevSchema(this as unknown as DrizzleAdapter)
}
if (typeof this.resolveInitializing === 'function') this.resolveInitializing()
}

View File

@@ -0,0 +1,33 @@
import type { ChainedMethods } from '@payloadcms/drizzle/types'
import { chainMethods } from '@payloadcms/drizzle'
import { sql } from 'drizzle-orm'
import type { CountDistinct, SQLiteAdapter } from './types.js'
export const countDistinct: CountDistinct = async function countDistinct(
this: SQLiteAdapter,
{ db, joins, tableName, where },
) {
const chainedMethods: ChainedMethods = []
joins.forEach(({ condition, table }) => {
chainedMethods.push({
args: [table, condition],
method: 'leftJoin',
})
})
const countResult = await chainMethods({
methods: chainedMethods,
query: db
.select({
count: sql<number>`count
(DISTINCT ${this.tables[tableName].id})`,
})
.from(this.tables[tableName])
.where(where),
})
return Number(countResult[0].count)
}

View File

@@ -0,0 +1,9 @@
export const convertPathToJSONTraversal = (incomingSegments: string[]): string => {
const segments = [...incomingSegments]
segments.shift()
return segments.reduce((res, segment) => {
const formattedSegment = Number.isNaN(parseInt(segment)) ? `'${segment}'` : segment
return `${res}->>${formattedSegment}`
}, '')
}

View File

@@ -0,0 +1,86 @@
import type { CreateJSONQueryArgs } from '@payloadcms/drizzle/types'
type FromArrayArgs = {
isRoot?: true
operator: string
pathSegments: string[]
table: string
treatAsArray?: string[]
value: boolean | number | string
}
const fromArray = ({
isRoot,
operator,
pathSegments,
table,
treatAsArray,
value,
}: FromArrayArgs) => {
const newPathSegments = pathSegments.slice(1)
const alias = `${pathSegments[isRoot ? 0 : 1]}_alias_${newPathSegments.length}`
return `EXISTS (
SELECT 1
FROM json_each(${table}.${pathSegments[0]}) AS ${alias}
WHERE ${createJSONQuery({
operator,
pathSegments: newPathSegments,
table: alias,
treatAsArray,
value,
})}
)`
}
type CreateConstraintArgs = {
alias?: string
operator: string
pathSegments: string[]
treatAsArray?: string[]
value: boolean | number | string
}
const createConstraint = ({
alias,
operator,
pathSegments,
value,
}: CreateConstraintArgs): string => {
const newAlias = `${pathSegments[0]}_alias_${pathSegments.length - 1}`
let formattedValue = value
let formattedOperator = operator
if (['contains', 'like'].includes(operator)) {
formattedOperator = 'like'
formattedValue = `%${value}%`
} else if (operator === 'equals') {
formattedOperator = '='
}
return `EXISTS (
SELECT 1
FROM json_each(${alias}.value -> '${pathSegments[0]}') AS ${newAlias}
WHERE ${newAlias}.value ->> '${pathSegments[1]}' ${formattedOperator} '${formattedValue}'
)`
}
export const createJSONQuery = ({
operator,
pathSegments,
table,
treatAsArray,
value,
}: CreateJSONQueryArgs): string => {
if (treatAsArray.includes(pathSegments[1])) {
return fromArray({
operator,
pathSegments,
table,
treatAsArray,
value,
})
}
return createConstraint({ alias: table, operator, pathSegments, treatAsArray, value })
}

View File

@@ -0,0 +1,116 @@
import type { DrizzleSnapshotJSON } from 'drizzle-kit/payload'
import type { CreateMigration } from 'payload'
import fs from 'fs'
import { createRequire } from 'module'
import path from 'path'
import { getPredefinedMigration } from 'payload'
import prompts from 'prompts'
import { fileURLToPath } from 'url'
import type { SQLiteAdapter } from './types.js'
import { defaultDrizzleSnapshot } from './defaultSnapshot.js'
import { getMigrationTemplate } from './getMigrationTemplate.js'
const require = createRequire(import.meta.url)
export const createMigration: CreateMigration = async function createMigration(
this: SQLiteAdapter,
{ file, forceAcceptWarning, migrationName, payload },
) {
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
const dir = payload.db.migrationDir
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir)
}
const { generateSQLiteDrizzleJson, generateSQLiteMigration } = require('drizzle-kit/payload')
const drizzleJsonAfter = await generateSQLiteDrizzleJson(this.schema)
const [yyymmdd, hhmmss] = new Date().toISOString().split('T')
const formattedDate = yyymmdd.replace(/\D/g, '')
const formattedTime = hhmmss.split('.')[0].replace(/\D/g, '')
let imports: string = ''
let downSQL: string
let upSQL: string
;({ downSQL, imports, upSQL } = await getPredefinedMigration({
dirname,
file,
migrationName,
payload,
}))
const timestamp = `${formattedDate}_${formattedTime}`
const name = migrationName || file?.split('/').slice(2).join('/')
const fileName = `${timestamp}${name ? `_${name.replace(/\W/g, '_')}` : ''}`
const filePath = `${dir}/${fileName}`
let drizzleJsonBefore = defaultDrizzleSnapshot as any
if (!upSQL) {
// Get latest migration snapshot
const latestSnapshot = fs
.readdirSync(dir)
.filter((file) => file.endsWith('.json'))
.sort()
.reverse()?.[0]
if (latestSnapshot) {
drizzleJsonBefore = JSON.parse(
fs.readFileSync(`${dir}/${latestSnapshot}`, 'utf8'),
) as DrizzleSnapshotJSON
}
const sqlStatementsUp = await generateSQLiteMigration(drizzleJsonBefore, drizzleJsonAfter)
const sqlStatementsDown = await generateSQLiteMigration(drizzleJsonAfter, drizzleJsonBefore)
// need to create tables as separate statements
const sqlExecute = 'await db.run(sql`'
if (sqlStatementsUp?.length) {
upSQL = sqlStatementsUp
.map((statement) => `${sqlExecute}${statement?.replaceAll('`', '\\`')}\`)`)
.join('\n')
}
if (sqlStatementsDown?.length) {
downSQL = sqlStatementsDown
.map((statement) => `${sqlExecute}${statement?.replaceAll('`', '\\`')}\`)`)
.join('\n')
}
if (!upSQL?.length && !downSQL?.length && !forceAcceptWarning) {
const { confirm: shouldCreateBlankMigration } = await prompts(
{
name: 'confirm',
type: 'confirm',
initial: false,
message: 'No schema changes detected. Would you like to create a blank migration file?',
},
{
onCancel: () => {
process.exit(0)
},
},
)
if (!shouldCreateBlankMigration) {
process.exit(0)
}
}
// write schema
fs.writeFileSync(`${filePath}.json`, JSON.stringify(drizzleJsonAfter, null, 2))
}
// write migration
fs.writeFileSync(
`${filePath}.ts`,
getMigrationTemplate({
downSQL: downSQL || ` // Migration code`,
imports,
upSQL: upSQL || ` // Migration code`,
}),
)
payload.logger.info({ msg: `Migration created at ${filePath}.ts` })
}

View File

@@ -0,0 +1,14 @@
import type { DrizzleSQLiteSnapshotJSON } from 'drizzle-kit/payload'
export const defaultDrizzleSnapshot: DrizzleSQLiteSnapshotJSON = {
id: '00000000-0000-0000-0000-000000000000',
_meta: {
columns: {},
tables: {},
},
dialect: 'sqlite',
enums: {},
prevId: '00000000-0000-0000-0000-00000000000',
tables: {},
version: '3',
}

View File

@@ -0,0 +1,6 @@
import type { DeleteWhere } from './types.js'
export const deleteWhere: DeleteWhere = async function deleteWhere({ db, tableName, where }) {
const table = this.tables[tableName]
await db.delete(table).where(where)
}

View File

@@ -0,0 +1,21 @@
import type { DropDatabase } from './types.js'
const getTables = (adapter) => {
return adapter.client.execute(`SELECT name
FROM sqlite_master
WHERE type = 'table'
AND name NOT LIKE 'sqlite_%';`)
}
const dropTables = (adapter, rows) => {
const multi = `
PRAGMA foreign_keys = OFF;\n
${rows.map(({ name }) => `DROP TABLE IF EXISTS ${name}`).join(';\n ')};\n
PRAGMA foreign_keys = ON;`
return adapter.client.executeMultiple(multi)
}
export const dropDatabase: DropDatabase = async function dropDatabase({ adapter }) {
const result = await getTables(adapter)
await dropTables(adapter, result.rows)
}

View File

@@ -0,0 +1,15 @@
import { sql } from 'drizzle-orm'
import type { Execute } from './types.js'
export const execute: Execute<any> = function execute({ db, drizzle, raw, sql: statement }) {
const executeFrom = db ?? drizzle
if (raw) {
const result = executeFrom.run(sql.raw(raw))
return result
} else {
const result = executeFrom.run(statement)
return result
}
}

View File

@@ -0,0 +1,16 @@
import type { MigrationTemplateArgs } from 'payload'
export const getMigrationTemplate = ({
downSQL,
imports,
upSQL,
}: MigrationTemplateArgs): string => `import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-sqlite'
${imports ? `${imports}\n` : ''}
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
${upSQL}
}
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
${downSQL}
}
`

View File

@@ -0,0 +1,159 @@
import type { Operators } from '@payloadcms/drizzle'
import type { DatabaseAdapterObj, Payload } from 'payload'
import {
beginTransaction,
commitTransaction,
count,
create,
createGlobal,
createGlobalVersion,
createVersion,
deleteMany,
deleteOne,
deleteVersions,
destroy,
find,
findGlobal,
findGlobalVersions,
findMigrationDir,
findOne,
findVersions,
migrate,
migrateDown,
migrateFresh,
migrateRefresh,
migrateReset,
migrateStatus,
operatorMap,
queryDrafts,
rollbackTransaction,
updateGlobal,
updateGlobalVersion,
updateOne,
updateVersion,
} from '@payloadcms/drizzle'
import { like } from 'drizzle-orm'
import { createDatabaseAdapter } from 'payload'
import type { Args, SQLiteAdapter } from './types.js'
import { connect } from './connect.js'
import { countDistinct } from './countDistinct.js'
import { convertPathToJSONTraversal } from './createJSONQuery/convertPathToJSONTraversal.js'
import { createJSONQuery } from './createJSONQuery/index.js'
import { createMigration } from './createMigration.js'
import { defaultDrizzleSnapshot } from './defaultSnapshot.js'
import { deleteWhere } from './deleteWhere.js'
import { dropDatabase } from './dropDatabase.js'
import { execute } from './execute.js'
import { getMigrationTemplate } from './getMigrationTemplate.js'
import { init } from './init.js'
import { insert } from './insert.js'
import { requireDrizzleKit } from './requireDrizzleKit.js'
export type { MigrateDownArgs, MigrateUpArgs } from './types.js'
export { sql } from 'drizzle-orm'
export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
const postgresIDType = args.idType || 'serial'
const payloadIDType = postgresIDType === 'serial' ? 'number' : 'text'
function adapter({ payload }: { payload: Payload }) {
const migrationDir = findMigrationDir(args.migrationDir)
let resolveInitializing
let rejectInitializing
const initializing = new Promise<void>((res, rej) => {
resolveInitializing = res
rejectInitializing = rej
})
// sqlite's like operator is case-insensitive, so we overwrite the DrizzleAdapter operators to not use ilike
const operators = {
...operatorMap,
contains: like,
like,
} as unknown as Operators
return createDatabaseAdapter<SQLiteAdapter>({
name: 'sqlite',
client: undefined,
clientConfig: args.client,
defaultDrizzleSnapshot,
drizzle: undefined,
features: {
json: true,
},
fieldConstraints: {},
getMigrationTemplate,
idType: postgresIDType,
initializing,
localesSuffix: args.localesSuffix || '_locales',
logger: args.logger,
operators,
push: args.push,
relations: {},
relationshipsSuffix: args.relationshipsSuffix || '_rels',
schema: {},
schemaName: args.schemaName,
sessions: {},
tableNameMap: new Map<string, string>(),
tables: {},
transactionOptions: args.transactionOptions || undefined,
versionsSuffix: args.versionsSuffix || '_v',
// DatabaseAdapter
beginTransaction: args.transactionOptions === false ? undefined : beginTransaction,
commitTransaction,
connect,
convertPathToJSONTraversal,
count,
countDistinct,
create,
createGlobal,
createGlobalVersion,
createJSONQuery,
createMigration,
createVersion,
defaultIDType: payloadIDType,
deleteMany,
deleteOne,
deleteVersions,
deleteWhere,
destroy,
dropDatabase,
execute,
find,
findGlobal,
findGlobalVersions,
findOne,
findVersions,
init,
insert,
migrate,
migrateDown,
migrateFresh,
migrateRefresh,
migrateReset,
migrateStatus,
migrationDir,
payload,
queryDrafts,
rejectInitializing,
requireDrizzleKit,
resolveInitializing,
rollbackTransaction,
updateGlobal,
updateGlobalVersion,
updateOne,
updateVersion,
})
}
return {
defaultIDType: payloadIDType,
init: adapter,
}
}

View File

@@ -0,0 +1,108 @@
/* eslint-disable no-param-reassign */
import type { DrizzleAdapter } from '@payloadcms/drizzle/types'
import type { Init, SanitizedCollectionConfig } from 'payload'
import { createTableName } from '@payloadcms/drizzle'
import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload'
import toSnakeCase from 'to-snake-case'
import type { SQLiteAdapter } from './types.js'
import { buildTable } from './schema/build.js'
export const init: Init = function init(this: SQLiteAdapter) {
let locales: [string, ...string[]] | undefined
if (this.payload.config.localization) {
locales = this.payload.config.localization.locales.map(({ code }) => code) as [
string,
...string[],
]
}
this.payload.config.collections.forEach((collection: SanitizedCollectionConfig) => {
createTableName({
adapter: this as unknown as DrizzleAdapter,
config: collection,
})
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))
buildTable({
adapter: this,
disableNotNull: !!collection?.versions?.drafts,
disableUnique: false,
fields: collection.fields,
locales,
tableName,
timestamps: collection.timestamps,
versions: false,
})
if (collection.versions) {
const versionsTableName = this.tableNameMap.get(
`_${toSnakeCase(collection.slug)}${this.versionsSuffix}`,
)
const versionFields = buildVersionCollectionFields(collection)
buildTable({
adapter: this,
disableNotNull: !!collection.versions?.drafts,
disableUnique: true,
fields: versionFields,
locales,
tableName: versionsTableName,
timestamps: true,
versions: true,
})
}
})
this.payload.config.globals.forEach((global) => {
const tableName = createTableName({
adapter: this as unknown as DrizzleAdapter,
config: global,
})
buildTable({
adapter: this,
disableNotNull: !!global?.versions?.drafts,
disableUnique: false,
fields: global.fields,
locales,
tableName,
timestamps: false,
versions: false,
})
if (global.versions) {
const versionsTableName = createTableName({
adapter: this as unknown as DrizzleAdapter,
config: global,
versions: true,
versionsCustomName: true,
})
const versionFields = buildVersionGlobalFields(global)
buildTable({
adapter: this,
disableNotNull: !!global.versions?.drafts,
disableUnique: true,
fields: versionFields,
locales,
tableName: versionsTableName,
timestamps: true,
versions: true,
})
}
})
}

View File

@@ -0,0 +1,19 @@
import type { Insert } from './types.js'
export const insert: Insert = async function insert({
db,
onConflictDoUpdate,
tableName,
values,
}): Promise<Record<string, unknown>[]> {
const table = this.tables[tableName]
let result
if (onConflictDoUpdate) {
result = db.insert(table).values(values).onConflictDoUpdate(onConflictDoUpdate).returning()
} else {
result = db.insert(table).values(values).returning()
}
result = await result
return result
}

View File

@@ -0,0 +1,15 @@
import type { RequireDrizzleKit } from '@payloadcms/drizzle/types'
import { createRequire } from 'module'
const require = createRequire(import.meta.url)
/**
* Dynamically requires the `drizzle-kit` package to access the `generateSQLiteDrizzleJson` and `pushSQLiteSchema` functions and exports them generically to call them from @payloadcms/drizzle.
*/
export const requireDrizzleKit: RequireDrizzleKit = () => {
const {
generateSQLiteDrizzleJson: generateDrizzleJson,
pushSQLiteSchema: pushSchema,
} = require('drizzle-kit/payload')
return { generateDrizzleJson, pushSchema }
}

View File

@@ -0,0 +1,490 @@
/* eslint-disable no-param-reassign */
import type { ColumnDataType, Relation } from 'drizzle-orm'
import type {
ForeignKeyBuilder,
IndexBuilder,
SQLiteColumn,
SQLiteColumnBuilder,
SQLiteTableWithColumns,
UniqueConstraintBuilder,
} from 'drizzle-orm/sqlite-core'
import type { Field } from 'payload'
import { 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 { getIDColumn } from './getIDColumn.js'
import { setColumnID } from './setColumnID.js'
import { traverseFields } from './traverseFields.js'
export type BaseExtraConfig = Record<
string,
(cols: {
[x: string]: SQLiteColumn<{
baseColumn: never
columnType: string
data: unknown
dataType: ColumnDataType
driverParam: unknown
enumValues: string[]
hasDefault: false
name: string
notNull: false
tableName: string
}>
}) => ForeignKeyBuilder | IndexBuilder | UniqueConstraintBuilder
>
export type RelationMap = Map<string, { localized: boolean; target: string; type: 'many' | 'one' }>
type Args = {
adapter: SQLiteAdapter
baseColumns?: Record<string, SQLiteColumnBuilder>
baseExtraConfig?: BaseExtraConfig
buildNumbers?: boolean
buildRelationships?: boolean
disableNotNull: boolean
disableUnique: boolean
fields: Field[]
locales?: [string, ...string[]]
rootRelationsToBuild?: RelationMap
rootRelationships?: Set<string>
rootTableIDColType?: IDType
rootTableName?: string
tableName: string
timestamps?: boolean
versions: boolean
}
type Result = {
hasManyNumberField: 'index' | boolean
hasManyTextField: 'index' | boolean
relationsToBuild: RelationMap
}
export const buildTable = ({
adapter,
baseColumns = {},
baseExtraConfig = {},
disableNotNull,
disableUnique = false,
fields,
locales,
rootRelationsToBuild,
rootRelationships,
rootTableIDColType,
rootTableName: incomingRootTableName,
tableName,
timestamps,
versions,
}: 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()
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,
disableUnique,
fields,
indexes,
locales,
localesColumns,
localesIndexes,
newTableName: tableName,
parentTableName: tableName,
relationsToBuild,
relationships,
rootRelationsToBuild: rootRelationsToBuild || relationsToBuild,
rootTableIDColType: rootTableIDColType || idColType,
rootTableName,
versions,
})
// split the relationsToBuild by localized and non-localized
const localizedRelations = new Map()
const nonLocalizedRelations = new Map()
relationsToBuild.forEach(({ type, localized, target }, key) => {
const map = localized ? localizedRelations : nonLocalizedRelations
map.set(key, { type, 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'
relationshipColumns[`${relationTo}ID`] = getIDColumn({
name: `${formattedRelationTo}_id`,
type: colType,
primaryKey: false,
})
relationExtraConfig[`${relationTo}IdFk`] = (cols) =>
foreignKey({
name: `${relationshipsTableName}_${toSnakeCase(relationTo)}_fk`,
columns: [cols[`${relationTo}ID`]],
foreignColumns: [adapter.tables[formattedRelationTo].id],
}).onDelete('cascade')
})
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, 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: 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 { hasManyNumberField, hasManyTextField, relationsToBuild }
}

View File

@@ -0,0 +1,28 @@
/* eslint-disable no-param-reassign */
import { index, uniqueIndex } from 'drizzle-orm/sqlite-core'
import type { GenericColumn } from '../types.js'
type CreateIndexArgs = {
columnName: string
name: string | string[]
tableName: string
unique?: boolean
}
export const createIndex = ({ name, columnName, tableName, 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(`${tableName}_${columnName}_idx`).on(columns[0], ...columns.slice(1))
return index(`${tableName}_${columnName}_idx`).on(columns[0], ...columns.slice(1))
}
}

View File

@@ -0,0 +1,38 @@
import { integer, numeric, text } from 'drizzle-orm/sqlite-core'
import type { IDType } from '../types.js'
export const getIDColumn = ({
name,
type,
notNull,
primaryKey,
}: {
name: string
notNull?: boolean
primaryKey: boolean
type: IDType
}) => {
let column
switch (type) {
case 'integer':
column = integer(name)
break
case 'numeric':
column = numeric(name)
break
case 'text':
column = text(name)
break
}
if (notNull) {
column.notNull()
}
if (primaryKey) {
column.primaryKey()
}
return column
}

View File

@@ -0,0 +1,13 @@
import type { Field } from 'payload'
export const idToUUID = (fields: Field[]): Field[] =>
fields.map((field) => {
if ('name' in field && field.name === 'id') {
return {
...field,
name: '_uuid',
}
}
return field
})

View File

@@ -0,0 +1,31 @@
import type { SQLiteColumnBuilder } from 'drizzle-orm/sqlite-core'
import { integer, numeric, text } from 'drizzle-orm/sqlite-core'
import { type Field, flattenTopLevelFields } from 'payload'
import { fieldAffectsData } from 'payload/shared'
import type { IDType } from '../types.js'
type Args = {
columns: Record<string, SQLiteColumnBuilder>
fields: Field[]
}
export const setColumnID = ({ columns, fields }: Args): IDType => {
const idField = flattenTopLevelFields(fields).find(
(field) => fieldAffectsData(field) && field.name === 'id',
)
if (idField) {
if (idField.type === 'number') {
columns.id = numeric('id').primaryKey()
return 'numeric'
}
if (idField.type === 'text') {
columns.id = text('id').primaryKey()
return 'text'
}
}
columns.id = integer('id').primaryKey()
return 'integer'
}

View File

@@ -0,0 +1,787 @@
/* eslint-disable no-param-reassign */
import type { Relation } from 'drizzle-orm'
import type { IndexBuilder, SQLiteColumnBuilder } from 'drizzle-orm/sqlite-core'
import type { Field, TabAsField } from 'payload'
import {
createTableName,
hasLocalesTable,
validateExistingBlockIsIdentical,
} from '@payloadcms/drizzle'
import { relations } from 'drizzle-orm'
import {
SQLiteIntegerBuilder,
SQLiteNumericBuilder,
SQLiteTextBuilder,
foreignKey,
index,
integer,
numeric,
text,
} from 'drizzle-orm/sqlite-core'
import { InvalidConfiguration } from 'payload'
import { fieldAffectsData, 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'
type Args = {
adapter: SQLiteAdapter
columnPrefix?: string
columns: Record<string, SQLiteColumnBuilder>
disableNotNull: boolean
disableUnique?: boolean
fieldPrefix?: string
fields: (Field | TabAsField)[]
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
relationsToBuild: RelationMap
relationships: Set<string>
rootRelationsToBuild?: RelationMap
rootTableIDColType: IDType
rootTableName: string
versions: boolean
}
type Result = {
hasLocalizedField: boolean
hasLocalizedManyNumberField: boolean
hasLocalizedManyTextField: boolean
hasLocalizedRelationshipField: boolean
hasManyNumberField: 'index' | boolean
hasManyTextField: 'index' | boolean
}
export const traverseFields = ({
adapter,
columnPrefix,
columns,
disableNotNull,
disableUnique = false,
fieldPrefix,
fields,
forceLocalized,
indexes,
locales,
localesColumns,
localesIndexes,
newTableName,
parentTableName,
relationsToBuild,
relationships,
rootRelationsToBuild,
rootTableIDColType,
rootTableName,
versions,
}: 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
let columnName: string
let fieldName: string
let targetTable = columns
let targetIndexes = indexes
if (fieldAffectsData(field)) {
columnName = `${columnPrefix || ''}${field.name[0] === '_' ? '_' : ''}${toSnakeCase(
field.name,
)}`
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) &&
!['array', 'blocks', 'group', 'point', 'relationship', 'upload'].includes(field.type) &&
!('hasMany' in field && field.hasMany === true)
) {
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
}
targetIndexes[`${newTableName}_${field.name}Idx`] = createIndex({
name: fieldName,
columnName,
tableName: newTableName,
unique,
})
}
}
switch (field.type) {
case 'text': {
if (field.hasMany) {
if (field.localized) {
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] = text(columnName)
}
break
}
case 'email':
case 'code':
case 'textarea': {
targetTable[fieldName] = text(columnName)
break
}
case 'number': {
if (field.hasMany) {
if (field.localized) {
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] = numeric(columnName)
}
break
}
case 'richText':
case 'json': {
targetTable[fieldName] = text(columnName, { mode: 'json' })
break
}
case 'date': {
targetTable[fieldName] = text(columnName)
break
}
case 'point': {
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),
}
if (field.localized) {
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] = text(fieldName, { enum: options })
}
break
}
case 'checkbox': {
targetTable[fieldName] = integer(columnName, { mode: 'boolean' })
break
}
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),
}
if (field.localized && adapter.payload.config.localization) {
baseColumns._locale = text('_locale', { enum: locales }).notNull()
baseExtraConfig._localeIdx = (cols) =>
index(`${arrayTableName}_locale_idx`).on(cols._locale)
}
const {
hasManyNumberField: subHasManyNumberField,
hasManyTextField: subHasManyTextField,
relationsToBuild: subRelationsToBuild,
} = buildTable({
adapter,
baseColumns,
baseExtraConfig,
disableNotNull: disableNotNullFromHere,
disableUnique,
fields: disableUnique ? idToUUID(field.fields) : field.fields,
rootRelationsToBuild,
rootRelationships: relationships,
rootTableIDColType,
rootTableName,
tableName: arrayTableName,
versions,
})
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),
_parentIDIdx: (cols) => index(`${blockTableName}_parent_id_idx`).on(cols._parentID),
_parentIdFk: (cols) =>
foreignKey({
name: `${blockTableName}_parent_id_fk`,
columns: [cols._parentID],
foreignColumns: [adapter.tables[rootTableName].id],
}).onDelete('cascade'),
_pathIdx: (cols) => index(`${blockTableName}_path_idx`).on(cols._path),
}
if (field.localized && adapter.payload.config.localization) {
baseColumns._locale = text('_locale', { enum: locales }).notNull()
baseExtraConfig._localeIdx = (cols) =>
index(`${blockTableName}_locale_idx`).on(cols._locale)
}
const {
hasManyNumberField: subHasManyNumberField,
hasManyTextField: subHasManyTextField,
relationsToBuild: subRelationsToBuild,
} = buildTable({
adapter,
baseColumns,
baseExtraConfig,
disableNotNull: disableNotNullFromHere,
disableUnique,
fields: disableUnique ? idToUUID(block.fields) : block.fields,
rootRelationsToBuild,
rootRelationships: relationships,
rootTableIDColType,
rootTableName,
tableName: blockTableName,
versions,
})
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 'tab':
case 'group': {
if (!('name' in field)) {
const {
hasLocalizedField: groupHasLocalizedField,
hasLocalizedManyNumberField: groupHasLocalizedManyNumberField,
hasLocalizedManyTextField: groupHasLocalizedManyTextField,
hasLocalizedRelationshipField: groupHasLocalizedRelationshipField,
hasManyNumberField: groupHasManyNumberField,
hasManyTextField: groupHasManyTextField,
} = traverseFields({
adapter,
columnPrefix,
columns,
disableNotNull,
disableUnique,
fieldPrefix,
fields: field.fields,
forceLocalized,
indexes,
locales,
localesColumns,
localesIndexes,
newTableName,
parentTableName,
relationsToBuild,
relationships,
rootRelationsToBuild,
rootTableIDColType,
rootTableName,
versions,
})
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
}
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.fields,
forceLocalized: field.localized,
indexes,
locales,
localesColumns,
localesIndexes,
newTableName: `${parentTableName}_${columnName}`,
parentTableName,
relationsToBuild,
relationships,
rootRelationsToBuild,
rootTableIDColType,
rootTableName,
versions,
})
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 'tabs': {
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
const {
hasLocalizedField: tabHasLocalizedField,
hasLocalizedManyNumberField: tabHasLocalizedManyNumberField,
hasLocalizedManyTextField: tabHasLocalizedManyTextField,
hasLocalizedRelationshipField: tabHasLocalizedRelationshipField,
hasManyNumberField: tabHasManyNumberField,
hasManyTextField: tabHasManyTextField,
} = traverseFields({
adapter,
columnPrefix,
columns,
disableNotNull: disableNotNullFromHere,
disableUnique,
fieldPrefix,
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
forceLocalized,
indexes,
locales,
localesColumns,
localesIndexes,
newTableName,
parentTableName,
relationsToBuild,
relationships,
rootRelationsToBuild,
rootTableIDColType,
rootTableName,
versions,
})
if (tabHasLocalizedField) hasLocalizedField = true
if (tabHasLocalizedRelationshipField) hasLocalizedRelationshipField = true
if (tabHasManyTextField) hasManyTextField = true
if (tabHasLocalizedManyTextField) hasLocalizedManyTextField = true
if (tabHasManyNumberField) hasManyNumberField = true
if (tabHasLocalizedManyNumberField) hasLocalizedManyNumberField = true
break
}
case 'row':
case 'collapsible': {
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
const {
hasLocalizedField: rowHasLocalizedField,
hasLocalizedManyNumberField: rowHasLocalizedManyNumberField,
hasLocalizedManyTextField: rowHasLocalizedManyTextField,
hasLocalizedRelationshipField: rowHasLocalizedRelationshipField,
hasManyNumberField: rowHasManyNumberField,
hasManyTextField: rowHasManyTextField,
} = traverseFields({
adapter,
columnPrefix,
columns,
disableNotNull: disableNotNullFromHere,
disableUnique,
fieldPrefix,
fields: field.fields,
forceLocalized,
indexes,
locales,
localesColumns,
localesIndexes,
newTableName,
parentTableName,
relationsToBuild,
relationships,
rootRelationsToBuild,
rootTableIDColType,
rootTableName,
versions,
})
if (rowHasLocalizedField) hasLocalizedField = true
if (rowHasLocalizedRelationshipField) hasLocalizedRelationshipField = true
if (rowHasManyTextField) hasManyTextField = true
if (rowHasLocalizedManyTextField) hasLocalizedManyTextField = true
if (rowHasManyNumberField) hasManyNumberField = true
if (rowHasLocalizedManyNumberField) hasLocalizedManyNumberField = true
break
}
case 'relationship':
case 'upload':
if (Array.isArray(field.relationTo)) {
field.relationTo.forEach((relation) => relationships.add(relation))
} else if (field.type === 'relationship' && field.hasMany) {
relationships.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,
target: tableName,
})
// add notNull when not required
if (!disableNotNull && field.required && !field.admin?.condition) {
targetTable[fieldName].notNull()
}
break
}
if (adapter.payload.config.localization && field.localized) {
hasLocalizedRelationshipField = true
}
break
default:
break
}
const condition = field.admin && field.admin.condition
if (
!disableNotNull &&
targetTable[fieldName] &&
'required' in field &&
field.required &&
!condition
) {
targetTable[fieldName].notNull()
}
})
return {
hasLocalizedField,
hasLocalizedManyNumberField,
hasLocalizedManyTextField,
hasLocalizedRelationshipField,
hasManyNumberField,
hasManyTextField,
}
}

View File

@@ -0,0 +1,167 @@
import type { Client, Config, ResultSet } from '@libsql/client'
import type { Operators } from '@payloadcms/drizzle'
import type { BuildQueryJoinAliases, DrizzleAdapter } from '@payloadcms/drizzle/types'
import type { ColumnDataType, DrizzleConfig, Relation, Relations, SQL } from 'drizzle-orm'
import type { LibSQLDatabase } from 'drizzle-orm/libsql'
import type {
SQLiteColumn,
SQLiteInsertOnConflictDoUpdateConfig,
SQLiteTableWithColumns,
SQLiteTransactionConfig,
} from 'drizzle-orm/sqlite-core'
import type { SQLiteRaw } from 'drizzle-orm/sqlite-core/query-builders/raw'
import type { Payload, PayloadRequest } from 'payload'
export type Args = {
client: Config
idType?: 'serial' | 'uuid'
localesSuffix?: string
logger?: DrizzleConfig['logger']
migrationDir?: string
push?: boolean
relationshipsSuffix?: string
schemaName?: string
transactionOptions?: SQLiteTransactionConfig | false
versionsSuffix?: string
}
export type GenericColumn = SQLiteColumn<
{
baseColumn: never
columnType: string
data: unknown
dataType: ColumnDataType
driverParam: unknown
enumValues: string[]
hasDefault: false
name: string
notNull: false
tableName: string
},
object
>
export type GenericColumns = {
[x: string]: GenericColumn
}
export type GenericTable = SQLiteTableWithColumns<{
columns: GenericColumns
dialect: string
name: string
schema: string
}>
export type GenericRelation = Relations<string, Record<string, Relation<string>>>
export type CountDistinct = (args: {
db: LibSQLDatabase
joins: BuildQueryJoinAliases
tableName: string
where: SQL
}) => Promise<number>
export type DeleteWhere = (args: {
db: LibSQLDatabase
tableName: string
where: SQL
}) => Promise<void>
export type DropDatabase = (args: { adapter: SQLiteAdapter }) => Promise<void>
export type Execute<T> = (args: {
db?: LibSQLDatabase
drizzle?: LibSQLDatabase
raw?: string
sql?: SQL<unknown>
}) => SQLiteRaw<Promise<T>> | SQLiteRaw<ResultSet>
export type Insert = (args: {
db: LibSQLDatabase
onConflictDoUpdate?: SQLiteInsertOnConflictDoUpdateConfig<any>
tableName: string
values: Record<string, unknown> | Record<string, unknown>[]
}) => Promise<Record<string, unknown>[]>
// Explicitly omit drizzle property for complete override in SQLiteAdapter, required in ts 5.5
type SQLiteDrizzleAdapter = Omit<
DrizzleAdapter,
| 'countDistinct'
| 'deleteWhere'
| 'drizzle'
| 'dropDatabase'
| 'execute'
| 'insert'
| 'operators'
| 'relations'
>
export type SQLiteAdapter = {
client: Client
clientConfig: Args['client']
countDistinct: CountDistinct
defaultDrizzleSnapshot: any
deleteWhere: DeleteWhere
drizzle: LibSQLDatabase
dropDatabase: DropDatabase
execute: Execute<unknown>
/**
* 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
*/
fieldConstraints: Record<string, Record<string, string>>
idType: Args['idType']
initializing: Promise<void>
insert: Insert
localesSuffix?: string
logger: DrizzleConfig['logger']
operators: Operators
push: boolean
rejectInitializing: () => void
relations: Record<string, GenericRelation>
relationshipsSuffix?: string
resolveInitializing: () => void
schema: Record<string, GenericRelation | GenericTable>
schemaName?: Args['schemaName']
tableNameMap: Map<string, string>
tables: Record<string, GenericTable>
transactionOptions: SQLiteTransactionConfig
versionsSuffix?: string
} & SQLiteDrizzleAdapter
export type IDType = 'integer' | 'numeric' | 'text'
export type MigrateUpArgs = {
db: LibSQLDatabase
payload: Payload
req?: Partial<PayloadRequest>
}
export type MigrateDownArgs = {
db: LibSQLDatabase
payload: Payload
req?: Partial<PayloadRequest>
}
declare module 'payload' {
export interface DatabaseAdapter
extends Omit<Args, 'idType' | 'logger' | 'migrationDir' | 'pool'>,
DrizzleAdapter {
/**
* 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
*/
fieldConstraints: Record<string, Record<string, string>>
idType: Args['idType']
initializing: Promise<void>
localesSuffix?: string
logger: DrizzleConfig['logger']
push: boolean
rejectInitializing: () => void
relationshipsSuffix?: string
resolveInitializing: () => void
schema: Record<string, GenericRelation | GenericTable>
tableNameMap: Map<string, string>
transactionOptions: SQLiteTransactionConfig
versionsSuffix?: string
}
}

View File

@@ -0,0 +1,41 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
// Make sure typescript knows that this module depends on their references
"noEmit": false
/* Do not emit outputs. */,
"emitDeclarationOnly": true,
"outDir": "./dist"
/* Specify an output folder for all emitted files. */,
"rootDir": "./src"
/* Specify the root folder within your source files. */
},
"exclude": [
"dist",
"build",
"tests",
"test",
"node_modules",
"eslint.config.js",
"src/**/*.spec.js",
"src/**/*.spec.jsx",
"src/**/*.spec.ts",
"src/**/*.spec.tsx"
],
"include": [
"src",
"src/**/*.ts"
],
"references": [
{
"path": "../payload"
},
{
"path": "../translations"
},
{
"path": "../drizzle"
}
]
}

View File

@@ -0,0 +1,10 @@
.tmp
**/.git
**/.hg
**/.pnp.*
**/.svn
**/.yarn/**
**/build
**/dist/**
**/node_modules
**/temp

View File

@@ -0,0 +1,7 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
parserOptions: {
project: ['./tsconfig.json'],
tsconfigRootDir: __dirname,
},
}

View File

@@ -0,0 +1,10 @@
.tmp
**/.git
**/.hg
**/.pnp.*
**/.svn
**/.yarn/**
**/build
**/dist/**
**/node_modules
**/temp

15
packages/drizzle/.swcrc Normal file
View File

@@ -0,0 +1,15 @@
{
"$schema": "https://json.schemastore.org/swcrc",
"sourceMaps": true,
"jsc": {
"target": "esnext",
"parser": {
"syntax": "typescript",
"tsx": true,
"dts": true
}
},
"module": {
"type": "es6"
}
}

View File

@@ -0,0 +1,3 @@
# Payload Drizzle Adapter
The Drizzle package is used by db-postgres and db-sqlite for shared functionality of SQL databases. It is not meant to be used directly in Payload projects.

View File

@@ -0,0 +1,71 @@
{
"name": "@payloadcms/drizzle",
"version": "3.0.0-beta.36",
"description": "A library of shared functions used by different payload database adapters",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",
"url": "https://github.com/payloadcms/payload.git",
"directory": "packages/drizzle"
},
"license": "MIT",
"author": "Payload <dev@payloadcms.com> (https://payloadcms.com)",
"type": "module",
"exports": {
".": {
"import": "./src/index.ts",
"types": "./src/index.ts",
"default": "./src/index.ts"
},
"./types": {
"import": "./src/types.ts",
"types": "./src/types.ts",
"default": "./src/types.ts"
}
},
"main": "./src/index.ts",
"types": "./src/types.ts",
"files": [
"dist",
"mock.js"
],
"scripts": {
"build": "pnpm build:swc && pnpm build:types",
"build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
"build:types": "tsc --emitDeclarationOnly --outDir dist",
"clean": "rimraf {dist,*.tsbuildinfo}",
"prepack": "pnpm clean && pnpm turbo build",
"prepublishOnly": "pnpm clean && pnpm turbo build"
},
"dependencies": {
"console-table-printer": "2.11.2",
"drizzle-orm": "0.29.4",
"prompts": "2.4.2",
"to-snake-case": "1.0.0",
"uuid": "9.0.0"
},
"devDependencies": {
"@libsql/client": "^0.6.2",
"@payloadcms/eslint-config": "workspace:*",
"@types/pg": "8.10.2",
"@types/to-snake-case": "1.0.0",
"payload": "workspace:*"
},
"peerDependencies": {
"payload": "workspace:*"
},
"publishConfig": {
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./types": {
"import": "./dist/types.js",
"types": "./dist/types.d.ts"
}
},
"main": "./dist/index.js",
"types": "./dist/index.d.ts"
}
}

View File

@@ -0,0 +1,36 @@
import type { Count } from 'payload'
import type { SanitizedCollectionConfig } from 'payload'
import toSnakeCase from 'to-snake-case'
import type { DrizzleAdapter } from './types.js'
import buildQuery from './queries/buildQuery.js'
export const count: Count = async function count(
this: DrizzleAdapter,
{ collection, locale, req, where: whereArg },
) {
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const tableName = this.tableNameMap.get(toSnakeCase(collectionConfig.slug))
const db = this.sessions[await req.transactionID]?.db || this.drizzle
const { joins, where } = await buildQuery({
adapter: this,
fields: collectionConfig.fields,
locale,
tableName,
where: whereArg,
})
const countResult = await this.countDistinct({
db,
joins,
tableName,
where,
})
return { totalDocs: countResult }
}

View File

@@ -2,12 +2,12 @@ import type { Create } from 'payload'
import toSnakeCase from 'to-snake-case' import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js' import type { DrizzleAdapter } from './types.js'
import { upsertRow } from './upsertRow/index.js' import { upsertRow } from './upsertRow/index.js'
export const create: Create = async function create( export const create: Create = async function create(
this: PostgresAdapter, this: DrizzleAdapter,
{ collection: collectionSlug, data, req }, { collection: collectionSlug, data, req },
) { ) {
const db = this.sessions[await req.transactionID]?.db || this.drizzle const db = this.sessions[await req.transactionID]?.db || this.drizzle

View File

@@ -2,12 +2,12 @@ import type { CreateGlobalArgs, PayloadRequest } from 'payload'
import toSnakeCase from 'to-snake-case' import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js' import type { DrizzleAdapter } from './types.js'
import { upsertRow } from './upsertRow/index.js' import { upsertRow } from './upsertRow/index.js'
export async function createGlobal<T extends Record<string, unknown>>( export async function createGlobal<T extends Record<string, unknown>>(
this: PostgresAdapter, this: DrizzleAdapter,
{ slug, data, req = {} as PayloadRequest }: CreateGlobalArgs, { slug, data, req = {} as PayloadRequest }: CreateGlobalArgs,
): Promise<T> { ): Promise<T> {
const db = this.sessions[await req.transactionID]?.db || this.drizzle const db = this.sessions[await req.transactionID]?.db || this.drizzle

View File

@@ -4,12 +4,12 @@ import { sql } from 'drizzle-orm'
import { buildVersionGlobalFields } from 'payload' import { buildVersionGlobalFields } from 'payload'
import toSnakeCase from 'to-snake-case' import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js' import type { DrizzleAdapter } from './types.js'
import { upsertRow } from './upsertRow/index.js' import { upsertRow } from './upsertRow/index.js'
export async function createGlobalVersion<T extends TypeWithID>( export async function createGlobalVersion<T extends TypeWithID>(
this: PostgresAdapter, this: DrizzleAdapter,
{ autosave, globalSlug, req = {} as PayloadRequest, versionData }: CreateGlobalVersionArgs, { autosave, globalSlug, req = {} as PayloadRequest, versionData }: CreateGlobalVersionArgs,
) { ) {
const db = this.sessions[await req.transactionID]?.db || this.drizzle const db = this.sessions[await req.transactionID]?.db || this.drizzle
@@ -32,13 +32,15 @@ export async function createGlobalVersion<T extends TypeWithID>(
}) })
const table = this.tables[tableName] const table = this.tables[tableName]
if (global.versions.drafts) { if (global.versions.drafts) {
await db.execute(sql` await this.execute({
db,
sql: sql`
UPDATE ${table} UPDATE ${table}
SET latest = false SET latest = false
WHERE ${table.id} != ${result.id}; WHERE ${table.id} != ${result.id};
`) `,
})
} }
return result return result

View File

@@ -3,10 +3,10 @@ import type { DBIdentifierName } from 'payload'
import { APIError } from 'payload' import { APIError } from 'payload'
import toSnakeCase from 'to-snake-case' import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from '../types.js' import type { DrizzleAdapter } from './types.js'
type Args = { type Args = {
adapter: PostgresAdapter adapter: Pick<DrizzleAdapter, 'tableNameMap' | 'versionsSuffix'>
/** The collection, global or field config **/ /** The collection, global or field config **/
config: { config: {
dbName?: DBIdentifierName dbName?: DBIdentifierName
@@ -20,6 +20,7 @@ type Args = {
prefix?: string prefix?: string
/** For tables based on fields that could have both enumName and dbName (ie: select with hasMany), default: 'dbName' */ /** For tables based on fields that could have both enumName and dbName (ie: select with hasMany), default: 'dbName' */
target?: 'dbName' | 'enumName' target?: 'dbName' | 'enumName'
/** Throws error if true for postgres when table and enum names exceed 63 characters */
throwValidationError?: boolean throwValidationError?: boolean
/** Adds the versions suffix to the default table name - should only be used on the base collection to avoid duplicate suffixing */ /** Adds the versions suffix to the default table name - should only be used on the base collection to avoid duplicate suffixing */
versions?: boolean versions?: boolean

View File

@@ -4,12 +4,12 @@ import { sql } from 'drizzle-orm'
import { buildVersionCollectionFields } from 'payload' import { buildVersionCollectionFields } from 'payload'
import toSnakeCase from 'to-snake-case' import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js' import type { DrizzleAdapter } from './types.js'
import { upsertRow } from './upsertRow/index.js' import { upsertRow } from './upsertRow/index.js'
export async function createVersion<T extends TypeWithID>( export async function createVersion<T extends TypeWithID>(
this: PostgresAdapter, this: DrizzleAdapter,
{ {
autosave, autosave,
collectionSlug, collectionSlug,
@@ -45,12 +45,15 @@ export async function createVersion<T extends TypeWithID>(
const table = this.tables[tableName] const table = this.tables[tableName]
if (collection.versions.drafts) { if (collection.versions.drafts) {
await db.execute(sql` await this.execute({
db,
sql: sql`
UPDATE ${table} UPDATE ${table}
SET latest = false SET latest = false
WHERE ${table.id} != ${result.id} WHERE ${table.id} != ${result.id}
AND ${table.parent} = ${parent} AND ${table.parent} = ${parent}
`) `,
})
} }
return result return result

View File

@@ -3,12 +3,12 @@ import type { DeleteMany, PayloadRequest } from 'payload'
import { inArray } from 'drizzle-orm' import { inArray } from 'drizzle-orm'
import toSnakeCase from 'to-snake-case' import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js' import type { DrizzleAdapter } from './types.js'
import { findMany } from './find/findMany.js' import { findMany } from './find/findMany.js'
export const deleteMany: DeleteMany = async function deleteMany( export const deleteMany: DeleteMany = async function deleteMany(
this: PostgresAdapter, this: DrizzleAdapter,
{ collection, req = {} as PayloadRequest, where }, { collection, req = {} as PayloadRequest, where },
) { ) {
const db = this.sessions[await req.transactionID]?.db || this.drizzle const db = this.sessions[await req.transactionID]?.db || this.drizzle
@@ -35,6 +35,10 @@ export const deleteMany: DeleteMany = async function deleteMany(
}) })
if (ids.length > 0) { if (ids.length > 0) {
await db.delete(this.tables[tableName]).where(inArray(this.tables[tableName].id, ids)) await this.deleteWhere({
db,
tableName,
where: inArray(this.tables[tableName].id, ids),
})
} }
} }

View File

@@ -3,7 +3,7 @@ import type { DeleteOne, PayloadRequest } from 'payload'
import { eq } from 'drizzle-orm' import { eq } from 'drizzle-orm'
import toSnakeCase from 'to-snake-case' import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js' import type { DrizzleAdapter } from './types.js'
import { buildFindManyArgs } from './find/buildFindManyArgs.js' import { buildFindManyArgs } from './find/buildFindManyArgs.js'
import buildQuery from './queries/buildQuery.js' import buildQuery from './queries/buildQuery.js'
@@ -11,7 +11,7 @@ import { selectDistinct } from './queries/selectDistinct.js'
import { transform } from './transform/read/index.js' import { transform } from './transform/read/index.js'
export const deleteOne: DeleteOne = async function deleteOne( export const deleteOne: DeleteOne = async function deleteOne(
this: PostgresAdapter, this: DrizzleAdapter,
{ collection: collectionSlug, req = {} as PayloadRequest, where: whereArg }, { collection: collectionSlug, req = {} as PayloadRequest, where: whereArg },
) { ) {
const db = this.sessions[await req.transactionID]?.db || this.drizzle const db = this.sessions[await req.transactionID]?.db || this.drizzle
@@ -63,7 +63,11 @@ export const deleteOne: DeleteOne = async function deleteOne(
fields: collection.fields, fields: collection.fields,
}) })
await db.delete(this.tables[tableName]).where(eq(this.tables[tableName].id, docToDelete.id)) await this.deleteWhere({
db,
tableName,
where: eq(this.tables[tableName].id, docToDelete.id),
})
return result return result
} }

View File

@@ -4,12 +4,12 @@ import { inArray } from 'drizzle-orm'
import { buildVersionCollectionFields } from 'payload' import { buildVersionCollectionFields } from 'payload'
import toSnakeCase from 'to-snake-case' import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js' import type { DrizzleAdapter } from './types.js'
import { findMany } from './find/findMany.js' import { findMany } from './find/findMany.js'
export const deleteVersions: DeleteVersions = async function deleteVersion( export const deleteVersions: DeleteVersions = async function deleteVersion(
this: PostgresAdapter, this: DrizzleAdapter,
{ collection, locale, req = {} as PayloadRequest, where: where }, { collection, locale, req = {} as PayloadRequest, where: where },
) { ) {
const db = this.sessions[await req.transactionID]?.db || this.drizzle const db = this.sessions[await req.transactionID]?.db || this.drizzle
@@ -40,7 +40,11 @@ export const deleteVersions: DeleteVersions = async function deleteVersion(
}) })
if (ids.length > 0) { if (ids.length > 0) {
await db.delete(this.tables[tableName]).where(inArray(this.tables[tableName].id, ids)) await this.deleteWhere({
db,
tableName,
where: inArray(this.tables[tableName].id, ids),
})
} }
return docs return docs

View File

@@ -1,10 +1,10 @@
import type { Destroy } from 'payload' import type { Destroy } from 'payload'
import type { PostgresAdapter } from './types.js' import type { DrizzleAdapter } from './types.js'
// eslint-disable-next-line @typescript-eslint/require-await // eslint-disable-next-line @typescript-eslint/require-await
export const destroy: Destroy = async function destroy(this: PostgresAdapter) { export const destroy: Destroy = async function destroy(this: DrizzleAdapter) {
this.enums = {} if (this.enums) this.enums = {}
this.schema = {} this.schema = {}
this.tables = {} this.tables = {}
this.relations = {} this.relations = {}

View File

@@ -2,12 +2,12 @@ import type { Find, PayloadRequest, SanitizedCollectionConfig } from 'payload'
import toSnakeCase from 'to-snake-case' import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js' import type { DrizzleAdapter } from './types.js'
import { findMany } from './find/findMany.js' import { findMany } from './find/findMany.js'
export const find: Find = async function find( export const find: Find = async function find(
this: PostgresAdapter, this: DrizzleAdapter,
{ {
collection, collection,
limit, limit,

View File

@@ -1,12 +1,12 @@
import type { DBQueryConfig } from 'drizzle-orm' import type { DBQueryConfig } from 'drizzle-orm'
import type { Field } from 'payload' import type { Field } from 'payload'
import type { PostgresAdapter } from '../types.js' import type { DrizzleAdapter } from '../types.js'
import { traverseFields } from './traverseFields.js' import { traverseFields } from './traverseFields.js'
type BuildFindQueryArgs = { type BuildFindQueryArgs = {
adapter: PostgresAdapter adapter: DrizzleAdapter
depth: number depth: number
fields: Field[] fields: Field[]
tableName: string tableName: string

View File

@@ -1,21 +1,20 @@
import type { Field, FindArgs, PayloadRequest, TypeWithID } from 'payload' import type { Field, FindArgs, PayloadRequest, TypeWithID } from 'payload'
import { inArray, sql } from 'drizzle-orm' import { inArray } from 'drizzle-orm'
import type { PostgresAdapter } from '../types.js' import type { DrizzleAdapter } from '../types.js'
import type { ChainedMethods } from './chainMethods.js' import type { ChainedMethods } from './chainMethods.js'
import buildQuery from '../queries/buildQuery.js' import buildQuery from '../queries/buildQuery.js'
import { selectDistinct } from '../queries/selectDistinct.js' import { selectDistinct } from '../queries/selectDistinct.js'
import { transform } from '../transform/read/index.js' import { transform } from '../transform/read/index.js'
import { buildFindManyArgs } from './buildFindManyArgs.js' import { buildFindManyArgs } from './buildFindManyArgs.js'
import { chainMethods } from './chainMethods.js'
type Args = Omit<FindArgs, 'collection'> & { type Args = {
adapter: PostgresAdapter adapter: DrizzleAdapter
fields: Field[] fields: Field[]
tableName: string tableName: string
} } & Omit<FindArgs, 'collection'>
export const findMany = async function find({ export const findMany = async function find({
adapter, adapter,
@@ -31,14 +30,17 @@ export const findMany = async function find({
where: whereArg, where: whereArg,
}: Args) { }: Args) {
const db = adapter.sessions[await req.transactionID]?.db || adapter.drizzle const db = adapter.sessions[await req.transactionID]?.db || adapter.drizzle
const table = adapter.tables[tableName] let limit = limitArg
const limit = limitArg ?? 10
let totalDocs: number let totalDocs: number
let totalPages: number let totalPages: number
let hasPrevPage: boolean let hasPrevPage: boolean
let hasNextPage: boolean let hasNextPage: boolean
let pagingCounter: number let pagingCounter: number
const offset = skip || (page - 1) * limit
if (limit === 0) {
limit = undefined
}
const { joins, orderBy, selectFields, where } = await buildQuery({ const { joins, orderBy, selectFields, where } = await buildQuery({
adapter, adapter,
@@ -68,8 +70,8 @@ export const findMany = async function find({
tableName, tableName,
}) })
selectDistinctMethods.push({ args: [skip || (page - 1) * limit], method: 'offset' }) selectDistinctMethods.push({ args: [offset], method: 'offset' })
selectDistinctMethods.push({ args: [limit === 0 ? undefined : limit], method: 'limit' }) selectDistinctMethods.push({ args: [limit], method: 'limit' })
const selectDistinctResult = await selectDistinct({ const selectDistinctResult = await selectDistinct({
adapter, adapter,
@@ -104,40 +106,25 @@ export const findMany = async function find({
findManyArgs.where = inArray(adapter.tables[tableName].id, orderedIDs) findManyArgs.where = inArray(adapter.tables[tableName].id, orderedIDs)
} }
} else { } else {
findManyArgs.limit = limitArg === 0 ? undefined : limitArg findManyArgs.limit = limit
findManyArgs.offset = offset
const offset = skip || (page - 1) * limitArg findManyArgs.orderBy = orderBy.order(orderBy.column)
if (!Number.isNaN(offset)) findManyArgs.offset = offset
if (where) { if (where) {
findManyArgs.where = where findManyArgs.where = where
} }
findManyArgs.orderBy = orderBy.order(orderBy.column)
} }
const findPromise = db.query[tableName].findMany(findManyArgs) const findPromise = db.query[tableName].findMany(findManyArgs)
if (pagination !== false && (orderedIDs ? orderedIDs?.length <= limit : true)) { if (pagination !== false && (orderedIDs ? orderedIDs?.length <= limit : true)) {
const selectCountMethods: ChainedMethods = [] totalDocs = await adapter.countDistinct({
joins.forEach(({ condition, table }) => { db,
selectCountMethods.push({ joins,
args: [table, condition], tableName,
method: 'leftJoin', where,
})
}) })
const countResult = await chainMethods({
methods: selectCountMethods,
query: db
.select({
count: sql<number>`count
(DISTINCT ${adapter.tables[tableName].id})`,
})
.from(table)
.where(where),
})
totalDocs = Number(countResult[0].count)
totalPages = typeof limit === 'number' && limit !== 0 ? Math.ceil(totalDocs / limit) : 1 totalPages = typeof limit === 'number' && limit !== 0 ? Math.ceil(totalDocs / limit) : 1
hasPrevPage = page > 1 hasPrevPage = page > 1
hasNextPage = totalPages > page hasNextPage = totalPages > page
@@ -171,7 +158,7 @@ export const findMany = async function find({
docs, docs,
hasNextPage, hasNextPage,
hasPrevPage, hasPrevPage,
limit, limit: limitArg,
nextPage: hasNextPage ? page + 1 : null, nextPage: hasNextPage ? page + 1 : null,
page, page,
pagingCounter, pagingCounter,

View File

@@ -4,12 +4,12 @@ import type { Field } from 'payload'
import { fieldAffectsData, tabHasName } from 'payload/shared' import { fieldAffectsData, tabHasName } from 'payload/shared'
import toSnakeCase from 'to-snake-case' import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from '../types.js' import type { DrizzleAdapter } from '../types.js'
import type { Result } from './buildFindManyArgs.js' import type { Result } from './buildFindManyArgs.js'
type TraverseFieldArgs = { type TraverseFieldArgs = {
_locales: Result _locales: Result
adapter: PostgresAdapter adapter: DrizzleAdapter
currentArgs: Result currentArgs: Result
currentTableName: string currentTableName: string
depth?: number depth?: number

View File

@@ -2,12 +2,12 @@ import type { FindGlobal } from 'payload'
import toSnakeCase from 'to-snake-case' import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js' import type { DrizzleAdapter } from './types.js'
import { findMany } from './find/findMany.js' import { findMany } from './find/findMany.js'
export const findGlobal: FindGlobal = async function findGlobal( export const findGlobal: FindGlobal = async function findGlobal(
this: PostgresAdapter, this: DrizzleAdapter,
{ slug, locale, req, where }, { slug, locale, req, where },
) { ) {
const globalConfig = this.payload.globals.config.find((config) => config.slug === slug) const globalConfig = this.payload.globals.config.find((config) => config.slug === slug)

View File

@@ -3,12 +3,12 @@ import type { FindGlobalVersions, PayloadRequest, SanitizedGlobalConfig } from '
import { buildVersionGlobalFields } from 'payload' import { buildVersionGlobalFields } from 'payload'
import toSnakeCase from 'to-snake-case' import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js' import type { DrizzleAdapter } from './types.js'
import { findMany } from './find/findMany.js' import { findMany } from './find/findMany.js'
export const findGlobalVersions: FindGlobalVersions = async function findGlobalVersions( export const findGlobalVersions: FindGlobalVersions = async function findGlobalVersions(
this: PostgresAdapter, this: DrizzleAdapter,
{ {
global, global,
limit, limit,

View File

@@ -0,0 +1,41 @@
import fs from 'fs'
import path from 'path'
/**
* Attempt to find migrations directory.
*
* Checks for the following directories in order:
* - `migrationDir` argument from Payload config
* - `src/migrations`
* - `dist/migrations`
* - `migrations`
*
* Defaults to `src/migrations`
*
* @param migrationDir
* @returns
*/
export function findMigrationDir(migrationDir?: string): string {
const cwd = process.cwd()
const srcDir = path.resolve(cwd, 'src/migrations')
const distDir = path.resolve(cwd, 'dist/migrations')
const relativeMigrations = path.resolve(cwd, 'migrations')
// Use arg if provided
if (migrationDir) return migrationDir
// Check other common locations
if (fs.existsSync(srcDir)) {
return srcDir
}
if (fs.existsSync(distDir)) {
return distDir
}
if (fs.existsSync(relativeMigrations)) {
return relativeMigrations
}
return srcDir
}

View File

@@ -2,12 +2,12 @@ import type { FindOneArgs, PayloadRequest, SanitizedCollectionConfig, TypeWithID
import toSnakeCase from 'to-snake-case' import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js' import type { DrizzleAdapter } from './types.js'
import { findMany } from './find/findMany.js' import { findMany } from './find/findMany.js'
export async function findOne<T extends TypeWithID>( export async function findOne<T extends TypeWithID>(
this: PostgresAdapter, this: DrizzleAdapter,
{ collection, locale, req = {} as PayloadRequest, where }: FindOneArgs, { collection, locale, req = {} as PayloadRequest, where }: FindOneArgs,
): Promise<T> { ): Promise<T> {
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config

View File

@@ -3,12 +3,12 @@ import type { FindVersions, PayloadRequest, SanitizedCollectionConfig } from 'pa
import { buildVersionCollectionFields } from 'payload' import { buildVersionCollectionFields } from 'payload'
import toSnakeCase from 'to-snake-case' import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js' import type { DrizzleAdapter } from './types.js'
import { findMany } from './find/findMany.js' import { findMany } from './find/findMany.js'
export const findVersions: FindVersions = async function findVersions( export const findVersions: FindVersions = async function findVersions(
this: PostgresAdapter, this: DrizzleAdapter,
{ {
collection, collection,
limit, limit,

View File

@@ -0,0 +1,37 @@
export { count } from './count.js'
export { create } from './create.js'
export { createGlobal } from './createGlobal.js'
export { createGlobalVersion } from './createGlobalVersion.js'
export { createTableName } from './createTableName.js'
export { createVersion } from './createVersion.js'
export { deleteMany } from './deleteMany.js'
export { deleteOne } from './deleteOne.js'
export { deleteVersions } from './deleteVersions.js'
export { destroy } from './destroy.js'
export { find } from './find.js'
export { chainMethods } from './find/chainMethods.js'
export { findGlobal } from './findGlobal.js'
export { findGlobalVersions } from './findGlobalVersions.js'
export { findMigrationDir } from './findMigrationDir.js'
export { findOne } from './findOne.js'
export { findVersions } from './findVersions.js'
export { migrate } from './migrate.js'
export { migrateDown } from './migrateDown.js'
export { migrateFresh } from './migrateFresh.js'
export { migrateRefresh } from './migrateRefresh.js'
export { migrateReset } from './migrateReset.js'
export { migrateStatus } from './migrateStatus.js'
export { operatorMap } from './queries/operatorMap.js'
export type { Operators } from './queries/operatorMap.js'
export { queryDrafts } from './queryDrafts.js'
export { beginTransaction } from './transactions/beginTransaction.js'
export { commitTransaction } from './transactions/commitTransaction.js'
export { rollbackTransaction } from './transactions/rollbackTransaction.js'
export { updateOne } from './update.js'
export { updateGlobal } from './updateGlobal.js'
export { updateGlobalVersion } from './updateGlobalVersion.js'
export { updateVersion } from './updateVersion.js'
export { upsertRow } from './upsertRow/index.js'
export { hasLocalesTable } from './utilities/hasLocalesTable.js'
export { pushDevSchema } from './utilities/pushDevSchema.js'
export { validateExistingBlockIsIdentical } from './utilities/validateExistingBlockIsIdentical.js'

View File

@@ -1,21 +1,15 @@
/* eslint-disable no-restricted-syntax, no-await-in-loop */
import type { Payload } from 'payload'
import type { PayloadRequest } from 'payload'
import type { Migration } from 'payload'
import { createRequire } from 'module' import type { Payload, PayloadRequest } from 'payload'
import { commitTransaction, initTransaction, killTransaction, readMigrationFiles } from 'payload' import { commitTransaction, initTransaction, killTransaction, readMigrationFiles } from 'payload'
import prompts from 'prompts' import prompts from 'prompts'
import type { PostgresAdapter } from './types.js' import type { DrizzleAdapter, Migration } from './types.js'
import { createMigrationTable } from './utilities/createMigrationTable.js'
import { migrationTableExists } from './utilities/migrationTableExists.js' import { migrationTableExists } from './utilities/migrationTableExists.js'
import { parseError } from './utilities/parseError.js' import { parseError } from './utilities/parseError.js'
const require = createRequire(import.meta.url) export async function migrate(this: DrizzleAdapter): Promise<void> {
export async function migrate(this: PostgresAdapter): Promise<void> {
const { payload } = this const { payload } = this
const migrationFiles = await readMigrationFiles({ payload }) const migrationFiles = await readMigrationFiles({ payload })
@@ -27,7 +21,7 @@ export async function migrate(this: PostgresAdapter): Promise<void> {
let latestBatch = 0 let latestBatch = 0
let migrationsInDB = [] let migrationsInDB = []
const hasMigrationTable = await migrationTableExists(this.drizzle) const hasMigrationTable = await migrationTableExists(this)
if (hasMigrationTable) { if (hasMigrationTable) {
;({ docs: migrationsInDB } = await payload.find({ ;({ docs: migrationsInDB } = await payload.find({
@@ -38,8 +32,6 @@ export async function migrate(this: PostgresAdapter): Promise<void> {
if (Number(migrationsInDB?.[0]?.batch) > 0) { if (Number(migrationsInDB?.[0]?.batch) > 0) {
latestBatch = Number(migrationsInDB[0]?.batch) latestBatch = Number(migrationsInDB[0]?.batch)
} }
} else {
await createMigrationTable(this)
} }
if (migrationsInDB.find((m) => m.batch === -1)) { if (migrationsInDB.find((m) => m.batch === -1)) {
@@ -72,7 +64,7 @@ export async function migrate(this: PostgresAdapter): Promise<void> {
// If already ran, skip // If already ran, skip
if (alreadyRan) { if (alreadyRan) {
continue // eslint-disable-line no-continue continue
} }
await runMigrationFile(payload, migration, newBatch) await runMigrationFile(payload, migration, newBatch)
@@ -80,19 +72,20 @@ export async function migrate(this: PostgresAdapter): Promise<void> {
} }
async function runMigrationFile(payload: Payload, migration: Migration, batch: number) { async function runMigrationFile(payload: Payload, migration: Migration, batch: number) {
const { generateDrizzleJson } = require('drizzle-kit/payload') const db = payload.db as DrizzleAdapter
const { generateDrizzleJson } = db.requireDrizzleKit()
const start = Date.now() const start = Date.now()
const req = { payload } as PayloadRequest const req = { payload } as PayloadRequest
const adapter = payload.db as DrizzleAdapter
payload.logger.info({ msg: `Migrating: ${migration.name}` }) payload.logger.info({ msg: `Migrating: ${migration.name}` })
const pgAdapter = payload.db const drizzleJSON = await generateDrizzleJson({ schema: adapter.schema })
const drizzleJSON = generateDrizzleJson(pgAdapter.schema)
try { try {
await initTransaction(req) await initTransaction(req)
await migration.up({ payload, req }) const db = adapter?.sessions[await req.transactionID]?.db || adapter.drizzle
await migration.up({ db, payload, req })
payload.logger.info({ msg: `Migrated: ${migration.name} (${Date.now() - start}ms)` }) payload.logger.info({ msg: `Migrated: ${migration.name} (${Date.now() - start}ms)` })
await payload.create({ await payload.create({
collection: 'payload-migrations', collection: 'payload-migrations',

View File

@@ -9,12 +9,12 @@ import {
readMigrationFiles, readMigrationFiles,
} from 'payload' } from 'payload'
import type { PostgresAdapter } from './types.js' import type { DrizzleAdapter } from './types.js'
import { migrationTableExists } from './utilities/migrationTableExists.js' import { migrationTableExists } from './utilities/migrationTableExists.js'
import { parseError } from './utilities/parseError.js' import { parseError } from './utilities/parseError.js'
export async function migrateDown(this: PostgresAdapter): Promise<void> { export async function migrateDown(this: DrizzleAdapter): Promise<void> {
const { payload } = this const { payload } = this
const migrationFiles = await readMigrationFiles({ payload }) const migrationFiles = await readMigrationFiles({ payload })
@@ -50,7 +50,7 @@ export async function migrateDown(this: PostgresAdapter): Promise<void> {
msg: `Migrated down: ${migrationFile.name} (${Date.now() - start}ms)`, msg: `Migrated down: ${migrationFile.name} (${Date.now() - start}ms)`,
}) })
const tableExists = await migrationTableExists(this.drizzle) const tableExists = await migrationTableExists(this)
if (tableExists) { if (tableExists) {
await payload.delete({ await payload.delete({
id: migration.id, id: migration.id,

View File

@@ -1,10 +1,9 @@
import type { PayloadRequest } from 'payload' import type { PayloadRequest } from 'payload'
import { sql } from 'drizzle-orm'
import { commitTransaction, initTransaction, killTransaction, readMigrationFiles } from 'payload' import { commitTransaction, initTransaction, killTransaction, readMigrationFiles } from 'payload'
import prompts from 'prompts' import prompts from 'prompts'
import type { PostgresAdapter } from './types.js' import type { DrizzleAdapter, Migration } from './types.js'
import { parseError } from './utilities/parseError.js' import { parseError } from './utilities/parseError.js'
@@ -12,7 +11,7 @@ import { parseError } from './utilities/parseError.js'
* Drop the current database and run all migrate up functions * Drop the current database and run all migrate up functions
*/ */
export async function migrateFresh( export async function migrateFresh(
this: PostgresAdapter, this: DrizzleAdapter,
{ forceAcceptWarning = false }, { forceAcceptWarning = false },
): Promise<void> { ): Promise<void> {
const { payload } = this const { payload } = this
@@ -41,12 +40,9 @@ export async function migrateFresh(
msg: `Dropping database.`, msg: `Dropping database.`,
}) })
await this.drizzle.execute( await this.dropDatabase({ adapter: this })
sql.raw(`drop schema ${this.schemaName || 'public'} cascade;
create schema ${this.schemaName || 'public'};`),
)
const migrationFiles = await readMigrationFiles({ payload }) const migrationFiles = (await readMigrationFiles({ payload })) as Migration[]
payload.logger.debug({ payload.logger.debug({
msg: `Found ${migrationFiles.length} migration files.`, msg: `Found ${migrationFiles.length} migration files.`,
}) })
@@ -58,7 +54,9 @@ export async function migrateFresh(
try { try {
const start = Date.now() const start = Date.now()
await initTransaction(req) await initTransaction(req)
await migration.up({ payload, req }) const adapter = payload.db as DrizzleAdapter
const db = adapter?.sessions[await req.transactionID]?.db || adapter.drizzle
await migration.up({ db, payload, req })
await payload.create({ await payload.create({
collection: 'payload-migrations', collection: 'payload-migrations',
data: { data: {

View File

@@ -9,7 +9,7 @@ import {
readMigrationFiles, readMigrationFiles,
} from 'payload' } from 'payload'
import type { PostgresAdapter } from './types.js' import type { DrizzleAdapter } from './types.js'
import { migrationTableExists } from './utilities/migrationTableExists.js' import { migrationTableExists } from './utilities/migrationTableExists.js'
import { parseError } from './utilities/parseError.js' import { parseError } from './utilities/parseError.js'
@@ -17,7 +17,7 @@ import { parseError } from './utilities/parseError.js'
/** /**
* Run all migration down functions before running up * Run all migration down functions before running up
*/ */
export async function migrateRefresh(this: PostgresAdapter) { export async function migrateRefresh(this: DrizzleAdapter) {
const { payload } = this const { payload } = this
const migrationFiles = await readMigrationFiles({ payload }) const migrationFiles = await readMigrationFiles({ payload })
@@ -54,7 +54,7 @@ export async function migrateRefresh(this: PostgresAdapter) {
msg: `Migrated down: ${migration.name} (${Date.now() - start}ms)`, msg: `Migrated down: ${migration.name} (${Date.now() - start}ms)`,
}) })
const tableExists = await migrationTableExists(this.drizzle) const tableExists = await migrationTableExists(this)
if (tableExists) { if (tableExists) {
await payload.delete({ await payload.delete({
collection: 'payload-migrations', collection: 'payload-migrations',

View File

@@ -9,14 +9,14 @@ import {
readMigrationFiles, readMigrationFiles,
} from 'payload' } from 'payload'
import type { PostgresAdapter } from './types.js' import type { DrizzleAdapter } from './types.js'
import { migrationTableExists } from './utilities/migrationTableExists.js' import { migrationTableExists } from './utilities/migrationTableExists.js'
/** /**
* Run all migrate down functions * Run all migrate down functions
*/ */
export async function migrateReset(this: PostgresAdapter): Promise<void> { export async function migrateReset(this: DrizzleAdapter): Promise<void> {
const { payload } = this const { payload } = this
const migrationFiles = await readMigrationFiles({ payload }) const migrationFiles = await readMigrationFiles({ payload })
@@ -45,7 +45,7 @@ export async function migrateReset(this: PostgresAdapter): Promise<void> {
msg: `Migrated down: ${migrationFile.name} (${Date.now() - start}ms)`, msg: `Migrated down: ${migrationFile.name} (${Date.now() - start}ms)`,
}) })
const tableExists = await migrationTableExists(this.drizzle) const tableExists = await migrationTableExists(this)
if (tableExists) { if (tableExists) {
await payload.delete({ await payload.delete({
id: migration.id, id: migration.id,
@@ -71,7 +71,7 @@ export async function migrateReset(this: PostgresAdapter): Promise<void> {
// Delete dev migration // Delete dev migration
const tableExists = await migrationTableExists(this.drizzle) const tableExists = await migrationTableExists(this)
if (tableExists) { if (tableExists) {
try { try {
await payload.delete({ await payload.delete({

View File

@@ -1,11 +1,11 @@
import { Table } from 'console-table-printer' import { Table } from 'console-table-printer'
import { getMigrations, readMigrationFiles } from 'payload' import { getMigrations, readMigrationFiles } from 'payload'
import type { PostgresAdapter } from './types.js' import type { DrizzleAdapter } from './types.js'
import { migrationTableExists } from './utilities/migrationTableExists.js' import { migrationTableExists } from './utilities/migrationTableExists.js'
export async function migrateStatus(this: PostgresAdapter): Promise<void> { export async function migrateStatus(this: DrizzleAdapter): Promise<void> {
const { payload } = this const { payload } = this
const migrationFiles = await readMigrationFiles({ payload }) const migrationFiles = await readMigrationFiles({ payload })
@@ -14,7 +14,7 @@ export async function migrateStatus(this: PostgresAdapter): Promise<void> {
}) })
let existingMigrations = [] let existingMigrations = []
const hasMigrationTable = await migrationTableExists(this.drizzle) const hasMigrationTable = await migrationTableExists(this)
if (hasMigrationTable) { if (hasMigrationTable) {
;({ existingMigrations } = await getMigrations({ payload })) ;({ existingMigrations } = await getMigrations({ payload }))

View File

@@ -1,7 +1,7 @@
import type { SQL } from 'drizzle-orm' import type { SQL } from 'drizzle-orm'
import type { Field, Where } from 'payload' import type { Field, Where } from 'payload'
import type { GenericColumn, PostgresAdapter } from '../types.js' import type { DrizzleAdapter, GenericColumn } from '../types.js'
import type { BuildQueryJoinAliases } from './buildQuery.js' import type { BuildQueryJoinAliases } from './buildQuery.js'
import { parseParams } from './parseParams.js' import { parseParams } from './parseParams.js'
@@ -15,7 +15,7 @@ export async function buildAndOrConditions({
tableName, tableName,
where, where,
}: { }: {
adapter: PostgresAdapter adapter: DrizzleAdapter
collectionSlug?: string collectionSlug?: string
fields: Field[] fields: Field[]
globalSlug?: string globalSlug?: string

View File

@@ -4,20 +4,18 @@ import type { Field, Where } from 'payload'
import { asc, desc } from 'drizzle-orm' import { asc, desc } from 'drizzle-orm'
import type { GenericColumn, GenericTable, PostgresAdapter } from '../types.js' import type { DrizzleAdapter, GenericColumn, GenericTable } from '../types.js'
import { getTableColumnFromPath } from './getTableColumnFromPath.js' import { getTableColumnFromPath } from './getTableColumnFromPath.js'
import { parseParams } from './parseParams.js' import { parseParams } from './parseParams.js'
export type BuildQueryJoins = Record<string, SQL>
export type BuildQueryJoinAliases = { export type BuildQueryJoinAliases = {
condition: SQL condition: SQL
table: GenericTable | PgTableWithColumns<any> table: GenericTable | PgTableWithColumns<any>
}[] }[]
type BuildQueryArgs = { type BuildQueryArgs = {
adapter: PostgresAdapter adapter: DrizzleAdapter
fields: Field[] fields: Field[]
locale?: string locale?: string
sort?: string sort?: string

Some files were not shown because too many files have changed in this diff Show More