diff --git a/packages/db-mongodb/.gitignore b/packages/db-mongodb/.gitignore new file mode 100644 index 0000000000..fc99080017 --- /dev/null +++ b/packages/db-mongodb/.gitignore @@ -0,0 +1 @@ +/migrations diff --git a/packages/db-mongodb/package.json b/packages/db-mongodb/package.json index 1070adeb25..7dba8f1621 100644 --- a/packages/db-mongodb/package.json +++ b/packages/db-mongodb/package.json @@ -26,6 +26,7 @@ "mongoose": "6.11.4", "mongoose-aggregate-paginate-v2": "1.0.6", "mongoose-paginate-v2": "1.7.22", + "prompts": "2.4.2", "uuid": "9.0.0" }, "devDependencies": { diff --git a/packages/db-mongodb/src/index.ts b/packages/db-mongodb/src/index.ts index 2bad49372b..13d2ec8c5a 100644 --- a/packages/db-mongodb/src/index.ts +++ b/packages/db-mongodb/src/index.ts @@ -3,6 +3,7 @@ import type { Payload } from 'payload' import type { BaseDatabaseAdapter } from 'payload/database' import mongoose from 'mongoose' +import path from 'path' import { createDatabaseAdapter } from 'payload/database' import { createMigration } from 'payload/database' @@ -27,6 +28,7 @@ import { findGlobalVersions } from './findGlobalVersions' import { findOne } from './findOne' import { findVersions } from './findVersions' import { init } from './init' +import { migrateFresh } from './migrateFresh' import { queryDrafts } from './queryDrafts' import { beginTransaction } from './transactions/beginTransaction' import { commitTransaction } from './transactions/commitTransaction' @@ -83,23 +85,34 @@ declare module 'payload' { export function mongooseAdapter({ autoPluralization = true, connectOptions, - migrationDir, + migrationDir: migrationDirArg, url, }: Args): MongooseAdapterResult { function adapter({ payload }: { payload: Payload }) { + const migrationDir = migrationDirArg || path.resolve(process.cwd(), 'src/migrations') mongoose.set('strictQuery', false) extendWebpackConfig(payload.config) extendViteConfig(payload.config) return createDatabaseAdapter({ + name: 'mongoose', + + // Mongoose-specific autoPluralization, - beginTransaction, collections: {}, - commitTransaction, - connect, connectOptions: connectOptions || {}, connection: undefined, + globals: undefined, + mongoMemoryServer: undefined, + sessions: {}, + url, + versions: {}, + + // DatabaseAdapter + beginTransaction, + commitTransaction, + connect, create, createGlobal, createGlobalVersion, @@ -115,21 +128,16 @@ export function mongooseAdapter({ findGlobalVersions, findOne, findVersions, - globals: undefined, init, - ...(migrationDir && { migrationDir }), - name: 'mongoose', - mongoMemoryServer: undefined, + migrateFresh, + migrationDir, payload, queryDrafts, rollbackTransaction, - sessions: {}, updateGlobal, updateGlobalVersion, updateOne, updateVersion, - url, - versions: {}, }) } diff --git a/packages/db-mongodb/src/migrateFresh.ts b/packages/db-mongodb/src/migrateFresh.ts new file mode 100644 index 0000000000..1701026dc4 --- /dev/null +++ b/packages/db-mongodb/src/migrateFresh.ts @@ -0,0 +1,73 @@ +import type { PayloadRequest } from 'payload/types' + +import { readMigrationFiles } from 'payload/database' +import prompts from 'prompts' + +import type { MongooseAdapter } from '.' + +/** + * Drop the current database and run all migrate up functions + */ +export async function migrateFresh(this: MongooseAdapter): Promise { + const { payload } = this + + const { confirm: acceptWarning } = await prompts( + { + name: 'confirm', + initial: false, + message: `WARNING: This will drop your database and run all migrations. Are you sure you want to proceed?`, + type: 'confirm', + }, + { + onCancel: () => { + process.exit(0) + }, + }, + ) + + if (!acceptWarning) { + process.exit(0) + } + + payload.logger.info({ + msg: `Dropping database.`, + }) + + await this.connection.dropDatabase() + + const migrationFiles = await readMigrationFiles({ payload }) + payload.logger.debug({ + msg: `Found ${migrationFiles.length} migration files.`, + }) + + let transactionID + // Run all migrate up + for (const migration of migrationFiles) { + payload.logger.info({ msg: `Migrating: ${migration.name}` }) + try { + const start = Date.now() + transactionID = await this.beginTransaction() + await migration.up({ payload }) + await payload.create({ + collection: 'payload-migrations', + data: { + name: migration.name, + batch: 1, + }, + req: { + transactionID, + } as PayloadRequest, + }) + await this.commitTransaction(transactionID) + + payload.logger.info({ msg: `Migrated: ${migration.name} (${Date.now() - start}ms)` }) + } catch (err: unknown) { + await this.rollbackTransaction(transactionID) + payload.logger.error({ + err, + msg: `Error running migration ${migration.name}. Rolling back.`, + }) + throw err + } + } +} diff --git a/packages/db-postgres/src/index.ts b/packages/db-postgres/src/index.ts index 90cc49c695..839b40d7ab 100644 --- a/packages/db-postgres/src/index.ts +++ b/packages/db-postgres/src/index.ts @@ -24,6 +24,10 @@ import { findOne } from './findOne' import { findVersions } from './findVersions' import { init } from './init' import { migrate } from './migrate' +import { migrateDown } from './migrateDown' +import { migrateFresh } from './migrateFresh' +import { migrateRefresh } from './migrateRefresh' +import { migrateReset } from './migrateReset' import { migrateStatus } from './migrateStatus' import { queryDrafts } from './queryDrafts' import { beginTransaction } from './transactions/beginTransaction' @@ -77,6 +81,10 @@ export function postgresAdapter(args: Args): PostgresAdapterResult { findVersions, init, migrate, + migrateDown, + migrateFresh, + migrateRefresh, + migrateReset, migrateStatus, migrationDir, payload, diff --git a/packages/db-postgres/src/migrateDown.ts b/packages/db-postgres/src/migrateDown.ts new file mode 100644 index 0000000000..1ec5971b31 --- /dev/null +++ b/packages/db-postgres/src/migrateDown.ts @@ -0,0 +1,69 @@ +/* eslint-disable no-restricted-syntax, no-await-in-loop */ +import type { PayloadRequest } from 'payload/types' + +import { getMigrations, readMigrationFiles } from 'payload/database' + +import type { PostgresAdapter } from './types' + +import { migrationTableExists } from './utilities/migrationTableExists' + +export async function migrateDown(this: PostgresAdapter): Promise { + const { payload } = this + const migrationFiles = await readMigrationFiles({ payload }) + + const { existingMigrations, latestBatch } = await getMigrations({ + payload, + }) + + const migrationsToRollback = existingMigrations.filter( + (migration) => migration.batch === latestBatch && migration.batch !== -1, + ) + + if (!migrationsToRollback?.length) { + payload.logger.info({ msg: 'No migrations to rollback.' }) + return + } + + payload.logger.info({ + msg: `Rolling back batch ${latestBatch} consisting of ${migrationsToRollback.length} migration(s).`, + }) + + for (const migration of migrationsToRollback) { + const migrationFile = migrationFiles.find((m) => m.name === migration.name) + if (!migrationFile) { + throw new Error(`Migration ${migration.name} not found locally.`) + } + + const start = Date.now() + let transactionID + + try { + payload.logger.info({ msg: `Migrating down: ${migrationFile.name}` }) + transactionID = await this.beginTransaction() + await migrationFile.down({ payload }) + payload.logger.info({ + msg: `Migrated down: ${migrationFile.name} (${Date.now() - start}ms)`, + }) + + const tableExists = await migrationTableExists(this.drizzle) + if (tableExists) { + await payload.delete({ + id: migration.id, + collection: 'payload-migrations', + req: { + transactionID, + } as PayloadRequest, + }) + } + + await this.commitTransaction(transactionID) + } catch (err: unknown) { + await this.rollbackTransaction(transactionID) + payload.logger.error({ + err, + msg: `Error running migration ${migrationFile.name}`, + }) + throw err + } + } +} diff --git a/packages/db-postgres/src/migrateFresh.ts b/packages/db-postgres/src/migrateFresh.ts new file mode 100644 index 0000000000..68abb25225 --- /dev/null +++ b/packages/db-postgres/src/migrateFresh.ts @@ -0,0 +1,74 @@ +import type { PayloadRequest } from 'payload/types' + +import { sql } from 'drizzle-orm' +import { readMigrationFiles } from 'payload/database' +import prompts from 'prompts' + +import type { PostgresAdapter } from './types' + +/** + * Drop the current database and run all migrate up functions + */ +export async function migrateFresh(this: PostgresAdapter): Promise { + const { payload } = this + + const { confirm: acceptWarning } = await prompts( + { + name: 'confirm', + initial: false, + message: `WARNING: This will drop your database and run all migrations. Are you sure you want to proceed?`, + type: 'confirm', + }, + { + onCancel: () => { + process.exit(0) + }, + }, + ) + + if (!acceptWarning) { + process.exit(0) + } + + payload.logger.info({ + msg: `Dropping database.`, + }) + + await this.drizzle.execute(sql`drop schema public cascade;\ncreate schema public;`) + + const migrationFiles = await readMigrationFiles({ payload }) + payload.logger.debug({ + msg: `Found ${migrationFiles.length} migration files.`, + }) + + let transactionID + // Run all migrate up + for (const migration of migrationFiles) { + payload.logger.info({ msg: `Migrating: ${migration.name}` }) + try { + const start = Date.now() + transactionID = await this.beginTransaction() + await migration.up({ payload }) + await payload.create({ + collection: 'payload-migrations', + data: { + name: migration.name, + batch: 1, + }, + req: { + transactionID, + } as PayloadRequest, + }) + await this.commitTransaction(transactionID) + + payload.logger.info({ msg: `Migrated: ${migration.name} (${Date.now() - start}ms)` }) + } catch (err: unknown) { + await this.rollbackTransaction(transactionID) + payload.logger.error({ + err, + msg: `Error running migration ${migration.name}. Rolling back.`, + }) + throw err + } + } +} diff --git a/packages/db-postgres/src/migrateRefresh.ts b/packages/db-postgres/src/migrateRefresh.ts new file mode 100644 index 0000000000..6848f95f9d --- /dev/null +++ b/packages/db-postgres/src/migrateRefresh.ts @@ -0,0 +1,111 @@ +/* eslint-disable no-restricted-syntax, no-await-in-loop */ +import type { PayloadRequest } from 'payload/types' + +import { getMigrations, readMigrationFiles } from 'payload/database' + +import type { PostgresAdapter } from './types' + +import { migrationTableExists } from './utilities/migrationTableExists' + +/** + * Run all migration down functions before running up + */ +export async function migrateRefresh(this: PostgresAdapter) { + const { payload } = this + const migrationFiles = await readMigrationFiles({ payload }) + + const { existingMigrations, latestBatch } = await getMigrations({ + payload, + }) + + const migrationsToRollback = existingMigrations.filter( + (migration) => migration.batch === latestBatch && migration.batch !== -1, + ) + + if (!migrationsToRollback?.length) { + payload.logger.info({ msg: 'No migrations to rollback.' }) + return + } + + payload.logger.info({ + msg: `Rolling back batch ${latestBatch} consisting of ${migrationsToRollback.length} migration(s).`, + }) + + let transactionID + + // Reverse order of migrations to rollback + migrationsToRollback.reverse() + + for (const migration of migrationsToRollback) { + try { + const migrationFile = migrationFiles.find((m) => m.name === migration.name) + if (!migrationFile) { + throw new Error(`Migration ${migration.name} not found locally.`) + } + + payload.logger.info({ msg: `Migrating down: ${migration.name}` }) + const start = Date.now() + transactionID = await this.beginTransaction() + await migrationFile.down({ payload }) + payload.logger.info({ + msg: `Migrated down: ${migration.name} (${Date.now() - start}ms)`, + }) + + const tableExists = await migrationTableExists(this.drizzle) + if (tableExists) { + await payload.delete({ + collection: 'payload-migrations', + req: { + transactionID, + } as PayloadRequest, + where: { + name: { + equals: migration.name, + }, + }, + }) + } + } catch (err: unknown) { + await this.rollbackTransaction(transactionID) + let msg = `Error running migration ${migration.name}. Rolling back.` + if (err instanceof Error) { + msg += ` ${err.message}` + } + payload.logger.error({ + err, + msg, + }) + throw err + } + } + + // Run all migrate up + for (const migration of migrationFiles) { + payload.logger.info({ msg: `Migrating: ${migration.name}` }) + try { + const start = Date.now() + transactionID = await this.beginTransaction() + await migration.up({ payload }) + await payload.create({ + collection: 'payload-migrations', + data: { + name: migration.name, + executed: true, + }, + req: { + transactionID, + } as PayloadRequest, + }) + await this.commitTransaction(transactionID) + + payload.logger.info({ msg: `Migrated: ${migration.name} (${Date.now() - start}ms)` }) + } catch (err: unknown) { + await this.rollbackTransaction(transactionID) + payload.logger.error({ + err, + msg: `Error running migration ${migration.name}. Rolling back.`, + }) + throw err + } + } +} diff --git a/packages/db-postgres/src/migrateReset.ts b/packages/db-postgres/src/migrateReset.ts new file mode 100644 index 0000000000..8dfd87a266 --- /dev/null +++ b/packages/db-postgres/src/migrateReset.ts @@ -0,0 +1,81 @@ +/* eslint-disable no-restricted-syntax, no-await-in-loop */ +import type { PayloadRequest } from 'payload/types' + +import { getMigrations, readMigrationFiles } from 'payload/database' + +import type { PostgresAdapter } from './types' + +import { migrationTableExists } from './utilities/migrationTableExists' + +/** + * Run all migrate down functions + */ +export async function migrateReset(this: PostgresAdapter): Promise { + const { payload } = this + const migrationFiles = await readMigrationFiles({ payload }) + + const { existingMigrations } = await getMigrations({ payload }) + + if (!existingMigrations?.length) { + payload.logger.info({ msg: 'No migrations to reset.' }) + return + } + + // Rollback all migrations in order + for (const migration of existingMigrations) { + const migrationFile = migrationFiles.find((m) => m.name === migration.name) + if (!migrationFile) { + throw new Error(`Migration ${migration.name} not found locally.`) + } + + const start = Date.now() + let transactionID + + try { + payload.logger.info({ msg: `Migrating down: ${migrationFile.name}` }) + transactionID = await this.beginTransaction() + await migrationFile.down({ payload }) + payload.logger.info({ + msg: `Migrated down: ${migrationFile.name} (${Date.now() - start}ms)`, + }) + + const tableExists = await migrationTableExists(this.drizzle) + if (tableExists) { + await payload.delete({ + id: migration.id, + collection: 'payload-migrations', + req: { + transactionID, + } as PayloadRequest, + }) + } + + await this.commitTransaction(transactionID) + } catch (err: unknown) { + await this.rollbackTransaction(transactionID) + payload.logger.error({ + err, + msg: `Error running migration ${migrationFile.name}`, + }) + throw err + } + } + + // Delete dev migration + + const tableExists = await migrationTableExists(this.drizzle) + if (tableExists) { + try { + await payload.delete({ + collection: 'payload-migrations', + where: { + batch: { + equals: -1, + }, + }, + }) + } catch (err: unknown) { + payload.logger.error({ error: err, msg: 'Error deleting dev migration' }) + } + } +} diff --git a/packages/payload/src/database/migrations/migrateRefresh.ts b/packages/payload/src/database/migrations/migrateRefresh.ts index 5432512c09..283a7ec9d8 100644 --- a/packages/payload/src/database/migrations/migrateRefresh.ts +++ b/packages/payload/src/database/migrations/migrateRefresh.ts @@ -2,22 +2,78 @@ import type { PayloadRequest } from '../../express/types' import type { BaseDatabaseAdapter } from '../types' +import { getMigrations } from './getMigrations' import { readMigrationFiles } from './readMigrationFiles' /** - * Reset and re-run all migrations. + * Run all migration down functions before running up */ export async function migrateRefresh(this: BaseDatabaseAdapter) { const { payload } = this const migrationFiles = await readMigrationFiles({ payload }) - // Clear all migrations - await payload.delete({ - collection: 'payload-migrations', - where: {}, // All migrations + const { existingMigrations, latestBatch } = await getMigrations({ + payload, }) + + const migrationsToRollback = existingMigrations.filter( + (migration) => migration.batch === latestBatch && migration.batch !== -1, + ) + + if (!migrationsToRollback?.length) { + payload.logger.info({ msg: 'No migrations to rollback.' }) + return + } + + payload.logger.info({ + msg: `Rolling back batch ${latestBatch} consisting of ${migrationsToRollback.length} migration(s).`, + }) + let transactionID - // Run all migrations + + // Reverse order of migrations to rollback + migrationsToRollback.reverse() + + for (const migration of migrationsToRollback) { + try { + const migrationFile = migrationFiles.find((m) => m.name === migration.name) + if (!migrationFile) { + throw new Error(`Migration ${migration.name} not found locally.`) + } + + payload.logger.info({ msg: `Migrating down: ${migration.name}` }) + const start = Date.now() + transactionID = await this.beginTransaction() + await migrationFile.down({ payload }) + payload.logger.info({ + msg: `Migrated down: ${migration.name} (${Date.now() - start}ms)`, + }) + await payload.delete({ + collection: 'payload-migrations', + req: { + transactionID, + } as PayloadRequest, + where: { + name: { + equals: migration.name, + }, + }, + }) + } catch (err: unknown) { + await this.rollbackTransaction(transactionID) + let msg = `Error running migration ${migration.name}. Rolling back.` + if (err instanceof Error) { + msg += ` ${err.message}` + } + payload.logger.error({ + err, + msg, + }) + throw err + } + } + + // Run all migrate up for (const migration of migrationFiles) { payload.logger.info({ msg: `Migrating: ${migration.name}` }) try { @@ -41,7 +97,7 @@ export async function migrateRefresh(this: BaseDatabaseAdapter) { await this.rollbackTransaction(transactionID) payload.logger.error({ err, - msg: `Error running migration ${migration.name}`, + msg: `Error running migration ${migration.name}. Rolling back.`, }) throw err } diff --git a/packages/payload/src/database/migrations/migrationTemplate.ts b/packages/payload/src/database/migrations/migrationTemplate.ts index 8858a4e842..ab5f4fa6e3 100644 --- a/packages/payload/src/database/migrations/migrationTemplate.ts +++ b/packages/payload/src/database/migrations/migrationTemplate.ts @@ -8,7 +8,7 @@ export async function up({ payload }: MigrateUpArgs): Promise { // Migration code }; -export async function down({ payload }: MigrateUpArgs): Promise { +export async function down({ payload }: MigrateDownArgs): Promise { // Migration code }; ` diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8108972002..5dc0241b6f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -293,6 +293,9 @@ importers: mongoose-paginate-v2: specifier: 1.7.22 version: 1.7.22 + prompts: + specifier: 2.4.2 + version: 2.4.2 uuid: specifier: 9.0.0 version: 9.0.0 diff --git a/test/buildConfigWithDefaults.ts b/test/buildConfigWithDefaults.ts index 49e29d94cd..dc6bafdaad 100644 --- a/test/buildConfigWithDefaults.ts +++ b/test/buildConfigWithDefaults.ts @@ -18,6 +18,7 @@ const bundlerAdapters = { const databaseAdapters = { mongoose: mongooseAdapter({ + migrationDir: path.resolve(__dirname, '../packages/db-mongodb/migrations'), url: 'mongodb://127.0.0.1/payloadtests', }), postgres: postgresAdapter({