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
/media
test/media
*payloadtests.db
*payloadtests.db-journal
*payloadtests.db-shm
*payloadtests.db-wal
/versions
# 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/.turbo" />
<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>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />

View File

@@ -33,18 +33,18 @@ export default buildConfig({
## Options
| Option | Description |
|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `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. |
| `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. |
| `schemaName` | A string for the postgres schema to use, defaults to 'public'. |
| `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'. |
| Option | Description |
|-----------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `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. |
| `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. |
| `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'. |
| `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

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.commitTransaction` - Takes the identifier for the transaction, finalizes 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:db-mongodb": "turbo build --filter db-mongodb",
"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-resend": "turbo build --filter email-resend",
"build:eslint-config": "turbo build --filter eslint-config",
@@ -93,6 +95,7 @@
},
"devDependencies": {
"@jest/globals": "29.7.0",
"@libsql/client": "0.6.2",
"@next/bundle-analyzer": "15.0.0-canary.53",
"@payloadcms/eslint-config": "workspace:*",
"@payloadcms/eslint-plugin": "workspace:*",
@@ -116,7 +119,6 @@
"create-payload-app": "workspace:*",
"cross-env": "7.0.3",
"dotenv": "16.4.5",
"drizzle-kit": "0.20.14-1f2c838",
"drizzle-orm": "0.29.4",
"escape-html": "^1.0.3",
"execa": "5.1.1",

View File

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

View File

@@ -40,11 +40,12 @@
"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",
"renamePredefinedMigrations": "tsx ./scripts/renamePredefinedMigrations.ts"
},
"dependencies": {
"@libsql/client": "^0.5.2",
"@payloadcms/drizzle": "workspace:*",
"console-table-printer": "2.11.2",
"drizzle-kit": "0.20.14-1f2c838",
"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 { sql } from 'drizzle-orm'
import { pushDevSchema } from '@payloadcms/drizzle'
import { drizzle } from 'drizzle-orm/node-postgres'
import pg from 'pg'
import type { PostgresAdapter } from './types.js'
import { pushDevSchema } from './utilities/pushDevSchema.js'
const connectWithReconnect = async function ({
adapter,
payload,
@@ -71,12 +70,7 @@ export const connect: Connect = async function connect(
if (!hotReload) {
if (process.env.PAYLOAD_DROP_DATABASE === 'true') {
this.payload.logger.info(`---- DROPPING TABLES SCHEMA(${this.schemaName || 'public'}) ----`)
await this.drizzle.execute(
sql.raw(`
drop schema if exists ${this.schemaName || 'public'} cascade;
create schema ${this.schemaName || 'public'};
`),
)
await this.dropDatabase({ adapter: this })
this.payload.logger.info('---- DROPPED TABLES ----')
}
}
@@ -92,7 +86,7 @@ export const connect: Connect = async function connect(
process.env.PAYLOAD_MIGRATING !== 'true' &&
this.push !== false
) {
await pushDevSchema(this)
await pushDevSchema(this as unknown as DrizzleAdapter)
}
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 { CreateMigration, MigrationTemplateArgs } from 'payload'
import type { CreateMigration } from 'payload'
import fs from 'fs'
import { createRequire } from 'module'
@@ -11,38 +10,11 @@ import { fileURLToPath } from 'url'
import type { PostgresAdapter } from './types.js'
import { defaultDrizzleSnapshot } from './defaultSnapshot.js'
import { getMigrationTemplate } from './getMigrationTemplate.js'
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(
this: PostgresAdapter,
{ file, forceAcceptWarning, migrationName, payload },
@@ -75,7 +47,7 @@ export const createMigration: CreateMigration = async function createMigration(
const filePath = `${dir}/${fileName}`
let drizzleJsonBefore = getDefaultDrizzleSnapshot()
let drizzleJsonBefore = defaultDrizzleSnapshot
if (!upSQL) {
// Get latest migration snapshot
@@ -93,7 +65,7 @@ export const createMigration: CreateMigration = async function createMigration(
const sqlStatementsUp = await generateMigration(drizzleJsonBefore, drizzleJsonAfter)
const sqlStatementsDown = await generateMigration(drizzleJsonAfter, drizzleJsonBefore)
const sqlExecute = 'await payload.db.drizzle.execute(sql`'
const sqlExecute = 'await db.execute(sql`'
if (sqlStatementsUp?.length) {
upSQL = `${sqlExecute}\n ${sqlStatementsUp?.join('\n')}\`)`
@@ -121,15 +93,15 @@ export const createMigration: CreateMigration = async function createMigration(
process.exit(0)
}
}
}
// write schema
fs.writeFileSync(`${filePath}.json`, JSON.stringify(drizzleJsonAfter, null, 2))
// write schema
fs.writeFileSync(`${filePath}.json`, JSON.stringify(drizzleJsonAfter, null, 2))
}
// write migration
fs.writeFileSync(
`${filePath}.ts`,
migrationTemplate({
getMigrationTemplate({
downSQL: downSQL || ` // Migration code`,
imports,
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 fs from 'fs'
import path from 'path'
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 { createDatabaseAdapter } from 'payload'
import type { Args, PostgresAdapter } from './types.js'
import { connect } from './connect.js'
import { count } from './count.js'
import { create } from './create.js'
import { createGlobal } from './createGlobal.js'
import { createGlobalVersion } from './createGlobalVersion.js'
import { countDistinct } from './countDistinct.js'
import { convertPathToJSONTraversal } from './createJSONQuery/convertPathToJSONTraversal.js'
import { createJSONQuery } from './createJSONQuery/index.js'
import { createMigration } from './createMigration.js'
import { createVersion } from './createVersion.js'
import { deleteMany } from './deleteMany.js'
import { deleteOne } from './deleteOne.js'
import { deleteVersions } from './deleteVersions.js'
import { destroy } from './destroy.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 { 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 { migrate } from './migrate.js'
import { migrateDown } from './migrateDown.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'
import { insert } from './insert.js'
import { requireDrizzleKit } from './requireDrizzleKit.js'
export type { MigrateDownArgs, MigrateUpArgs } from './types.js'
@@ -58,13 +70,19 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
return createDatabaseAdapter<PostgresAdapter>({
name: 'postgres',
defaultDrizzleSnapshot,
drizzle: undefined,
enums: {},
features: {
json: true,
},
fieldConstraints: {},
getMigrationTemplate,
idType: postgresIDType,
initializing,
localesSuffix: args.localesSuffix || '_locales',
logger: args.logger,
operators: operatorMap,
pgSchema: undefined,
pool: undefined,
poolOptions: args.pool,
@@ -76,29 +94,37 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
sessions: {},
tableNameMap: new Map<string, string>(),
tables: {},
transactionOptions: args.transactionOptions || undefined,
versionsSuffix: args.versionsSuffix || '_v',
// DatabaseAdapter
beginTransaction,
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,
@@ -109,6 +135,7 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
payload,
queryDrafts,
rejectInitializing,
requireDrizzleKit,
resolveInitializing,
rollbackTransaction,
updateGlobal,
@@ -123,42 +150,3 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
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 { createTableName } from '../../drizzle/src/createTableName.js'
import { buildTable } from './schema/build.js'
import { createTableName } from './schema/createTableName.js'
export const init: Init = function init(this: PostgresAdapter) {
if (this.schemaName) {
@@ -17,7 +17,6 @@ export const init: Init = function init(this: PostgresAdapter) {
} else {
this.pgSchema = { table: pgTable }
}
if (this.payload.config.localization) {
this.enums.enum__locales = pgEnum(
'_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 { DrizzleTransaction, PostgresAdapter } from '../../../types.js'
import { upsertRow } from '@payloadcms/drizzle'
import type { PostgresAdapter } from '../../../types.js'
import type { DocsToResave } from '../types.js'
import { upsertRow } from '../../../upsertRow/index.js'
import { traverseFields } from './traverseFields.js'
type Args = {
adapter: PostgresAdapter
collectionSlug?: string
db: DrizzleTransaction
db: TransactionPg
debug: boolean
docsToResave: DocsToResave
fields: Field[]

View File

@@ -1,3 +1,4 @@
import type { TransactionPg } from '@payloadcms/drizzle/types'
import type { DrizzleSnapshotJSON } from 'drizzle-kit/payload'
import type { Payload, PayloadRequest } from 'payload'
@@ -37,8 +38,8 @@ type Args = {
* @param req
*/
export const migratePostgresV2toV3 = async ({ debug, payload, req }: Args) => {
const adapter = payload.db as PostgresAdapter
const db = adapter.sessions[await req.transactionID]?.db
const adapter = payload.db as unknown as PostgresAdapter
const db = adapter.sessions[await req.transactionID].db as TransactionPg
const dir = payload.db.migrationDir
// 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 { 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 { fetchAndResave } from './fetchAndResave/index.js'
@@ -10,7 +11,7 @@ import { fetchAndResave } from './fetchAndResave/index.js'
type Args = {
adapter: PostgresAdapter
collectionSlug?: string
db: DrizzleTransaction
db: TransactionPg
debug: boolean
fields: Field[]
globalSlug?: string

View File

@@ -1,16 +1,17 @@
import type { TransactionPg } from '@payloadcms/drizzle/types'
import type { Field, Payload } from 'payload'
import { tabHasName } from 'payload/shared'
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'
type Args = {
adapter: PostgresAdapter
collectionSlug?: string
columnPrefix: string
db: DrizzleTransaction
db: TransactionPg
disableNotNull: boolean
fields: Field[]
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'
import type { Field } from 'payload'
import { createTableName } from '@payloadcms/drizzle'
import { relations } from 'drizzle-orm'
import {
foreignKey,
@@ -24,7 +25,6 @@ import toSnakeCase from 'to-snake-case'
import type { GenericColumns, GenericTable, IDType, PostgresAdapter } from '../types.js'
import { createTableName } from './createTableName.js'
import { parentIDColumnMap } from './parentIDColumnMap.js'
import { setColumnID } from './setColumnID.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 { Field, TabAsField } from 'payload'
import {
createTableName,
hasLocalesTable,
validateExistingBlockIsIdentical,
} from '@payloadcms/drizzle'
import { relations } from 'drizzle-orm'
import {
PgNumericBuilder,
@@ -26,13 +31,10 @@ import toSnakeCase from 'to-snake-case'
import type { GenericColumns, IDType, PostgresAdapter } from '../types.js'
import type { BaseExtraConfig, RelationMap } from './build.js'
import { hasLocalesTable } from '../utilities/hasLocalesTable.js'
import { buildTable } from './build.js'
import { createIndex } from './createIndex.js'
import { createTableName } from './createTableName.js'
import { idToUUID } from './idToUUID.js'
import { parentIDColumnMap } from './parentIDColumnMap.js'
import { validateExistingBlockIsIdentical } from './validateExistingBlockIsIdentical.js'
type Args = {
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 {
ColumnBaseConfig,
ColumnDataType,
DrizzleConfig,
ExtractTablesWithRelations,
Relation,
Relations,
SQL,
} from 'drizzle-orm'
import type { NodePgDatabase, NodePgQueryResultHKT } from 'drizzle-orm/node-postgres'
import type { NodePgDatabase } from 'drizzle-orm/node-postgres'
import type {
PgColumn,
PgEnum,
PgInsertOnConflictDoUpdateConfig,
PgSchema,
PgTableWithColumns,
PgTransaction,
PgTransactionConfig,
} from 'drizzle-orm/pg-core'
import type { PgTableFn } from 'drizzle-orm/pg-core/table'
import type { BaseDatabaseAdapter, Payload, PayloadRequest } from 'payload'
import type { Pool, PoolConfig } from 'pg'
export type DrizzleDB = NodePgDatabase<Record<string, unknown>>
import type { Payload, PayloadRequest } from 'payload'
import type { Pool, PoolConfig, QueryResult } from 'pg'
export type Args = {
idType?: 'serial' | 'uuid'
@@ -28,7 +34,12 @@ export type Args = {
pool: PoolConfig
push?: boolean
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
transactionOptions?: PgTransactionConfig | false
versionsSuffix?: string
}
@@ -45,22 +56,64 @@ export type GenericTable = PgTableWithColumns<{
columns: GenericColumns
dialect: string
name: string
schema: undefined
schema: string
}>
export type GenericEnum = PgEnum<[string, ...string[]]>
export type GenericRelation = Relations<string, Record<string, Relation<string>>>
export type DrizzleTransaction = PgTransaction<
NodePgQueryResultHKT,
Record<string, unknown>,
ExtractTablesWithRelations<Record<string, unknown>>
export type PostgresDB = NodePgDatabase<Record<string, unknown>>
export type CountDistinct = (args: {
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 & {
drizzle: DrizzleDB
export type PostgresAdapter = {
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
@@ -68,8 +121,10 @@ export type PostgresAdapter = BaseDatabaseAdapter & {
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']
@@ -82,44 +137,45 @@ export type PostgresAdapter = BaseDatabaseAdapter & {
schemaName?: Args['schemaName']
sessions: {
[id: string]: {
db: DrizzleTransaction
db: PostgresDB | TransactionPg
reject: () => Promise<void>
resolve: () => Promise<void>
}
}
tableNameMap: Map<string, string>
tables: Record<string, GenericTable | PgTableWithColumns<any>>
tables: Record<string, GenericTable>
versionsSuffix?: string
}
} & PostgresDrizzleAdapter
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
extends Omit<Args, 'idType' | 'logger' | 'migrationDir' | 'pool'>,
DrizzleAdapter {
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
* Used for returning properly formed errors from unique fields
*/
fieldConstraints: Record<string, Record<string, string>>
localeSuffix?: string
idType: Args['idType']
initializing: Promise<void>
localesSuffix?: string
logger: DrizzleConfig['logger']
pgSchema?: { table: PgTableFn } | PgSchema
pool: Pool
poolOptions: Args['pool']
push: boolean
relations: Record<string, GenericRelation>
rejectInitializing: () => void
relationshipsSuffix?: string
resolveInitializing: () => void
schema: Record<string, GenericEnum | GenericRelation | GenericTable>
sessions: {
[id: string]: {
db: DrizzleTransaction
reject: () => Promise<void>
resolve: () => Promise<void>
}
}
tables: Record<string, GenericTable>
schemaName?: Args['schemaName']
tableNameMap: Map<string, 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",
"compilerOptions": {
"composite": true, // Make sure typescript knows that this module depends on their references
"noEmit": false /* Do not emit outputs. */,
// Make sure typescript knows that this module depends on their references
"composite": true,
/* Do not emit outputs. */
"noEmit": false,
"emitDeclarationOnly": true,
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
"rootDir": "./src" /* Specify the root folder within your source files. */
/* Specify an output folder for all emitted files. */
"outDir": "./dist",
/* Specify the root folder within your source files. */
"rootDir": "./src"
},
"exclude": [
"dist",
@@ -19,6 +23,19 @@
"src/**/*.spec.ts",
"src/**/*.spec.tsx"
],
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts", "src/**/*.json"],
"references": [{ "path": "../payload" }, { "path": "../translations" }]
"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,
},
}

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 type { PostgresAdapter } from './types.js'
import type { DrizzleAdapter } from './types.js'
import { upsertRow } from './upsertRow/index.js'
export const create: Create = async function create(
this: PostgresAdapter,
this: DrizzleAdapter,
{ collection: collectionSlug, data, req },
) {
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 type { PostgresAdapter } from './types.js'
import type { DrizzleAdapter } from './types.js'
import { upsertRow } from './upsertRow/index.js'
export async function createGlobal<T extends Record<string, unknown>>(
this: PostgresAdapter,
this: DrizzleAdapter,
{ slug, data, req = {} as PayloadRequest }: CreateGlobalArgs,
): Promise<T> {
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 toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import type { DrizzleAdapter } from './types.js'
import { upsertRow } from './upsertRow/index.js'
export async function createGlobalVersion<T extends TypeWithID>(
this: PostgresAdapter,
this: DrizzleAdapter,
{ autosave, globalSlug, req = {} as PayloadRequest, versionData }: CreateGlobalVersionArgs,
) {
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]
if (global.versions.drafts) {
await db.execute(sql`
UPDATE ${table}
SET latest = false
WHERE ${table.id} != ${result.id};
`)
await this.execute({
db,
sql: sql`
UPDATE ${table}
SET latest = false
WHERE ${table.id} != ${result.id};
`,
})
}
return result

View File

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

View File

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

View File

@@ -3,12 +3,12 @@ import type { DeleteMany, PayloadRequest } from 'payload'
import { inArray } from 'drizzle-orm'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import type { DrizzleAdapter } from './types.js'
import { findMany } from './find/findMany.js'
export const deleteMany: DeleteMany = async function deleteMany(
this: PostgresAdapter,
this: DrizzleAdapter,
{ collection, req = {} as PayloadRequest, where },
) {
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) {
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 toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import type { DrizzleAdapter } from './types.js'
import { buildFindManyArgs } from './find/buildFindManyArgs.js'
import buildQuery from './queries/buildQuery.js'
@@ -11,7 +11,7 @@ import { selectDistinct } from './queries/selectDistinct.js'
import { transform } from './transform/read/index.js'
export const deleteOne: DeleteOne = async function deleteOne(
this: PostgresAdapter,
this: DrizzleAdapter,
{ collection: collectionSlug, req = {} as PayloadRequest, where: whereArg },
) {
const db = this.sessions[await req.transactionID]?.db || this.drizzle
@@ -63,7 +63,11 @@ export const deleteOne: DeleteOne = async function deleteOne(
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
}

View File

@@ -4,12 +4,12 @@ import { inArray } from 'drizzle-orm'
import { buildVersionCollectionFields } from 'payload'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import type { DrizzleAdapter } from './types.js'
import { findMany } from './find/findMany.js'
export const deleteVersions: DeleteVersions = async function deleteVersion(
this: PostgresAdapter,
this: DrizzleAdapter,
{ collection, locale, req = {} as PayloadRequest, where: where },
) {
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) {
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

View File

@@ -1,10 +1,10 @@
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
export const destroy: Destroy = async function destroy(this: PostgresAdapter) {
this.enums = {}
export const destroy: Destroy = async function destroy(this: DrizzleAdapter) {
if (this.enums) this.enums = {}
this.schema = {}
this.tables = {}
this.relations = {}

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,12 +2,12 @@ import type { FindGlobal } from 'payload'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import type { DrizzleAdapter } from './types.js'
import { findMany } from './find/findMany.js'
export const findGlobal: FindGlobal = async function findGlobal(
this: PostgresAdapter,
this: DrizzleAdapter,
{ slug, locale, req, where },
) {
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 toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import type { DrizzleAdapter } from './types.js'
import { findMany } from './find/findMany.js'
export const findGlobalVersions: FindGlobalVersions = async function findGlobalVersions(
this: PostgresAdapter,
this: DrizzleAdapter,
{
global,
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 type { PostgresAdapter } from './types.js'
import type { DrizzleAdapter } from './types.js'
import { findMany } from './find/findMany.js'
export async function findOne<T extends TypeWithID>(
this: PostgresAdapter,
this: DrizzleAdapter,
{ collection, locale, req = {} as PayloadRequest, where }: FindOneArgs,
): Promise<T> {
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 toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import type { DrizzleAdapter } from './types.js'
import { findMany } from './find/findMany.js'
export const findVersions: FindVersions = async function findVersions(
this: PostgresAdapter,
this: DrizzleAdapter,
{
collection,
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 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 { parseError } from './utilities/parseError.js'
const require = createRequire(import.meta.url)
export async function migrate(this: PostgresAdapter): Promise<void> {
export async function migrate(this: DrizzleAdapter): Promise<void> {
const { payload } = this
const migrationFiles = await readMigrationFiles({ payload })
@@ -27,7 +21,7 @@ export async function migrate(this: PostgresAdapter): Promise<void> {
let latestBatch = 0
let migrationsInDB = []
const hasMigrationTable = await migrationTableExists(this.drizzle)
const hasMigrationTable = await migrationTableExists(this)
if (hasMigrationTable) {
;({ docs: migrationsInDB } = await payload.find({
@@ -38,8 +32,6 @@ export async function migrate(this: PostgresAdapter): Promise<void> {
if (Number(migrationsInDB?.[0]?.batch) > 0) {
latestBatch = Number(migrationsInDB[0]?.batch)
}
} else {
await createMigrationTable(this)
}
if (migrationsInDB.find((m) => m.batch === -1)) {
@@ -72,7 +64,7 @@ export async function migrate(this: PostgresAdapter): Promise<void> {
// If already ran, skip
if (alreadyRan) {
continue // eslint-disable-line no-continue
continue
}
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) {
const { generateDrizzleJson } = require('drizzle-kit/payload')
const db = payload.db as DrizzleAdapter
const { generateDrizzleJson } = db.requireDrizzleKit()
const start = Date.now()
const req = { payload } as PayloadRequest
const adapter = payload.db as DrizzleAdapter
payload.logger.info({ msg: `Migrating: ${migration.name}` })
const pgAdapter = payload.db
const drizzleJSON = generateDrizzleJson(pgAdapter.schema)
const drizzleJSON = await generateDrizzleJson({ schema: adapter.schema })
try {
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)` })
await payload.create({
collection: 'payload-migrations',

View File

@@ -9,12 +9,12 @@ import {
readMigrationFiles,
} from 'payload'
import type { PostgresAdapter } from './types.js'
import type { DrizzleAdapter } from './types.js'
import { migrationTableExists } from './utilities/migrationTableExists.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 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)`,
})
const tableExists = await migrationTableExists(this.drizzle)
const tableExists = await migrationTableExists(this)
if (tableExists) {
await payload.delete({
id: migration.id,

View File

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

View File

@@ -9,7 +9,7 @@ import {
readMigrationFiles,
} from 'payload'
import type { PostgresAdapter } from './types.js'
import type { DrizzleAdapter } from './types.js'
import { migrationTableExists } from './utilities/migrationTableExists.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
*/
export async function migrateRefresh(this: PostgresAdapter) {
export async function migrateRefresh(this: DrizzleAdapter) {
const { payload } = this
const migrationFiles = await readMigrationFiles({ payload })
@@ -54,7 +54,7 @@ export async function migrateRefresh(this: PostgresAdapter) {
msg: `Migrated down: ${migration.name} (${Date.now() - start}ms)`,
})
const tableExists = await migrationTableExists(this.drizzle)
const tableExists = await migrationTableExists(this)
if (tableExists) {
await payload.delete({
collection: 'payload-migrations',

View File

@@ -9,14 +9,14 @@ import {
readMigrationFiles,
} from 'payload'
import type { PostgresAdapter } from './types.js'
import type { DrizzleAdapter } from './types.js'
import { migrationTableExists } from './utilities/migrationTableExists.js'
/**
* 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 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)`,
})
const tableExists = await migrationTableExists(this.drizzle)
const tableExists = await migrationTableExists(this)
if (tableExists) {
await payload.delete({
id: migration.id,
@@ -71,7 +71,7 @@ export async function migrateReset(this: PostgresAdapter): Promise<void> {
// Delete dev migration
const tableExists = await migrationTableExists(this.drizzle)
const tableExists = await migrationTableExists(this)
if (tableExists) {
try {
await payload.delete({

View File

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

View File

@@ -1,7 +1,7 @@
import type { SQL } from 'drizzle-orm'
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 { parseParams } from './parseParams.js'
@@ -15,7 +15,7 @@ export async function buildAndOrConditions({
tableName,
where,
}: {
adapter: PostgresAdapter
adapter: DrizzleAdapter
collectionSlug?: string
fields: Field[]
globalSlug?: string

View File

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

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