diff --git a/docs/admin/overview.mdx b/docs/admin/overview.mdx index a84ad55318a..5da2abb3487 100644 --- a/docs/admin/overview.mdx +++ b/docs/admin/overview.mdx @@ -86,20 +86,21 @@ const config = buildConfig({ The following options are available: -| Option | Description | -|---------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| **`avatar`** | Set account profile picture. Options: `gravatar`, `default` or a custom React component. | -| **`autoLogin`** | Used to automate log-in for dev and demonstration convenience. [More details](../authentication/overview). | -| **`buildPath`** | Specify an absolute path for where to store the built Admin bundle used in production. Defaults to `path.resolve(process.cwd(), 'build')`. | -| **`components`** | Component overrides that affect the entirety of the Admin Panel. [More details](./components). | -| **`custom`** | Any custom properties you wish to pass to the Admin Panel. | -| **`dateFormat`** | The date format that will be used for all dates within the Admin Panel. Any valid [date-fns](https://date-fns.org/) format pattern can be used. | -| **`disable`** | If set to `true`, the entire Admin Panel will be disabled. | -| **`livePreview`** | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). | -| **`meta`** | Base metadata to use for the Admin Panel. [More details](./metadata). | -| **`routes`** | Replace built-in Admin Panel routes with your own custom routes. [More details](#customizing-routes). | -| **`theme`** | Restrict the Admin Panel theme to use only one of your choice. Default is `all`. -| **`user`** | The `slug` of the Collection that you want to allow to login to the Admin Panel. [More details](#the-admin-user-collection). | +| Option | Description | +|--------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **`avatar`** | Set account profile picture. Options: `gravatar`, `default` or a custom React component. | +| **`autoLogin`** | Used to automate log-in for dev and demonstration convenience. [More details](../authentication/overview). | +| **`buildPath`** | Specify an absolute path for where to store the built Admin bundle used in production. Defaults to `path.resolve(process.cwd(), 'build')`. | +| **`components`** | Component overrides that affect the entirety of the Admin Panel. [More details](./components). | +| **`custom`** | Any custom properties you wish to pass to the Admin Panel. | +| **`dateFormat`** | The date format that will be used for all dates within the Admin Panel. Any valid [date-fns](https://date-fns.org/) format pattern can be used. | +| **`disable`** | If set to `true`, the entire Admin Panel will be disabled. | +| **`livePreview`** | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). | +| **`meta`** | Base metadata to use for the Admin Panel. [More details](./metadata). | +| **`routes`** | Replace built-in Admin Panel routes with your own custom routes. [More details](#customizing-routes). | +| **`suppressHydrationWarning`** | If set to `true`, suppresses React hydration mismatch warnings during the hydration of the root tag. Defaults to `false`. | +| **`theme`** | Restrict the Admin Panel theme to use only one of your choice. Default is `all`. | +| **`user`** | The `slug` of the Collection that you want to allow to login to the Admin Panel. [More details](#the-admin-user-collection). | Reminder: diff --git a/docs/database/migrations.mdx b/docs/database/migrations.mdx index beb593be4a5..0473f999adb 100644 --- a/docs/database/migrations.mdx +++ b/docs/database/migrations.mdx @@ -57,6 +57,38 @@ you need to do is pass the `req` object to any [local API](/docs/local-api/overv after your `up` or `down` function runs. If the migration errors at any point or fails to commit, it is caught and the transaction gets aborted. This way no change is made to the database if the migration fails. +### Using database directly with the transaction + +Additionally, you can bypass Payload's layer entirely and perform operations directly on your underlying database within the active transaction: + +### MongoDB: +```ts +import { type MigrateUpArgs } from '@payloadcms/db-mongodb' + +export async function up({ session, payload, req }: MigrateUpArgs): Promise { + const posts = await payload.db.collections.posts.collection.find({ session }).toArray() +} +``` + +### Postgres: +```ts +import { type MigrateUpArgs, sql } from '@payloadcms/db-postgres' + +export async function up({ db, payload, req }: MigrateUpArgs): Promise { + const { rows: posts } = await db.execute(sql`SELECT * from posts`) +} +``` + +### SQLite: +In SQLite, transactions are disabled by default. [More](./transactions). +```ts +import { type MigrateUpArgs, sql } from '@payloadcms/db-sqlite' + +export async function up({ db, payload, req }: MigrateUpArgs): Promise { + const { rows: posts } = await db.run(sql`SELECT * from posts`) +} +``` + ## Migrations Directory Each DB adapter has an optional property `migrationDir` where you can override where you want your migrations to be diff --git a/docs/database/transactions.mdx b/docs/database/transactions.mdx index 030f3684495..789a9d531a9 100644 --- a/docs/database/transactions.mdx +++ b/docs/database/transactions.mdx @@ -16,6 +16,12 @@ By default, Payload will use transactions for all data changing operations, as l MongoDB requires a connection to a replicaset in order to make use of transactions. + + Note: +
+ Transactions in SQLite are disabled by default. You need to pass `transactionOptions: {}` to enable them. +
+ The initial request made to Payload will begin a new transaction and attach it to the `req.transactionID`. If you have a `hook` that interacts with the database, you can opt in to using the same transaction by passing the `req` in the arguments. For example: ```ts diff --git a/docs/local-api/overview.mdx b/docs/local-api/overview.mdx index 31fd1071dda..caf3ce6f0bd 100644 --- a/docs/local-api/overview.mdx +++ b/docs/local-api/overview.mdx @@ -131,6 +131,9 @@ const post = await payload.create({ // Alternatively, you can directly pass a File, // if file is provided, filePath will be omitted file: uploadedFile, + + // If you want to create a document that is a duplicate of another document + duplicateFromID: 'document-id-to-duplicate', }) ``` diff --git a/packages/db-mongodb/src/createMigration.ts b/packages/db-mongodb/src/createMigration.ts index c99eee52cac..ae54ba1a683 100644 --- a/packages/db-mongodb/src/createMigration.ts +++ b/packages/db-mongodb/src/createMigration.ts @@ -10,11 +10,11 @@ const migrationTemplate = ({ downSQL, imports, upSQL }: MigrationTemplateArgs): MigrateUpArgs, } from '@payloadcms/db-mongodb' ${imports ?? ''} -export async function up({ payload, req }: MigrateUpArgs): Promise { +export async function up({ payload, req, session }: MigrateUpArgs): Promise { ${upSQL ?? ` // Migration code`} } -export async function down({ payload, req }: MigrateDownArgs): Promise { +export async function down({ payload, req, session }: MigrateDownArgs): Promise { ${downSQL ?? ` // Migration code`} } ` diff --git a/packages/db-mongodb/src/types.ts b/packages/db-mongodb/src/types.ts index 7945f75664a..89c60eab098 100644 --- a/packages/db-mongodb/src/types.ts +++ b/packages/db-mongodb/src/types.ts @@ -1,3 +1,4 @@ +import type { ClientSession } from 'mongodb' import type { AggregatePaginateModel, IndexDefinition, @@ -110,5 +111,65 @@ export type FieldToSchemaMap = { upload: FieldGeneratorFunction } -export type MigrateUpArgs = { payload: Payload; req: PayloadRequest } -export type MigrateDownArgs = { payload: Payload; req: PayloadRequest } +export type MigrateUpArgs = { + /** + * The Payload instance that you can use to execute Local API methods + * To use the current transaction you must pass `req` to arguments + * @example + * ```ts + * import { type MigrateUpArgs } from '@payloadcms/db-mongodb' + * + * export async function up({ session, payload, req }: MigrateUpArgs): Promise { + * const posts = await payload.find({ collection: 'posts', req }) + * } + * ``` + */ + payload: Payload + /** + * The `PayloadRequest` object that contains the current transaction + */ + req: PayloadRequest + /** + * The MongoDB client session that you can use to execute MongoDB methods directly within the current transaction. + * @example + * ```ts + * import { type MigrateUpArgs } from '@payloadcms/db-mongodb' + * + * export async function up({ session, payload, req }: MigrateUpArgs): Promise { + * const { rows: posts } = await payload.db.collections.posts.collection.find({ session }).toArray() + * } + * ``` + */ + session?: ClientSession +} +export type MigrateDownArgs = { + /** + * The Payload instance that you can use to execute Local API methods + * To use the current transaction you must pass `req` to arguments + * @example + * ```ts + * import { type MigrateDownArgs } from '@payloadcms/db-mongodb' + * + * export async function down({ session, payload, req }: MigrateDownArgs): Promise { + * const posts = await payload.find({ collection: 'posts', req }) + * } + * ``` + */ + payload: Payload + /** + * The `PayloadRequest` object that contains the current transaction + */ + req: PayloadRequest + /** + * The MongoDB client session that you can use to execute MongoDB methods directly within the current transaction. + * @example + * ```ts + * import { type MigrateDownArgs } from '@payloadcms/db-mongodb' + * + * export async function down({ session, payload, req }: MigrateDownArgs): Promise { + * const { rows: posts } = await payload.db.collections.posts.collection.find({ session }).toArray() + * } + * ``` + */ + session?: ClientSession +} diff --git a/packages/db-postgres/src/exports/migration-utils.ts b/packages/db-postgres/src/exports/migration-utils.ts index e67c9579f52..f5b74e7fa2b 100644 --- a/packages/db-postgres/src/exports/migration-utils.ts +++ b/packages/db-postgres/src/exports/migration-utils.ts @@ -1 +1 @@ -export { migratePostgresV2toV3 } from '../predefinedMigrations/v2-v3/index.js' +export { migratePostgresV2toV3 } from '@payloadcms/drizzle/postgres' diff --git a/packages/db-postgres/src/index.ts b/packages/db-postgres/src/index.ts index 4048c0be416..fe6e4b2a8b5 100644 --- a/packages/db-postgres/src/index.ts +++ b/packages/db-postgres/src/index.ts @@ -2,6 +2,7 @@ import type { DatabaseAdapterObj, Payload } from 'payload' import { beginTransaction, + buildCreateMigration, commitTransaction, count, countGlobalVersions, @@ -39,18 +40,15 @@ import { createDatabase, createExtensions, createJSONQuery, - createMigration, defaultDrizzleSnapshot, deleteWhere, dropDatabase, execute, - getMigrationTemplate, init, insert, requireDrizzleKit, } from '@payloadcms/drizzle/postgres' import { pgEnum, pgSchema, pgTable } from 'drizzle-orm/pg-core' -import path from 'path' import { createDatabaseAdapter, defaultBeginTransaction } from 'payload' import { fileURLToPath } from 'url' @@ -59,7 +57,6 @@ import type { Args, PostgresAdapter } from './types.js' import { connect } from './connect.js' const filename = fileURLToPath(import.meta.url) -const dirname = path.dirname(filename) export function postgresAdapter(args: Args): DatabaseAdapterObj { const postgresIDType = args.idType || 'serial' @@ -93,9 +90,13 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj beforeSchemaInit: args.beforeSchemaInit ?? [], createDatabase, createExtensions, - createMigration(args) { - return createMigration.bind(this)({ ...args, dirname }) - }, + createMigration: buildCreateMigration({ + executeMethod: 'execute', + filename, + sanitizeStatements({ sqlExecute, statements }) { + return `${sqlExecute}\n ${statements.join('\n')}\`)` + }, + }), defaultDrizzleSnapshot, disableCreateDatabase: args.disableCreateDatabase ?? false, drizzle: undefined, @@ -105,7 +106,6 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj json: true, }, fieldConstraints: {}, - getMigrationTemplate, idType: postgresIDType, initializing, localesSuffix: args.localesSuffix || '_locales', diff --git a/packages/db-postgres/src/predefinedMigrations/v2-v3/index.ts b/packages/db-postgres/src/predefinedMigrations/v2-v3/index.ts deleted file mode 100644 index 9b0ff6b8f34..00000000000 --- a/packages/db-postgres/src/predefinedMigrations/v2-v3/index.ts +++ /dev/null @@ -1,282 +0,0 @@ -import type { TransactionPg } from '@payloadcms/drizzle/types' -import type { DrizzleSnapshotJSON } from 'drizzle-kit/api' -import type { Payload, PayloadRequest } from 'payload' - -import { sql } from 'drizzle-orm' -import fs from 'fs' -import { createRequire } from 'module' -import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload' -import toSnakeCase from 'to-snake-case' - -import type { PostgresAdapter } from '../../types.js' -import type { PathsToQuery } from './types.js' - -import { groupUpSQLStatements } from './groupUpSQLStatements.js' -import { migrateRelationships } from './migrateRelationships.js' -import { traverseFields } from './traverseFields.js' - -const require = createRequire(import.meta.url) - -type Args = { - debug?: boolean - payload: Payload - req?: Partial -} - -/** - * Moves upload and relationship columns from the join table and into the tables while moving data - * This is done in the following order: - * ADD COLUMNs - * -- manipulate data to move relationships to new columns - * ADD CONSTRAINTs - * NOT NULLs - * DROP TABLEs - * DROP CONSTRAINTs - * DROP COLUMNs - * @param debug - * @param payload - * @param req - */ -export const migratePostgresV2toV3 = async ({ debug, payload, req }: Args) => { - 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 - const { generateDrizzleJson, generateMigration, upPgSnapshot } = require('drizzle-kit/api') - const drizzleJsonAfter = generateDrizzleJson(adapter.schema) - - // Get the previous migration snapshot - const previousSnapshot = fs - .readdirSync(dir) - .filter((file) => file.endsWith('.json') && !file.endsWith('relationships_v2_v3.json')) - .sort() - .reverse()?.[0] - - if (!previousSnapshot) { - throw new Error( - `No previous migration schema file found! A prior migration from v2 is required to migrate to v3.`, - ) - } - - let drizzleJsonBefore = JSON.parse( - fs.readFileSync(`${dir}/${previousSnapshot}`, 'utf8'), - ) as DrizzleSnapshotJSON - - if (drizzleJsonBefore.version < drizzleJsonAfter.version) { - drizzleJsonBefore = upPgSnapshot(drizzleJsonBefore) - } - - const generatedSQL = await generateMigration(drizzleJsonBefore, drizzleJsonAfter) - - if (!generatedSQL.length) { - payload.logger.info(`No schema changes needed.`) - process.exit(0) - } - - const sqlUpStatements = groupUpSQLStatements(generatedSQL) - - const addColumnsStatement = sqlUpStatements.addColumn.join('\n') - - if (debug) { - payload.logger.info('CREATING NEW RELATIONSHIP COLUMNS') - payload.logger.info(addColumnsStatement) - } - - await db.execute(sql.raw(addColumnsStatement)) - - for (const collection of payload.config.collections) { - const tableName = adapter.tableNameMap.get(toSnakeCase(collection.slug)) - const pathsToQuery: PathsToQuery = new Set() - - traverseFields({ - adapter, - collectionSlug: collection.slug, - columnPrefix: '', - db, - disableNotNull: false, - fields: collection.flattenedFields, - isVersions: false, - newTableName: tableName, - parentTableName: tableName, - path: '', - pathsToQuery, - payload, - rootTableName: tableName, - }) - - await migrateRelationships({ - adapter, - collectionSlug: collection.slug, - db, - debug, - fields: collection.flattenedFields, - isVersions: false, - pathsToQuery, - payload, - req, - tableName, - }) - - if (collection.versions) { - const versionsTableName = adapter.tableNameMap.get( - `_${toSnakeCase(collection.slug)}${adapter.versionsSuffix}`, - ) - const versionFields = buildVersionCollectionFields(payload.config, collection, true) - const versionPathsToQuery: PathsToQuery = new Set() - - traverseFields({ - adapter, - collectionSlug: collection.slug, - columnPrefix: '', - db, - disableNotNull: true, - fields: versionFields, - isVersions: true, - newTableName: versionsTableName, - parentTableName: versionsTableName, - path: '', - pathsToQuery: versionPathsToQuery, - payload, - rootTableName: versionsTableName, - }) - - await migrateRelationships({ - adapter, - collectionSlug: collection.slug, - db, - debug, - fields: versionFields, - isVersions: true, - pathsToQuery: versionPathsToQuery, - payload, - req, - tableName: versionsTableName, - }) - } - } - - for (const global of payload.config.globals) { - const tableName = adapter.tableNameMap.get(toSnakeCase(global.slug)) - - const pathsToQuery: PathsToQuery = new Set() - - traverseFields({ - adapter, - columnPrefix: '', - db, - disableNotNull: false, - fields: global.flattenedFields, - globalSlug: global.slug, - isVersions: false, - newTableName: tableName, - parentTableName: tableName, - path: '', - pathsToQuery, - payload, - rootTableName: tableName, - }) - - await migrateRelationships({ - adapter, - db, - debug, - fields: global.flattenedFields, - globalSlug: global.slug, - isVersions: false, - pathsToQuery, - payload, - req, - tableName, - }) - - if (global.versions) { - const versionsTableName = adapter.tableNameMap.get( - `_${toSnakeCase(global.slug)}${adapter.versionsSuffix}`, - ) - - const versionFields = buildVersionGlobalFields(payload.config, global, true) - - const versionPathsToQuery: PathsToQuery = new Set() - - traverseFields({ - adapter, - columnPrefix: '', - db, - disableNotNull: true, - fields: versionFields, - globalSlug: global.slug, - isVersions: true, - newTableName: versionsTableName, - parentTableName: versionsTableName, - path: '', - pathsToQuery: versionPathsToQuery, - payload, - rootTableName: versionsTableName, - }) - - await migrateRelationships({ - adapter, - db, - debug, - fields: versionFields, - globalSlug: global.slug, - isVersions: true, - pathsToQuery: versionPathsToQuery, - payload, - req, - tableName: versionsTableName, - }) - } - } - - // ADD CONSTRAINT - const addConstraintsStatement = sqlUpStatements.addConstraint.join('\n') - - if (debug) { - payload.logger.info('ADDING CONSTRAINTS') - payload.logger.info(addConstraintsStatement) - } - - await db.execute(sql.raw(addConstraintsStatement)) - - // NOT NULL - const notNullStatements = sqlUpStatements.notNull.join('\n') - - if (debug) { - payload.logger.info('NOT NULL CONSTRAINTS') - payload.logger.info(notNullStatements) - } - - await db.execute(sql.raw(notNullStatements)) - - // DROP TABLE - const dropTablesStatement = sqlUpStatements.dropTable.join('\n') - - if (debug) { - payload.logger.info('DROPPING TABLES') - payload.logger.info(dropTablesStatement) - } - - await db.execute(sql.raw(dropTablesStatement)) - - // DROP CONSTRAINT - const dropConstraintsStatement = sqlUpStatements.dropConstraint.join('\n') - - if (debug) { - payload.logger.info('DROPPING CONSTRAINTS') - payload.logger.info(dropConstraintsStatement) - } - - await db.execute(sql.raw(dropConstraintsStatement)) - - // DROP COLUMN - const dropColumnsStatement = sqlUpStatements.dropColumn.join('\n') - - if (debug) { - payload.logger.info('DROPPING COLUMNS') - payload.logger.info(dropColumnsStatement) - } - - await db.execute(sql.raw(dropColumnsStatement)) -} diff --git a/packages/db-sqlite/src/createMigration.ts b/packages/db-sqlite/src/createMigration.ts deleted file mode 100644 index e9c0505d371..00000000000 --- a/packages/db-sqlite/src/createMigration.ts +++ /dev/null @@ -1,123 +0,0 @@ -import type { DrizzleSnapshotJSON } from 'drizzle-kit/api' -import type { CreateMigration } from 'payload' - -import fs from 'fs' -import { createRequire } from 'module' -import path from 'path' -import { getPredefinedMigration, writeMigrationIndex } 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, migrationName, payload, skipEmpty }, -) { - 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/api') - 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 payload.db.drizzle.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) { - if (skipEmpty) { - process.exit(0) - } - - 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`, - }), - ) - - writeMigrationIndex({ migrationsDir: payload.db.migrationDir }) - - payload.logger.info({ msg: `Migration created at ${filePath}.ts` }) -} diff --git a/packages/db-sqlite/src/getMigrationTemplate.ts b/packages/db-sqlite/src/getMigrationTemplate.ts deleted file mode 100644 index 5b79b6bfdbf..00000000000 --- a/packages/db-sqlite/src/getMigrationTemplate.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { MigrationTemplateArgs } from 'payload' - -export const indent = (text: string) => - text - .split('\n') - .map((line) => ` ${line}`) - .join('\n') - -export const getMigrationTemplate = ({ - downSQL, - imports, - upSQL, -}: MigrationTemplateArgs): string => `import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-sqlite' -${imports ? `${imports}\n` : ''} -export async function up({ payload, req }: MigrateUpArgs): Promise { -${indent(upSQL)} -} - -export async function down({ payload, req }: MigrateDownArgs): Promise { -${indent(downSQL)} -} -` diff --git a/packages/db-sqlite/src/index.ts b/packages/db-sqlite/src/index.ts index 320a2326e2a..4259d0c8e28 100644 --- a/packages/db-sqlite/src/index.ts +++ b/packages/db-sqlite/src/index.ts @@ -3,6 +3,7 @@ import type { DatabaseAdapterObj, Payload } from 'payload' import { beginTransaction, + buildCreateMigration, commitTransaction, count, countGlobalVersions, @@ -37,6 +38,7 @@ import { } from '@payloadcms/drizzle' import { like } from 'drizzle-orm' import { createDatabaseAdapter, defaultBeginTransaction } from 'payload' +import { fileURLToPath } from 'url' import type { Args, SQLiteAdapter } from './types.js' @@ -44,12 +46,10 @@ 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' @@ -58,6 +58,8 @@ export type { MigrateDownArgs, MigrateUpArgs } from './types.js' export { sql } from 'drizzle-orm' +const filename = fileURLToPath(import.meta.url) + export function sqliteAdapter(args: Args): DatabaseAdapterObj { const postgresIDType = args.idType || 'serial' const payloadIDType = postgresIDType === 'serial' ? 'number' : 'text' @@ -91,7 +93,6 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj { json: true, }, fieldConstraints: {}, - getMigrationTemplate, idType: postgresIDType, initializing, localesSuffix: args.localesSuffix || '_locales', @@ -122,7 +123,15 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj { createGlobal, createGlobalVersion, createJSONQuery, - createMigration, + createMigration: buildCreateMigration({ + executeMethod: 'run', + filename, + sanitizeStatements({ sqlExecute, statements }) { + return statements + .map((statement) => `${sqlExecute}${statement?.replaceAll('`', '\\`')}\`)`) + .join('\n') + }, + }), createVersion, defaultIDType: payloadIDType, deleteMany, diff --git a/packages/db-sqlite/src/requireDrizzleKit.ts b/packages/db-sqlite/src/requireDrizzleKit.ts index e143382f1bc..800ef9fcae4 100644 --- a/packages/db-sqlite/src/requireDrizzleKit.ts +++ b/packages/db-sqlite/src/requireDrizzleKit.ts @@ -1,15 +1,19 @@ 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, + generateSQLiteDrizzleJson, + generateSQLiteMigration, + pushSQLiteSchema, } = require('drizzle-kit/api') - return { generateDrizzleJson, pushSchema } + + return { + generateDrizzleJson: generateSQLiteDrizzleJson, + generateMigration: generateSQLiteMigration, + pushSchema: pushSQLiteSchema, + } } diff --git a/packages/db-sqlite/src/types.ts b/packages/db-sqlite/src/types.ts index 53e4fe9c228..6e07a7756cc 100644 --- a/packages/db-sqlite/src/types.ts +++ b/packages/db-sqlite/src/types.ts @@ -154,11 +154,65 @@ export type SQLiteAdapter = { export type IDType = 'integer' | 'numeric' | 'text' export type MigrateUpArgs = { + /** + * The SQLite Drizzle instance that you can use to execute SQL directly within the current transaction. + * @example + * ```ts + * import { type MigrateUpArgs, sql } from '@payloadcms/db-sqlite' + * + * export async function up({ db, payload, req }: MigrateUpArgs): Promise { + * const { rows: posts } = await db.run(sql`SELECT * FROM posts`) + * } + * ``` + */ + db: LibSQLDatabase + /** + * The Payload instance that you can use to execute Local API methods + * To use the current transaction you must pass `req` to arguments + * @example + * ```ts + * import { type MigrateUpArgs } from '@payloadcms/db-sqlite' + * + * export async function up({ db, payload, req }: MigrateUpArgs): Promise { + * const posts = await payload.find({ collection: 'posts', req }) + * } + * ``` + */ payload: Payload + /** + * The `PayloadRequest` object that contains the current transaction + */ req: PayloadRequest } export type MigrateDownArgs = { + /** + * The SQLite Drizzle instance that you can use to execute SQL directly within the current transaction. + * @example + * ```ts + * import { type MigrateDownArgs, sql } from '@payloadcms/db-sqlite' + * + * export async function down({ db, payload, req }: MigrateDownArgs): Promise { + * const { rows: posts } = await db.run(sql`SELECT * FROM posts`) + * } + * ``` + */ + db: LibSQLDatabase + /** + * The Payload instance that you can use to execute Local API methods + * To use the current transaction you must pass `req` to arguments + * @example + * ```ts + * import { type MigrateDownArgs } from '@payloadcms/db-sqlite' + * + * export async function down({ db, payload, req }: MigrateDownArgs): Promise { + * const posts = await payload.find({ collection: 'posts', req }) + * } + * ``` + */ payload: Payload + /** + * The `PayloadRequest` object that contains the current transaction + */ req: PayloadRequest } diff --git a/packages/db-vercel-postgres/src/exports/migration-utils.ts b/packages/db-vercel-postgres/src/exports/migration-utils.ts index e67c9579f52..f5b74e7fa2b 100644 --- a/packages/db-vercel-postgres/src/exports/migration-utils.ts +++ b/packages/db-vercel-postgres/src/exports/migration-utils.ts @@ -1 +1 @@ -export { migratePostgresV2toV3 } from '../predefinedMigrations/v2-v3/index.js' +export { migratePostgresV2toV3 } from '@payloadcms/drizzle/postgres' diff --git a/packages/db-vercel-postgres/src/index.ts b/packages/db-vercel-postgres/src/index.ts index 0aad626561b..e735190932b 100644 --- a/packages/db-vercel-postgres/src/index.ts +++ b/packages/db-vercel-postgres/src/index.ts @@ -2,6 +2,7 @@ import type { DatabaseAdapterObj, Payload } from 'payload' import { beginTransaction, + buildCreateMigration, commitTransaction, count, countGlobalVersions, @@ -39,18 +40,15 @@ import { createDatabase, createExtensions, createJSONQuery, - createMigration, defaultDrizzleSnapshot, deleteWhere, dropDatabase, execute, - getMigrationTemplate, init, insert, requireDrizzleKit, } from '@payloadcms/drizzle/postgres' import { pgEnum, pgSchema, pgTable } from 'drizzle-orm/pg-core' -import path from 'path' import { createDatabaseAdapter, defaultBeginTransaction } from 'payload' import { fileURLToPath } from 'url' @@ -59,7 +57,6 @@ import type { Args, VercelPostgresAdapter } from './types.js' import { connect } from './connect.js' const filename = fileURLToPath(import.meta.url) -const dirname = path.dirname(filename) export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj { const postgresIDType = args.idType || 'serial' @@ -102,7 +99,6 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj(), initializing, @@ -138,9 +134,13 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj { - for (const [id, rows] of Object.entries(docsToResave)) { - if (collectionSlug) { - const collectionConfig = payload.collections[collectionSlug].config - - if (collectionConfig) { - if (isVersions) { - const doc = await payload.findVersionByID({ - id, - collection: collectionSlug, - depth: 0, - fallbackLocale: null, - locale: 'all', - req, - showHiddenFields: true, - }) - - if (debug) { - payload.logger.info( - `The collection "${collectionConfig.slug}" version with ID ${id} will be migrated`, - ) - } - - traverseFields({ - doc, - fields, - path: '', - rows, - }) - - try { - await upsertRow({ - id: doc.id, - adapter, - data: doc, - db, - fields, - ignoreResult: true, - operation: 'update', - req, - tableName, - }) - } catch (err) { - payload.logger.error( - `"${collectionConfig.slug}" version with ID ${doc.id} FAILED TO MIGRATE`, - ) - - throw err - } - - if (debug) { - payload.logger.info( - `"${collectionConfig.slug}" version with ID ${doc.id} migrated successfully!`, - ) - } - } else { - const doc = await payload.findByID({ - id, - collection: collectionSlug, - depth: 0, - fallbackLocale: null, - locale: 'all', - req, - showHiddenFields: true, - }) - - if (debug) { - payload.logger.info( - `The collection "${collectionConfig.slug}" with ID ${doc.id} will be migrated`, - ) - } - - traverseFields({ - doc, - fields, - path: '', - rows, - }) - - try { - await upsertRow({ - id: doc.id, - adapter, - data: doc, - db, - fields, - ignoreResult: true, - operation: 'update', - req, - tableName, - }) - } catch (err) { - payload.logger.error( - `The collection "${collectionConfig.slug}" with ID ${doc.id} has FAILED TO MIGRATE`, - ) - - throw err - } - - if (debug) { - payload.logger.info( - `The collection "${collectionConfig.slug}" with ID ${doc.id} has migrated successfully!`, - ) - } - } - } - } - - if (globalSlug) { - const globalConfig = payload.config.globals?.find((global) => global.slug === globalSlug) - - if (globalConfig) { - if (isVersions) { - const { docs } = await payload.findGlobalVersions({ - slug: globalSlug, - depth: 0, - fallbackLocale: null, - limit: 0, - locale: 'all', - req, - showHiddenFields: true, - }) - - if (debug) { - payload.logger.info(`${docs.length} global "${globalSlug}" versions will be migrated`) - } - - for (const doc of docs) { - traverseFields({ - doc, - fields, - path: '', - rows, - }) - - try { - await upsertRow({ - id: doc.id, - adapter, - data: doc, - db, - fields, - ignoreResult: true, - operation: 'update', - req, - tableName, - }) - } catch (err) { - payload.logger.error(`"${globalSlug}" version with ID ${doc.id} FAILED TO MIGRATE`) - - throw err - } - - if (debug) { - payload.logger.info( - `"${globalSlug}" version with ID ${doc.id} migrated successfully!`, - ) - } - } - } else { - const doc = await payload.findGlobal({ - slug: globalSlug, - depth: 0, - fallbackLocale: null, - locale: 'all', - req, - showHiddenFields: true, - }) - - traverseFields({ - doc, - fields, - path: '', - rows, - }) - - try { - await upsertRow({ - adapter, - data: doc, - db, - fields, - ignoreResult: true, - operation: 'update', - req, - tableName, - }) - } catch (err) { - payload.logger.error(`The global "${globalSlug}" has FAILED TO MIGRATE`) - - throw err - } - - if (debug) { - payload.logger.info(`The global "${globalSlug}" has migrated successfully!`) - } - } - } - } - } -} diff --git a/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/fetchAndResave/traverseFields.ts b/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/fetchAndResave/traverseFields.ts deleted file mode 100644 index 8304c659be7..00000000000 --- a/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/fetchAndResave/traverseFields.ts +++ /dev/null @@ -1,171 +0,0 @@ -import type { FlattenedField } from 'payload' - -type Args = { - doc: Record - fields: FlattenedField[] - locale?: string - path: string - rows: Record[] -} - -export const traverseFields = ({ doc, fields, locale, path, rows }: Args) => { - fields.forEach((field) => { - switch (field.type) { - case 'array': { - const rowData = doc?.[field.name] - - if (field.localized && typeof rowData === 'object' && rowData !== null) { - Object.entries(rowData).forEach(([locale, localeRows]) => { - if (Array.isArray(localeRows)) { - localeRows.forEach((row, i) => { - return traverseFields({ - doc: row as Record, - fields: field.flattenedFields, - locale, - path: `${path ? `${path}.` : ''}${field.name}.${i}`, - rows, - }) - }) - } - }) - } - - if (Array.isArray(rowData)) { - rowData.forEach((row, i) => { - return traverseFields({ - doc: row as Record, - fields: field.flattenedFields, - path: `${path ? `${path}.` : ''}${field.name}.${i}`, - rows, - }) - }) - } - - break - } - - case 'blocks': { - const rowData = doc?.[field.name] - - if (field.localized && typeof rowData === 'object' && rowData !== null) { - Object.entries(rowData).forEach(([locale, localeRows]) => { - if (Array.isArray(localeRows)) { - localeRows.forEach((row, i) => { - const matchedBlock = field.blocks.find((block) => block.slug === row.blockType) - - if (matchedBlock) { - return traverseFields({ - doc: row as Record, - fields: matchedBlock.flattenedFields, - locale, - path: `${path ? `${path}.` : ''}${field.name}.${i}`, - rows, - }) - } - }) - } - }) - } - - if (Array.isArray(rowData)) { - rowData.forEach((row, i) => { - const matchedBlock = field.blocks.find((block) => block.slug === row.blockType) - - if (matchedBlock) { - return traverseFields({ - doc: row as Record, - fields: matchedBlock.flattenedFields, - path: `${path ? `${path}.` : ''}${field.name}.${i}`, - rows, - }) - } - }) - } - - break - } - - case 'group': - case 'tab': { - const newPath = `${path ? `${path}.` : ''}${field.name}` - const newDoc = doc?.[field.name] - - if (typeof newDoc === 'object' && newDoc !== null) { - if (field.localized) { - Object.entries(newDoc).forEach(([locale, localeDoc]) => { - return traverseFields({ - doc: localeDoc, - fields: field.flattenedFields, - locale, - path: newPath, - rows, - }) - }) - } else { - return traverseFields({ - doc: newDoc as Record, - fields: field.flattenedFields, - path: newPath, - rows, - }) - } - } - - break - } - - case 'relationship': - // falls through - case 'upload': { - if (typeof field.relationTo === 'string') { - if (field.type === 'upload' || !field.hasMany) { - const relationshipPath = `${path ? `${path}.` : ''}${field.name}` - - if (field.localized) { - const matchedRelationshipsWithLocales = rows.filter( - (row) => row.path === relationshipPath, - ) - - if (matchedRelationshipsWithLocales.length && !doc[field.name]) { - doc[field.name] = {} - } - - const newDoc = doc[field.name] as Record - - matchedRelationshipsWithLocales.forEach((localeRow) => { - if (typeof localeRow.locale === 'string') { - const [, id] = Object.entries(localeRow).find( - ([key, val]) => - val !== null && !['id', 'locale', 'order', 'parent_id', 'path'].includes(key), - ) - - newDoc[localeRow.locale] = id - } - }) - } else { - const matchedRelationship = rows.find((row) => { - const matchesPath = row.path === relationshipPath - - if (locale) { - return matchesPath && locale === row.locale - } - - return row.path === relationshipPath - }) - - if (matchedRelationship) { - const [, id] = Object.entries(matchedRelationship).find( - ([key, val]) => - val !== null && !['id', 'locale', 'order', 'parent_id', 'path'].includes(key), - ) - - doc[field.name] = id - } - } - } - } - break - } - } - }) -} diff --git a/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/groupUpSQLStatements.ts b/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/groupUpSQLStatements.ts deleted file mode 100644 index f7ec0045d37..00000000000 --- a/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/groupUpSQLStatements.ts +++ /dev/null @@ -1,74 +0,0 @@ -export type Groups = - | 'addColumn' - | 'addConstraint' - | 'dropColumn' - | 'dropConstraint' - | 'dropTable' - | 'notNull' - -/** - * Convert an "ADD COLUMN" statement to an "ALTER COLUMN" statement - * example: ALTER TABLE "pages_blocks_my_block" ADD COLUMN "person_id" integer NOT NULL; - * to: ALTER TABLE "pages_blocks_my_block" ALTER COLUMN "person_id" SET NOT NULL; - * @param sql - */ -function convertAddColumnToAlterColumn(sql) { - // Regular expression to match the ADD COLUMN statement with its constraints - const regex = /ALTER TABLE ("[^"]+") ADD COLUMN ("[^"]+") [\w\s]+ NOT NULL;/ - - // Replace the matched part with "ALTER COLUMN ... SET NOT NULL;" - return sql.replace(regex, 'ALTER TABLE $1 ALTER COLUMN $2 SET NOT NULL;') -} - -export const groupUpSQLStatements = (list: string[]): Record => { - const groups = { - addColumn: 'ADD COLUMN', - // example: ALTER TABLE "posts" ADD COLUMN "category_id" integer - - addConstraint: 'ADD CONSTRAINT', - //example: - // DO $$ BEGIN - // ALTER TABLE "pages_blocks_my_block" ADD CONSTRAINT "pages_blocks_my_block_person_id_users_id_fk" FOREIGN KEY ("person_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action; - // EXCEPTION - // WHEN duplicate_object THEN null; - // END $$; - - dropColumn: 'DROP COLUMN', - // example: ALTER TABLE "_posts_v_rels" DROP COLUMN IF EXISTS "posts_id"; - - dropConstraint: 'DROP CONSTRAINT', - // example: ALTER TABLE "_posts_v_rels" DROP CONSTRAINT "_posts_v_rels_posts_fk"; - - dropTable: 'DROP TABLE', - // example: DROP TABLE "pages_rels"; - - notNull: 'NOT NULL', - // example: ALTER TABLE "pages_blocks_my_block" ALTER COLUMN "person_id" SET NOT NULL; - } - - const result = Object.keys(groups).reduce((result, group: Groups) => { - result[group] = [] - return result - }, {}) as Record - - for (const line of list) { - Object.entries(groups).some(([key, value]) => { - if (line.endsWith('NOT NULL;')) { - // split up the ADD COLUMN and ALTER COLUMN NOT NULL statements - // example: ALTER TABLE "pages_blocks_my_block" ADD COLUMN "person_id" integer NOT NULL; - // becomes two separate statements: - // 1. ALTER TABLE "pages_blocks_my_block" ADD COLUMN "person_id" integer; - // 2. ALTER TABLE "pages_blocks_my_block" ALTER COLUMN "person_id" SET NOT NULL; - result.addColumn.push(line.replace(' NOT NULL;', ';')) - result.notNull.push(convertAddColumnToAlterColumn(line)) - return true - } - if (line.includes(value)) { - result[key].push(line) - return true - } - }) - } - - return result -} diff --git a/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/migrateRelationships.ts b/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/migrateRelationships.ts deleted file mode 100644 index fd2b5397cd5..00000000000 --- a/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/migrateRelationships.ts +++ /dev/null @@ -1,109 +0,0 @@ -import type { TransactionPg } from '@payloadcms/drizzle/types' -import type { FlattenedField, Payload, PayloadRequest } from 'payload' - -import { sql } from 'drizzle-orm' - -import type { VercelPostgresAdapter } from '../../types.js' -import type { DocsToResave, PathsToQuery } from './types.js' - -import { fetchAndResave } from './fetchAndResave/index.js' - -type Args = { - adapter: VercelPostgresAdapter - collectionSlug?: string - db: TransactionPg - debug: boolean - fields: FlattenedField[] - globalSlug?: string - isVersions: boolean - pathsToQuery: PathsToQuery - payload: Payload - req?: Partial - tableName: string -} - -export const migrateRelationships = async ({ - adapter, - collectionSlug, - db, - debug, - fields, - globalSlug, - isVersions, - pathsToQuery, - payload, - req, - tableName, -}: Args) => { - if (pathsToQuery.size === 0) { - return - } - - let offset = 0 - - let paginationResult - - const where = Array.from(pathsToQuery).reduce((statement, path, i) => { - return (statement += ` -"${tableName}${adapter.relationshipsSuffix}"."path" LIKE '${path}'${pathsToQuery.size !== i + 1 ? ' OR' : ''} -`) - }, '') - - while (typeof paginationResult === 'undefined' || paginationResult.rows.length > 0) { - const paginationStatement = `SELECT DISTINCT parent_id FROM ${tableName}${adapter.relationshipsSuffix} WHERE - ${where} ORDER BY parent_id LIMIT 500 OFFSET ${offset * 500}; - ` - - paginationResult = await adapter.drizzle.execute(sql.raw(`${paginationStatement}`)) - - if (paginationResult.rows.length === 0) { - return - } - - offset += 1 - - const statement = `SELECT * FROM ${tableName}${adapter.relationshipsSuffix} WHERE - (${where}) AND parent_id IN (${paginationResult.rows.map((row) => row.parent_id).join(', ')}); -` - if (debug) { - payload.logger.info('FINDING ROWS TO MIGRATE') - payload.logger.info(statement) - } - - const result = await adapter.drizzle.execute(sql.raw(`${statement}`)) - - const docsToResave: DocsToResave = {} - - result.rows.forEach((row) => { - const parentID = row.parent_id - - if (typeof parentID === 'string' || typeof parentID === 'number') { - if (!docsToResave[parentID]) { - docsToResave[parentID] = [] - } - docsToResave[parentID].push(row) - } - }) - - await fetchAndResave({ - adapter, - collectionSlug, - db, - debug, - docsToResave, - fields, - globalSlug, - isVersions, - payload, - req: req as unknown as PayloadRequest, - tableName, - }) - } - - const deleteStatement = `DELETE FROM ${tableName}${adapter.relationshipsSuffix} WHERE ${where}` - if (debug) { - payload.logger.info('DELETING ROWS') - payload.logger.info(deleteStatement) - } - await db.execute(sql.raw(`${deleteStatement}`)) -} diff --git a/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/traverseFields.ts b/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/traverseFields.ts deleted file mode 100644 index cc16ee906b5..00000000000 --- a/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/traverseFields.ts +++ /dev/null @@ -1,90 +0,0 @@ -import type { TransactionPg } from '@payloadcms/drizzle/types' -import type { FlattenedField, Payload } from 'payload' - -import toSnakeCase from 'to-snake-case' - -import type { VercelPostgresAdapter } from '../../types.js' -import type { PathsToQuery } from './types.js' - -type Args = { - adapter: VercelPostgresAdapter - collectionSlug?: string - columnPrefix: string - db: TransactionPg - disableNotNull: boolean - fields: FlattenedField[] - globalSlug?: string - isVersions: boolean - newTableName: string - parentTableName: string - path: string - pathsToQuery: PathsToQuery - payload: Payload - rootTableName: string -} - -export const traverseFields = (args: Args) => { - args.fields.forEach((field) => { - switch (field.type) { - case 'array': { - const newTableName = args.adapter.tableNameMap.get( - `${args.newTableName}_${toSnakeCase(field.name)}`, - ) - - return traverseFields({ - ...args, - columnPrefix: '', - fields: field.flattenedFields, - newTableName, - parentTableName: newTableName, - path: `${args.path ? `${args.path}.` : ''}${field.name}.%`, - }) - } - - case 'blocks': { - return field.blocks.forEach((block) => { - const newTableName = args.adapter.tableNameMap.get( - `${args.rootTableName}_blocks_${toSnakeCase(block.slug)}`, - ) - - traverseFields({ - ...args, - columnPrefix: '', - fields: block.flattenedFields, - newTableName, - parentTableName: newTableName, - path: `${args.path ? `${args.path}.` : ''}${field.name}.%`, - }) - }) - } - - case 'group': - case 'tab': { - let newTableName = `${args.newTableName}_${toSnakeCase(field.name)}` - - if (field.localized && args.payload.config.localization) { - newTableName += args.adapter.localesSuffix - } - - return traverseFields({ - ...args, - columnPrefix: `${args.columnPrefix}${toSnakeCase(field.name)}_`, - fields: field.flattenedFields, - newTableName, - path: `${args.path ? `${args.path}.` : ''}${field.name}`, - }) - } - - case 'relationship': - case 'upload': { - if (typeof field.relationTo === 'string') { - if (field.type === 'upload' || !field.hasMany) { - args.pathsToQuery.add(`${args.path ? `${args.path}.` : ''}${field.name}`) - } - } - - return null - } - } - }) -} diff --git a/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/types.ts b/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/types.ts deleted file mode 100644 index 8980e64b940..00000000000 --- a/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Set of all paths which should be moved - * This will be built up into one WHERE query - */ -export type PathsToQuery = Set - -export type DocsToResave = { - [id: number | string]: Record[] -} diff --git a/packages/drizzle/src/exports/postgres.ts b/packages/drizzle/src/exports/postgres.ts index 27ccc92da91..bf63a1868cf 100644 --- a/packages/drizzle/src/exports/postgres.ts +++ b/packages/drizzle/src/exports/postgres.ts @@ -2,13 +2,12 @@ export { countDistinct } from '../postgres/countDistinct.js' export { createDatabase } from '../postgres/createDatabase.js' export { createExtensions } from '../postgres/createExtensions.js' export { createJSONQuery } from '../postgres/createJSONQuery/index.js' -export { createMigration } from '../postgres/createMigration.js' export { defaultDrizzleSnapshot } from '../postgres/defaultSnapshot.js' export { deleteWhere } from '../postgres/deleteWhere.js' export { dropDatabase } from '../postgres/dropDatabase.js' export { execute } from '../postgres/execute.js' -export { getMigrationTemplate } from '../postgres/getMigrationTemplate.js' export { init } from '../postgres/init.js' export { insert } from '../postgres/insert.js' +export { migratePostgresV2toV3 } from '../postgres/predefinedMigrations/v2-v3/index.js' export { requireDrizzleKit } from '../postgres/requireDrizzleKit.js' export * from '../postgres/types.js' diff --git a/packages/drizzle/src/index.ts b/packages/drizzle/src/index.ts index 6c72222f707..3e09527fd39 100644 --- a/packages/drizzle/src/index.ts +++ b/packages/drizzle/src/index.ts @@ -34,6 +34,7 @@ export { updateGlobal } from './updateGlobal.js' export { updateGlobalVersion } from './updateGlobalVersion.js' export { updateVersion } from './updateVersion.js' export { upsertRow } from './upsertRow/index.js' +export { buildCreateMigration } from './utilities/buildCreateMigration.js' export { buildIndexName } from './utilities/buildIndexName.js' export { executeSchemaHooks } from './utilities/executeSchemaHooks.js' export { extendDrizzleTable } from './utilities/extendDrizzleTable.js' diff --git a/packages/drizzle/src/migrateDown.ts b/packages/drizzle/src/migrateDown.ts index 4297bd99299..5673e7c92b5 100644 --- a/packages/drizzle/src/migrateDown.ts +++ b/packages/drizzle/src/migrateDown.ts @@ -44,7 +44,8 @@ export async function migrateDown(this: DrizzleAdapter): Promise { try { payload.logger.info({ msg: `Migrating down: ${migrationFile.name}` }) await initTransaction(req) - await migrationFile.down({ payload, req }) + const db = this.sessions[await req.transactionID]?.db || this.drizzle + await migrationFile.down({ db, payload, req }) payload.logger.info({ msg: `Migrated down: ${migrationFile.name} (${Date.now() - start}ms)`, }) diff --git a/packages/drizzle/src/migrateFresh.ts b/packages/drizzle/src/migrateFresh.ts index 8aea223782c..6583098644a 100644 --- a/packages/drizzle/src/migrateFresh.ts +++ b/packages/drizzle/src/migrateFresh.ts @@ -59,8 +59,7 @@ export async function migrateFresh( try { const start = Date.now() await initTransaction(req) - const adapter = payload.db as DrizzleAdapter - const db = adapter?.sessions[await req.transactionID]?.db || adapter.drizzle + const db = this.sessions[await req.transactionID]?.db || this.drizzle await migration.up({ db, payload, req }) await payload.create({ collection: 'payload-migrations', diff --git a/packages/drizzle/src/migrateRefresh.ts b/packages/drizzle/src/migrateRefresh.ts index badcb6686e5..fde077dbb8b 100644 --- a/packages/drizzle/src/migrateRefresh.ts +++ b/packages/drizzle/src/migrateRefresh.ts @@ -48,7 +48,8 @@ export async function migrateRefresh(this: DrizzleAdapter) { payload.logger.info({ msg: `Migrating down: ${migration.name}` }) const start = Date.now() await initTransaction(req) - await migrationFile.down({ payload, req }) + const db = this.sessions[await req.transactionID]?.db || this.drizzle + await migrationFile.down({ db, payload, req }) payload.logger.info({ msg: `Migrated down: ${migration.name} (${Date.now() - start}ms)`, }) diff --git a/packages/drizzle/src/migrateReset.ts b/packages/drizzle/src/migrateReset.ts index b6111406983..940429d1be1 100644 --- a/packages/drizzle/src/migrateReset.ts +++ b/packages/drizzle/src/migrateReset.ts @@ -39,7 +39,8 @@ export async function migrateReset(this: DrizzleAdapter): Promise { const start = Date.now() payload.logger.info({ msg: `Migrating down: ${migrationFile.name}` }) await initTransaction(req) - await migrationFile.down({ payload, req }) + const db = this.sessions[await req.transactionID]?.db || this.drizzle + await migrationFile.down({ db, payload, req }) payload.logger.info({ msg: `Migrated down: ${migrationFile.name} (${Date.now() - start}ms)`, }) diff --git a/packages/drizzle/src/postgres/createMigration.ts b/packages/drizzle/src/postgres/createMigration.ts deleted file mode 100644 index 04e5879caa1..00000000000 --- a/packages/drizzle/src/postgres/createMigration.ts +++ /dev/null @@ -1,122 +0,0 @@ -import type { CreateMigration } from 'payload' - -import fs from 'fs' -import { createRequire } from 'module' -import { getPredefinedMigration, writeMigrationIndex } from 'payload' -import prompts from 'prompts' - -import type { BasePostgresAdapter } 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: BasePostgresAdapter, - { dirname, file, forceAcceptWarning, migrationName, payload, skipEmpty }, -) { - const dir = payload.db.migrationDir - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir) - } - const { generateDrizzleJson, generateMigration, upPgSnapshot } = require('drizzle-kit/api') - const drizzleJsonAfter = generateDrizzleJson(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 - - if (this.schemaName) { - drizzleJsonBefore.schemas = { - [this.schemaName]: this.schemaName, - } - } - - 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')) - - if (drizzleJsonBefore.version < drizzleJsonAfter.version) { - drizzleJsonBefore = upPgSnapshot(drizzleJsonBefore) - } - } - - const sqlStatementsUp = await generateMigration(drizzleJsonBefore, drizzleJsonAfter) - const sqlStatementsDown = await generateMigration(drizzleJsonAfter, drizzleJsonBefore) - const sqlExecute = 'await payload.db.drizzle.execute(sql`' - - if (sqlStatementsUp?.length) { - upSQL = `${sqlExecute}\n ${sqlStatementsUp?.join('\n')}\`)` - } - if (sqlStatementsDown?.length) { - downSQL = `${sqlExecute}\n ${sqlStatementsDown?.join('\n')}\`)` - } - - if (!upSQL?.length && !downSQL?.length && !forceAcceptWarning) { - if (skipEmpty) { - process.exit(0) - } - - 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, - packageName: payload.db.packageName, - upSQL: upSQL || ` // Migration code`, - }), - ) - - writeMigrationIndex({ migrationsDir: payload.db.migrationDir }) - - payload.logger.info({ msg: `Migration created at ${filePath}.ts` }) -} diff --git a/packages/db-postgres/src/predefinedMigrations/v2-v3/fetchAndResave/index.ts b/packages/drizzle/src/postgres/predefinedMigrations/v2-v3/fetchAndResave/index.ts similarity index 96% rename from packages/db-postgres/src/predefinedMigrations/v2-v3/fetchAndResave/index.ts rename to packages/drizzle/src/postgres/predefinedMigrations/v2-v3/fetchAndResave/index.ts index 11140fa3692..6cd33455ff7 100644 --- a/packages/db-postgres/src/predefinedMigrations/v2-v3/fetchAndResave/index.ts +++ b/packages/drizzle/src/postgres/predefinedMigrations/v2-v3/fetchAndResave/index.ts @@ -1,15 +1,14 @@ -import type { TransactionPg } from '@payloadcms/drizzle/types' import type { FlattenedField, Payload, PayloadRequest } from 'payload' -import { upsertRow } from '@payloadcms/drizzle' - -import type { PostgresAdapter } from '../../../types.js' +import type { TransactionPg } from '../../../../types.js' +import type { BasePostgresAdapter } from '../../../types.js' import type { DocsToResave } from '../types.js' +import { upsertRow } from '../../../../upsertRow/index.js' import { traverseFields } from './traverseFields.js' type Args = { - adapter: PostgresAdapter + adapter: BasePostgresAdapter collectionSlug?: string db: TransactionPg debug: boolean diff --git a/packages/db-postgres/src/predefinedMigrations/v2-v3/fetchAndResave/traverseFields.ts b/packages/drizzle/src/postgres/predefinedMigrations/v2-v3/fetchAndResave/traverseFields.ts similarity index 100% rename from packages/db-postgres/src/predefinedMigrations/v2-v3/fetchAndResave/traverseFields.ts rename to packages/drizzle/src/postgres/predefinedMigrations/v2-v3/fetchAndResave/traverseFields.ts diff --git a/packages/db-postgres/src/predefinedMigrations/v2-v3/groupUpSQLStatements.ts b/packages/drizzle/src/postgres/predefinedMigrations/v2-v3/groupUpSQLStatements.ts similarity index 100% rename from packages/db-postgres/src/predefinedMigrations/v2-v3/groupUpSQLStatements.ts rename to packages/drizzle/src/postgres/predefinedMigrations/v2-v3/groupUpSQLStatements.ts diff --git a/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/index.ts b/packages/drizzle/src/postgres/predefinedMigrations/v2-v3/index.ts similarity index 93% rename from packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/index.ts rename to packages/drizzle/src/postgres/predefinedMigrations/v2-v3/index.ts index de163f4c691..211674e1ff6 100644 --- a/packages/db-vercel-postgres/src/predefinedMigrations/v2-v3/index.ts +++ b/packages/drizzle/src/postgres/predefinedMigrations/v2-v3/index.ts @@ -1,22 +1,19 @@ -import type { TransactionPg } from '@payloadcms/drizzle/types' import type { DrizzleSnapshotJSON } from 'drizzle-kit/api' import type { Payload, PayloadRequest } from 'payload' import { sql } from 'drizzle-orm' import fs from 'fs' -import { createRequire } from 'module' import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload' import toSnakeCase from 'to-snake-case' -import type { VercelPostgresAdapter } from '../../types.js' +import type { TransactionPg } from '../../../types.js' +import type { BasePostgresAdapter } from '../../types.js' import type { PathsToQuery } from './types.js' import { groupUpSQLStatements } from './groupUpSQLStatements.js' import { migrateRelationships } from './migrateRelationships.js' import { traverseFields } from './traverseFields.js' -const require = createRequire(import.meta.url) - type Args = { debug?: boolean payload: Payload @@ -38,13 +35,13 @@ type Args = { * @param req */ export const migratePostgresV2toV3 = async ({ debug, payload, req }: Args) => { - const adapter = payload.db as unknown as VercelPostgresAdapter + const adapter = payload.db as unknown as BasePostgresAdapter 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 - const { generateDrizzleJson, generateMigration } = require('drizzle-kit/api') - const drizzleJsonAfter = generateDrizzleJson(adapter.schema) + const { generateDrizzleJson, generateMigration, upSnapshot } = adapter.requireDrizzleKit() + const drizzleJsonAfter = generateDrizzleJson(adapter.schema) as DrizzleSnapshotJSON // Get the previous migration snapshot const previousSnapshot = fs @@ -59,10 +56,14 @@ export const migratePostgresV2toV3 = async ({ debug, payload, req }: Args) => { ) } - const drizzleJsonBefore = JSON.parse( + let drizzleJsonBefore = JSON.parse( fs.readFileSync(`${dir}/${previousSnapshot}`, 'utf8'), ) as DrizzleSnapshotJSON + if (upSnapshot && drizzleJsonBefore.version < drizzleJsonAfter.version) { + drizzleJsonBefore = upSnapshot(drizzleJsonBefore) + } + const generatedSQL = await generateMigration(drizzleJsonBefore, drizzleJsonAfter) if (!generatedSQL.length) { @@ -118,7 +119,6 @@ export const migratePostgresV2toV3 = async ({ debug, payload, req }: Args) => { const versionsTableName = adapter.tableNameMap.get( `_${toSnakeCase(collection.slug)}${adapter.versionsSuffix}`, ) - const versionFields = buildVersionCollectionFields(payload.config, collection, true) const versionPathsToQuery: PathsToQuery = new Set() diff --git a/packages/db-postgres/src/predefinedMigrations/v2-v3/migrateRelationships.ts b/packages/drizzle/src/postgres/predefinedMigrations/v2-v3/migrateRelationships.ts similarity index 94% rename from packages/db-postgres/src/predefinedMigrations/v2-v3/migrateRelationships.ts rename to packages/drizzle/src/postgres/predefinedMigrations/v2-v3/migrateRelationships.ts index 70853259972..cd893bac23d 100644 --- a/packages/db-postgres/src/predefinedMigrations/v2-v3/migrateRelationships.ts +++ b/packages/drizzle/src/postgres/predefinedMigrations/v2-v3/migrateRelationships.ts @@ -1,15 +1,15 @@ -import type { TransactionPg } from '@payloadcms/drizzle/types' import type { FlattenedField, Payload, PayloadRequest } from 'payload' import { sql } from 'drizzle-orm' -import type { PostgresAdapter } from '../../types.js' +import type { TransactionPg } from '../../../types.js' +import type { BasePostgresAdapter } from '../../types.js' import type { DocsToResave, PathsToQuery } from './types.js' import { fetchAndResave } from './fetchAndResave/index.js' type Args = { - adapter: PostgresAdapter + adapter: BasePostgresAdapter collectionSlug?: string db: TransactionPg debug: boolean diff --git a/packages/db-postgres/src/predefinedMigrations/v2-v3/traverseFields.ts b/packages/drizzle/src/postgres/predefinedMigrations/v2-v3/traverseFields.ts similarity index 94% rename from packages/db-postgres/src/predefinedMigrations/v2-v3/traverseFields.ts rename to packages/drizzle/src/postgres/predefinedMigrations/v2-v3/traverseFields.ts index 7e4dea42445..9f1995fea93 100644 --- a/packages/db-postgres/src/predefinedMigrations/v2-v3/traverseFields.ts +++ b/packages/drizzle/src/postgres/predefinedMigrations/v2-v3/traverseFields.ts @@ -1,13 +1,13 @@ -import type { TransactionPg } from '@payloadcms/drizzle/types' import type { FlattenedField, Payload } from 'payload' import toSnakeCase from 'to-snake-case' -import type { PostgresAdapter } from '../../types.js' +import type { TransactionPg } from '../../../types.js' +import type { BasePostgresAdapter } from '../../types.js' import type { PathsToQuery } from './types.js' type Args = { - adapter: PostgresAdapter + adapter: BasePostgresAdapter collectionSlug?: string columnPrefix: string db: TransactionPg diff --git a/packages/db-postgres/src/predefinedMigrations/v2-v3/types.ts b/packages/drizzle/src/postgres/predefinedMigrations/v2-v3/types.ts similarity index 100% rename from packages/db-postgres/src/predefinedMigrations/v2-v3/types.ts rename to packages/drizzle/src/postgres/predefinedMigrations/v2-v3/types.ts diff --git a/packages/drizzle/src/postgres/requireDrizzleKit.ts b/packages/drizzle/src/postgres/requireDrizzleKit.ts index c2868e6c343..234d86d11de 100644 --- a/packages/drizzle/src/postgres/requireDrizzleKit.ts +++ b/packages/drizzle/src/postgres/requireDrizzleKit.ts @@ -3,4 +3,19 @@ import { createRequire } from 'module' import type { RequireDrizzleKit } from '../types.js' const require = createRequire(import.meta.url) -export const requireDrizzleKit: RequireDrizzleKit = () => require('drizzle-kit/api') + +export const requireDrizzleKit: RequireDrizzleKit = () => { + const { + generateDrizzleJson, + generateMigration, + pushSchema, + upPgSnapshot, + } = require('drizzle-kit/api') + + return { + generateDrizzleJson, + generateMigration, + pushSchema, + upSnapshot: upPgSnapshot, + } +} diff --git a/packages/drizzle/src/postgres/types.ts b/packages/drizzle/src/postgres/types.ts index 749bd68a121..7a6f6ed410c 100644 --- a/packages/drizzle/src/postgres/types.ts +++ b/packages/drizzle/src/postgres/types.ts @@ -191,5 +191,66 @@ export type PostgresDrizzleAdapter = Omit< export type IDType = 'integer' | 'numeric' | 'uuid' | 'varchar' -export type MigrateUpArgs = { payload: Payload; req: PayloadRequest } -export type MigrateDownArgs = { payload: Payload; req: PayloadRequest } +export type MigrateUpArgs = { + /** + * The Postgres Drizzle instance that you can use to execute SQL directly within the current transaction. + * @example + * ```ts + * import { type MigrateUpArgs, sql } from '@payloadcms/db-postgres' + * + * export async function up({ db, payload, req }: MigrateUpArgs): Promise { + * const { rows: posts } = await db.execute(sql`SELECT * FROM posts`) + * } + * ``` + */ + db: PostgresDB + /** + * The Payload instance that you can use to execute Local API methods + * To use the current transaction you must pass `req` to arguments + * @example + * ```ts + * import { type MigrateUpArgs, sql } from '@payloadcms/db-postgres' + * + * export async function up({ db, payload, req }: MigrateUpArgs): Promise { + * const posts = await payload.find({ collection: 'posts', req }) + * } + * ``` + */ + payload: Payload + /** + * The `PayloadRequest` object that contains the current transaction + */ + req: PayloadRequest +} + +export type MigrateDownArgs = { + /** + * The Postgres Drizzle instance that you can use to execute SQL directly within the current transaction. + * @example + * ```ts + * import { type MigrateDownArgs, sql } from '@payloadcms/db-postgres' + * + * export async function down({ db, payload, req }: MigrateDownArgs): Promise { + * const { rows: posts } = await db.execute(sql`SELECT * FROM posts`) + * } + * ``` + */ + db: PostgresDB + /** + * The Payload instance that you can use to execute Local API methods + * To use the current transaction you must pass `req` to arguments + * @example + * ```ts + * import { type MigrateDownArgs } from '@payloadcms/db-postgres' + * + * export async function down({ db, payload, req }: MigrateDownArgs): Promise { + * const posts = await payload.find({ collection: 'posts', req }) + * } + * ``` + */ + payload: Payload + /** + * The `PayloadRequest` object that contains the current transaction + */ + req: PayloadRequest +} diff --git a/packages/drizzle/src/types.ts b/packages/drizzle/src/types.ts index 58c00912437..dc940b1d37a 100644 --- a/packages/drizzle/src/types.ts +++ b/packages/drizzle/src/types.ts @@ -14,19 +14,14 @@ import type { NodePgDatabase, NodePgQueryResultHKT } from 'drizzle-orm/node-post import type { PgColumn, PgTable, PgTransaction } from 'drizzle-orm/pg-core' import type { SQLiteColumn, SQLiteTable, SQLiteTransaction } from 'drizzle-orm/sqlite-core' import type { Result } from 'drizzle-orm/sqlite-core/session' -import type { - BaseDatabaseAdapter, - MigrationData, - MigrationTemplateArgs, - Payload, - PayloadRequest, -} from 'payload' +import type { BaseDatabaseAdapter, MigrationData, Payload, PayloadRequest } from 'payload' import type { BuildQueryJoinAliases } from './queries/buildQuery.js' export { BuildQueryJoinAliases } import type { ResultSet } from '@libsql/client' +import type { DrizzleSnapshotJSON } from 'drizzle-kit/api' import type { SQLiteRaw } from 'drizzle-orm/sqlite-core/query-builders/raw' import type { QueryResult } from 'pg' @@ -117,7 +112,10 @@ export type Insert = (args: { }) => Promise[]> export type RequireDrizzleKit = () => { - generateDrizzleJson: (args: { schema: Record }) => unknown + generateDrizzleJson: ( + args: Record, + ) => DrizzleSnapshotJSON | Promise + generateMigration: (prev: DrizzleSnapshotJSON, cur: DrizzleSnapshotJSON) => Promise pushSchema: ( schema: Record, drizzle: DrizzleAdapter['drizzle'], @@ -125,6 +123,7 @@ export type RequireDrizzleKit = () => { tablesFilter?: string[], extensionsFilter?: string[], ) => Promise<{ apply; hasDataLoss; warnings }> + upSnapshot?: (snapshot: Record) => DrizzleSnapshotJSON } export type Migration = { @@ -177,7 +176,6 @@ export interface DrizzleAdapter extends BaseDatabaseAdapter { * Used for returning properly formed errors from unique fields */ fieldConstraints: Record> - getMigrationTemplate: (args: MigrationTemplateArgs) => string idType: 'serial' | 'uuid' indexes: Set initializing: Promise diff --git a/packages/drizzle/src/utilities/buildCreateMigration.ts b/packages/drizzle/src/utilities/buildCreateMigration.ts new file mode 100644 index 00000000000..2b287470acf --- /dev/null +++ b/packages/drizzle/src/utilities/buildCreateMigration.ts @@ -0,0 +1,134 @@ +import type { DrizzleSnapshotJSON } from 'drizzle-kit/api' +import type { CreateMigration } from 'payload' + +import fs from 'fs' +import path from 'path' +import { getPredefinedMigration, writeMigrationIndex } from 'payload' +import prompts from 'prompts' + +import type { DrizzleAdapter } from '../types.js' + +import { getMigrationTemplate } from './getMigrationTemplate.js' + +export const buildCreateMigration = ({ + executeMethod, + filename, + sanitizeStatements, +}: { + executeMethod: string + filename: string + sanitizeStatements: (args: { sqlExecute: string; statements: string[] }) => string +}): CreateMigration => { + const dirname = path.dirname(filename) + return async function createMigration( + this: DrizzleAdapter, + { file, forceAcceptWarning, migrationName, payload, skipEmpty }, + ) { + const dir = payload.db.migrationDir + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir) + } + + const { generateDrizzleJson, generateMigration, upSnapshot } = this.requireDrizzleKit() + + const drizzleJsonAfter = await generateDrizzleJson(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 = this.defaultDrizzleSnapshot as DrizzleSnapshotJSON + + if (this.schemaName) { + drizzleJsonBefore.schemas = { + [this.schemaName]: this.schemaName, + } + } + + 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')) + + if (upSnapshot && drizzleJsonBefore.version < drizzleJsonAfter.version) { + drizzleJsonBefore = upSnapshot(drizzleJsonBefore) + } + } + + const sqlStatementsUp = await generateMigration(drizzleJsonBefore, drizzleJsonAfter) + const sqlStatementsDown = await generateMigration(drizzleJsonAfter, drizzleJsonBefore) + const sqlExecute = `await db.${executeMethod}(` + 'sql`' + + if (sqlStatementsUp?.length) { + upSQL = sanitizeStatements({ sqlExecute, statements: sqlStatementsUp }) + } + if (sqlStatementsDown?.length) { + downSQL = sanitizeStatements({ sqlExecute, statements: sqlStatementsDown }) + } + + if (!upSQL?.length && !downSQL?.length && !forceAcceptWarning) { + if (skipEmpty) { + process.exit(0) + } + + 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, + packageName: payload.db.packageName, + upSQL: upSQL || ` // Migration code`, + }), + ) + + writeMigrationIndex({ migrationsDir: payload.db.migrationDir }) + + payload.logger.info({ msg: `Migration created at ${filePath}.ts` }) + } +} diff --git a/packages/drizzle/src/postgres/getMigrationTemplate.ts b/packages/drizzle/src/utilities/getMigrationTemplate.ts similarity index 75% rename from packages/drizzle/src/postgres/getMigrationTemplate.ts rename to packages/drizzle/src/utilities/getMigrationTemplate.ts index 1f2c8ba9a66..3b1b05be36f 100644 --- a/packages/drizzle/src/postgres/getMigrationTemplate.ts +++ b/packages/drizzle/src/utilities/getMigrationTemplate.ts @@ -13,11 +13,11 @@ export const getMigrationTemplate = ({ upSQL, }: MigrationTemplateArgs): string => `import { MigrateUpArgs, MigrateDownArgs, sql } from '${packageName}' ${imports ? `${imports}\n` : ''} -export async function up({ payload, req }: MigrateUpArgs): Promise { +export async function up({ db, payload, req }: MigrateUpArgs): Promise { ${indent(upSQL)} } -export async function down({ payload, req }: MigrateDownArgs): Promise { +export async function down({ db, payload, req }: MigrateDownArgs): Promise { ${indent(downSQL)} } ` diff --git a/packages/graphql/src/resolvers/collections/create.ts b/packages/graphql/src/resolvers/collections/create.ts index 17ad5180d61..ce7a4e49584 100644 --- a/packages/graphql/src/resolvers/collections/create.ts +++ b/packages/graphql/src/resolvers/collections/create.ts @@ -30,15 +30,13 @@ export function createResolver( context.req.locale = args.locale } - const options = { + const result = await createOperation({ collection, data: args.data, depth: 0, draft: args.draft, req: isolateObjectProperty(context.req, 'transactionID'), - } - - const result = await createOperation(options) + }) return result } diff --git a/packages/graphql/src/resolvers/collections/duplicate.ts b/packages/graphql/src/resolvers/collections/duplicate.ts index b80609b30dd..73fca9b15db 100644 --- a/packages/graphql/src/resolvers/collections/duplicate.ts +++ b/packages/graphql/src/resolvers/collections/duplicate.ts @@ -7,6 +7,7 @@ import type { Context } from '../types.js' export type Resolver = ( _: unknown, args: { + data: TData draft: boolean fallbackLocale?: string id: string @@ -28,15 +29,14 @@ export function duplicateResolver( req.fallbackLocale = args.fallbackLocale || fallbackLocale context.req = req - const options = { + const result = await duplicateOperation({ id: args.id, collection, + data: args.data, depth: 0, draft: args.draft, req: isolateObjectProperty(req, 'transactionID'), - } - - const result = await duplicateOperation(options) + }) return result } diff --git a/packages/graphql/src/schema/initCollections.ts b/packages/graphql/src/schema/initCollections.ts index 3f08e1ce7e2..394f621b801 100644 --- a/packages/graphql/src/schema/initCollections.ts +++ b/packages/graphql/src/schema/initCollections.ts @@ -280,6 +280,9 @@ export function initCollections({ config, graphqlResult }: InitCollectionsGraphQ type: collection.graphQL.type, args: { id: { type: new GraphQLNonNull(idType) }, + ...(createMutationInputType + ? { data: { type: collection.graphQL.mutationInputType } } + : {}), }, resolve: duplicateResolver(collection), } diff --git a/packages/next/src/layouts/Root/index.tsx b/packages/next/src/layouts/Root/index.tsx index 39d89887df2..32e45e782dd 100644 --- a/packages/next/src/layouts/Root/index.tsx +++ b/packages/next/src/layouts/Root/index.tsx @@ -93,7 +93,12 @@ export const RootLayout = async ({ }) return ( - + diff --git a/packages/next/src/routes/rest/collections/duplicate.ts b/packages/next/src/routes/rest/collections/duplicate.ts index 57350f53550..7e7a7d56c39 100644 --- a/packages/next/src/routes/rest/collections/duplicate.ts +++ b/packages/next/src/routes/rest/collections/duplicate.ts @@ -27,6 +27,7 @@ export const duplicate: CollectionRouteHandlerWithID = async ({ const doc = await duplicateOperation({ id, collection, + data: req.data, depth: isNumber(depth) ? Number(depth) : undefined, draft, populate: sanitizePopulateParam(req.query.populate), diff --git a/packages/next/src/templates/Default/index.tsx b/packages/next/src/templates/Default/index.tsx index 32b9019fd43..b410ee6a15f 100644 --- a/packages/next/src/templates/Default/index.tsx +++ b/packages/next/src/templates/Default/index.tsx @@ -48,6 +48,20 @@ export const DefaultTemplate: React.FC = ({ } = {}, } = payload.config || {} + const serverProps = React.useMemo( + () => ({ + i18n, + locale, + params, + payload, + permissions, + searchParams, + user, + visibleEntities, + }), + [i18n, locale, params, payload, permissions, searchParams, user, visibleEntities], + ) + const { Actions } = React.useMemo<{ Actions: Record }>(() => { @@ -59,11 +73,13 @@ export const DefaultTemplate: React.FC = ({ acc[action.path] = RenderServerComponent({ Component: action, importMap: payload.importMap, + serverProps, }) } else { acc[action] = RenderServerComponent({ Component: action, importMap: payload.importMap, + serverProps, }) } } @@ -72,23 +88,14 @@ export const DefaultTemplate: React.FC = ({ }, {}) : undefined, } - }, [viewActions, payload]) + }, [payload, serverProps, viewActions]) const NavComponent = RenderServerComponent({ clientProps: { clientProps: { visibleEntities } }, Component: CustomNav, Fallback: DefaultNav, importMap: payload.importMap, - serverProps: { - i18n, - locale, - params, - payload, - permissions, - searchParams, - user, - visibleEntities, - }, + serverProps, }) return ( @@ -99,16 +106,7 @@ export const DefaultTemplate: React.FC = ({ clientProps: { clientProps: { visibleEntities } }, Component: CustomHeader, importMap: payload.importMap, - serverProps: { - i18n, - locale, - params, - payload, - permissions, - searchParams, - user, - visibleEntities, - }, + serverProps, })}