diff --git a/packages/db-mongodb/src/connect.ts b/packages/db-mongodb/src/connect.ts index 4d17766c7..7c1a1fb2c 100644 --- a/packages/db-mongodb/src/connect.ts +++ b/packages/db-mongodb/src/connect.ts @@ -54,6 +54,10 @@ export const connect: Connect = async function connect( this.payload.logger.info('---- DROPPED DATABASE ----') } } + + if (process.env.NODE_ENV === 'production' && this.prodMigrations) { + await this.migrate({ migrations: this.prodMigrations }) + } } catch (err) { console.log(err) this.payload.logger.error(`Error: cannot connect to MongoDB. Details: ${err.message}`, err) diff --git a/packages/db-mongodb/src/index.ts b/packages/db-mongodb/src/index.ts index e11d1f99e..9f96aa5ed 100644 --- a/packages/db-mongodb/src/index.ts +++ b/packages/db-mongodb/src/index.ts @@ -8,7 +8,7 @@ import mongoose from 'mongoose' import path from 'path' import { createDatabaseAdapter } from 'payload' -import type { CollectionModel, GlobalModel } from './types.js' +import type { CollectionModel, GlobalModel, MigrateDownArgs, MigrateUpArgs } from './types.js' import { connect } from './connect.js' import { count } from './count.js' @@ -78,6 +78,11 @@ export interface Args { * typed as any to avoid dependency */ mongoMemoryServer?: MongoMemoryReplSet + prodMigrations?: { + down: (args: MigrateDownArgs) => Promise + name: string + up: (args: MigrateUpArgs) => Promise + }[] transactionOptions?: TransactionOptions | false /** The URL to connect to MongoDB or false to start payload and prevent connecting */ url: false | string @@ -90,6 +95,11 @@ export type MongooseAdapter = { connection: Connection globals: GlobalModel mongoMemoryServer: MongoMemoryReplSet + prodMigrations?: { + down: (args: MigrateDownArgs) => Promise + name: string + up: (args: MigrateUpArgs) => Promise + }[] sessions: Record versions: { [slug: string]: CollectionModel @@ -107,6 +117,11 @@ declare module 'payload' { connection: Connection globals: GlobalModel mongoMemoryServer: MongoMemoryReplSet + prodMigrations?: { + down: (args: MigrateDownArgs) => Promise + name: string + up: (args: MigrateUpArgs) => Promise + }[] sessions: Record transactionOptions: TransactionOptions versions: { @@ -121,6 +136,7 @@ export function mongooseAdapter({ disableIndexHints = false, migrationDir: migrationDirArg, mongoMemoryServer, + prodMigrations, transactionOptions = {}, url, }: Args): DatabaseAdapterObj { @@ -167,6 +183,7 @@ export function mongooseAdapter({ migrateFresh, migrationDir, payload, + prodMigrations, queryDrafts, rollbackTransaction, updateGlobal, diff --git a/packages/db-postgres/src/connect.ts b/packages/db-postgres/src/connect.ts index 1e7be5ae3..45c5132cd 100644 --- a/packages/db-postgres/src/connect.ts +++ b/packages/db-postgres/src/connect.ts @@ -91,4 +91,8 @@ export const connect: Connect = async function connect( } if (typeof this.resolveInitializing === 'function') this.resolveInitializing() + + if (process.env.NODE_ENV === 'production' && this.prodMigrations) { + await this.migrate({ migrations: this.prodMigrations }) + } } diff --git a/packages/db-postgres/src/createMigration.ts b/packages/db-postgres/src/createMigration.ts index 03a5e6745..52b0bf68c 100644 --- a/packages/db-postgres/src/createMigration.ts +++ b/packages/db-postgres/src/createMigration.ts @@ -4,7 +4,7 @@ import type { CreateMigration } from 'payload' import fs from 'fs' import { createRequire } from 'module' import path from 'path' -import { getPredefinedMigration } from 'payload' +import { getPredefinedMigration, writeMigrationIndex } from 'payload' import prompts from 'prompts' import { fileURLToPath } from 'url' @@ -115,5 +115,8 @@ export const createMigration: CreateMigration = async function createMigration( 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/index.ts b/packages/db-postgres/src/index.ts index 4aa41696f..755365a5a 100644 --- a/packages/db-postgres/src/index.ts +++ b/packages/db-postgres/src/index.ts @@ -32,7 +32,7 @@ import { updateOne, updateVersion, } from '@payloadcms/drizzle' -import { type PgSchema, pgEnum, pgSchema, pgTable } from 'drizzle-orm/pg-core' +import { pgEnum, pgSchema, pgTable } from 'drizzle-orm/pg-core' import { createDatabaseAdapter } from 'payload' import type { Args, PostgresAdapter } from './types.js' @@ -94,6 +94,7 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj pgSchema: adapterSchema, pool: undefined, poolOptions: args.pool, + prodMigrations: args.prodMigrations, push: args.push, relations: {}, relationshipsSuffix: args.relationshipsSuffix || '_rels', diff --git a/packages/db-postgres/src/types.ts b/packages/db-postgres/src/types.ts index 8615df1ca..fbc757efa 100644 --- a/packages/db-postgres/src/types.ts +++ b/packages/db-postgres/src/types.ts @@ -33,6 +33,11 @@ export type Args = { logger?: DrizzleConfig['logger'] migrationDir?: string pool: PoolConfig + prodMigrations?: { + down: (args: MigrateDownArgs) => Promise + name: string + up: (args: MigrateUpArgs) => Promise + }[] push?: boolean relationshipsSuffix?: string /** @@ -136,6 +141,11 @@ export type PostgresAdapter = { pgSchema?: Schema pool: Pool poolOptions: Args['pool'] + prodMigrations?: { + down: (args: MigrateDownArgs) => Promise + name: string + up: (args: MigrateUpArgs) => Promise + }[] push: boolean rejectInitializing: () => void relations: Record @@ -178,6 +188,11 @@ declare module 'payload' { pgSchema?: { table: PgTableFn } | PgSchema pool: Pool poolOptions: Args['pool'] + prodMigrations?: { + down: (args: MigrateDownArgs) => Promise + name: string + up: (args: MigrateUpArgs) => Promise + }[] push: boolean rejectInitializing: () => void relationshipsSuffix?: string diff --git a/packages/db-sqlite/src/connect.ts b/packages/db-sqlite/src/connect.ts index 19e340a82..a710e819e 100644 --- a/packages/db-sqlite/src/connect.ts +++ b/packages/db-sqlite/src/connect.ts @@ -52,4 +52,8 @@ export const connect: Connect = async function connect( } if (typeof this.resolveInitializing === 'function') this.resolveInitializing() + + if (process.env.NODE_ENV === 'production' && this.prodMigrations) { + await this.migrate({ migrations: this.prodMigrations }) + } } diff --git a/packages/db-sqlite/src/createMigration.ts b/packages/db-sqlite/src/createMigration.ts index 3846692b6..4beb5acab 100644 --- a/packages/db-sqlite/src/createMigration.ts +++ b/packages/db-sqlite/src/createMigration.ts @@ -4,7 +4,7 @@ import type { CreateMigration } from 'payload' import fs from 'fs' import { createRequire } from 'module' import path from 'path' -import { getPredefinedMigration } from 'payload' +import { getPredefinedMigration, writeMigrationIndex } from 'payload' import prompts from 'prompts' import { fileURLToPath } from 'url' @@ -112,5 +112,8 @@ export const createMigration: CreateMigration = async function createMigration( 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/index.ts b/packages/db-sqlite/src/index.ts index 2a1377033..a33757e09 100644 --- a/packages/db-sqlite/src/index.ts +++ b/packages/db-sqlite/src/index.ts @@ -93,6 +93,7 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj { localesSuffix: args.localesSuffix || '_locales', logger: args.logger, operators, + prodMigrations: args.prodMigrations, push: args.push, relations: {}, relationshipsSuffix: args.relationshipsSuffix || '_rels', diff --git a/packages/db-sqlite/src/types.ts b/packages/db-sqlite/src/types.ts index 8fad05453..754910bed 100644 --- a/packages/db-sqlite/src/types.ts +++ b/packages/db-sqlite/src/types.ts @@ -18,6 +18,11 @@ export type Args = { localesSuffix?: string logger?: DrizzleConfig['logger'] migrationDir?: string + prodMigrations?: { + down: (args: MigrateDownArgs) => Promise + name: string + up: (args: MigrateUpArgs) => Promise + }[] push?: boolean relationshipsSuffix?: string schemaName?: string @@ -100,6 +105,11 @@ export type SQLiteAdapter = { localesSuffix?: string logger: DrizzleConfig['logger'] operators: Operators + prodMigrations?: { + down: (args: MigrateDownArgs) => Promise + name: string + up: (args: MigrateUpArgs) => Promise + }[] push: boolean rejectInitializing: () => void relations: Record @@ -139,6 +149,11 @@ declare module 'payload' { initializing: Promise localesSuffix?: string logger: DrizzleConfig['logger'] + prodMigrations?: { + down: (args: MigrateDownArgs) => Promise + name: string + up: (args: MigrateUpArgs) => Promise + }[] push: boolean rejectInitializing: () => void relationshipsSuffix?: string diff --git a/packages/drizzle/src/migrate.ts b/packages/drizzle/src/migrate.ts index e8ab834ad..7790cd7c6 100644 --- a/packages/drizzle/src/migrate.ts +++ b/packages/drizzle/src/migrate.ts @@ -1,4 +1,3 @@ - import type { Payload, PayloadRequest } from 'payload' import { commitTransaction, initTransaction, killTransaction, readMigrationFiles } from 'payload' @@ -9,9 +8,12 @@ import type { DrizzleAdapter, Migration } from './types.js' import { migrationTableExists } from './utilities/migrationTableExists.js' import { parseError } from './utilities/parseError.js' -export async function migrate(this: DrizzleAdapter): Promise { +export const migrate: DrizzleAdapter['migrate'] = async function migrate( + this: DrizzleAdapter, + args, +): Promise { const { payload } = this - const migrationFiles = await readMigrationFiles({ payload }) + const migrationFiles = args?.migrations || (await readMigrationFiles({ payload })) if (!migrationFiles.length) { payload.logger.info({ msg: 'No migrations to run.' }) @@ -64,7 +66,7 @@ export async function migrate(this: DrizzleAdapter): Promise { // If already ran, skip if (alreadyRan) { - continue + continue } await runMigrationFile(payload, migration, newBatch) diff --git a/packages/drizzle/src/migrateFresh.ts b/packages/drizzle/src/migrateFresh.ts index df9014611..00f4310f0 100644 --- a/packages/drizzle/src/migrateFresh.ts +++ b/packages/drizzle/src/migrateFresh.ts @@ -42,7 +42,7 @@ export async function migrateFresh( await this.dropDatabase({ adapter: this }) - const migrationFiles = (await readMigrationFiles({ payload })) as Migration[] + const migrationFiles = await readMigrationFiles({ payload }) payload.logger.debug({ msg: `Found ${migrationFiles.length} migration files.`, }) diff --git a/packages/drizzle/src/types.ts b/packages/drizzle/src/types.ts index e8e6ef22e..3a197b289 100644 --- a/packages/drizzle/src/types.ts +++ b/packages/drizzle/src/types.ts @@ -133,7 +133,7 @@ export type Migration = { db?: DrizzleTransaction | LibSQLDatabase> | PostgresDB payload: Payload req: PayloadRequest - }) => Promise + }) => Promise up: ({ db, payload, @@ -142,7 +142,7 @@ export type Migration = { db?: DrizzleTransaction | LibSQLDatabase | PostgresDB payload: Payload req: PayloadRequest - }) => Promise + }) => Promise } & MigrationData export type CreateJSONQueryArgs = { diff --git a/packages/payload/src/database/migrations/createMigration.ts b/packages/payload/src/database/migrations/createMigration.ts index 423141873..ceb0be45f 100644 --- a/packages/payload/src/database/migrations/createMigration.ts +++ b/packages/payload/src/database/migrations/createMigration.ts @@ -2,9 +2,10 @@ import fs from 'fs' import type { CreateMigration } from '../types.js' +import { writeMigrationIndex } from '../../index.js' import { migrationTemplate } from './migrationTemplate.js' -export const createMigration: CreateMigration = async function createMigration({ +export const createMigration: CreateMigration = function createMigration({ migrationName, payload, }) { @@ -23,5 +24,8 @@ export const createMigration: CreateMigration = async function createMigration({ const fileName = `${timestamp}_${formattedName}.ts` const filePath = `${dir}/${fileName}` fs.writeFileSync(filePath, migrationTemplate) + + writeMigrationIndex({ migrationsDir: payload.db.migrationDir }) + payload.logger.info({ msg: `Migration created at ${filePath}` }) } diff --git a/packages/payload/src/database/migrations/migrate.ts b/packages/payload/src/database/migrations/migrate.ts index 2bddddf3a..94d105155 100644 --- a/packages/payload/src/database/migrations/migrate.ts +++ b/packages/payload/src/database/migrations/migrate.ts @@ -7,9 +7,12 @@ import { killTransaction } from '../../utilities/killTransaction.js' import { getMigrations } from './getMigrations.js' import { readMigrationFiles } from './readMigrationFiles.js' -export async function migrate(this: BaseDatabaseAdapter): Promise { +export const migrate: BaseDatabaseAdapter['migrate'] = async function migrate( + this: BaseDatabaseAdapter, + args, +): Promise { const { payload } = this - const migrationFiles = await readMigrationFiles({ payload }) + const migrationFiles = args?.migrations || (await readMigrationFiles({ payload })) const { existingMigrations, latestBatch } = await getMigrations({ payload }) const newBatch = latestBatch + 1 diff --git a/packages/payload/src/database/migrations/readMigrationFiles.ts b/packages/payload/src/database/migrations/readMigrationFiles.ts index 3568ebd39..806f4d391 100644 --- a/packages/payload/src/database/migrations/readMigrationFiles.ts +++ b/packages/payload/src/database/migrations/readMigrationFiles.ts @@ -27,7 +27,7 @@ export const readMigrationFiles = async ({ .readdirSync(payload.db.migrationDir) .sort() .filter((f) => { - return f.endsWith('.ts') || f.endsWith('.js') + return (f.endsWith('.ts') || f.endsWith('.js')) && !f.includes('index.') }) .map((file) => { return path.resolve(payload.db.migrationDir, file) diff --git a/packages/payload/src/database/migrations/writeMigrationIndex.ts b/packages/payload/src/database/migrations/writeMigrationIndex.ts new file mode 100644 index 000000000..62644906e --- /dev/null +++ b/packages/payload/src/database/migrations/writeMigrationIndex.ts @@ -0,0 +1,45 @@ +import fs from 'fs' +import { getTsconfig } from 'get-tsconfig' +import path from 'path' + +// Function to get all migration files (TS or JS) excluding 'index' +const getMigrationFiles = (dir: string) => { + return fs + .readdirSync(dir) + .filter( + (file) => + (file.endsWith('.ts') || file.endsWith('.js')) && + file !== 'index.ts' && + file !== 'index.js', + ) + .sort() +} + +// Function to generate the index.ts content +const generateIndexContent = (files: string[]) => { + const tsconfig = getTsconfig() + const importExt = tsconfig?.config?.compilerOptions?.moduleResolution === 'NodeNext' ? '.js' : '' + + let imports = '' + let exportsArray = 'export const migrations = [\n' + + files.forEach((file, index) => { + const fileNameWithoutExt = file.replace(/\.[^/.]+$/, '') + imports += `import * as migration_${fileNameWithoutExt} from './${fileNameWithoutExt}${importExt}';\n` + exportsArray += ` { + up: migration_${fileNameWithoutExt}.up, + down: migration_${fileNameWithoutExt}.down, + name: '${fileNameWithoutExt}'${index !== files.length - 1 ? ',' : ''}\n },\n` + }) + + exportsArray += '];\n' + return imports + '\n' + exportsArray +} + +// Main function to create the index.ts file +export const writeMigrationIndex = (args: { migrationsDir: string }) => { + const migrationFiles = getMigrationFiles(args.migrationsDir) + const indexContent = generateIndexContent(migrationFiles) + + fs.writeFileSync(path.join(args.migrationsDir, 'index.ts'), indexContent) +} diff --git a/packages/payload/src/database/types.ts b/packages/payload/src/database/types.ts index b0a92ba56..3800d8182 100644 --- a/packages/payload/src/database/types.ts +++ b/packages/payload/src/database/types.ts @@ -44,7 +44,6 @@ export interface BaseDatabaseAdapter { deleteOne: DeleteOne deleteVersions: DeleteVersions - /** * Terminate the connection with the database */ @@ -68,7 +67,7 @@ export interface BaseDatabaseAdapter { /** * Run any migration up functions that have not yet been performed and update the status */ - migrate: () => Promise + migrate: (args?: { migrations?: Migration[] }) => Promise /** * Run any migration down functions that have been performed @@ -79,15 +78,16 @@ export interface BaseDatabaseAdapter { * Drop the current database and run all migrate up functions */ migrateFresh: (args: { forceAcceptWarning?: boolean }) => Promise + /** * Run all migration down functions before running up */ migrateRefresh: () => Promise - /** * Run all migrate down functions */ migrateReset: () => Promise + /** * Read the current state of migrations and output the result to show which have been run */ @@ -148,7 +148,7 @@ export type CreateMigration = (args: { forceAcceptWarning?: boolean migrationName?: string payload: Payload -}) => Promise +}) => Promise | void export type Transaction = ( callback: () => Promise, @@ -396,8 +396,8 @@ export type DeleteManyArgs = { export type DeleteMany = (args: DeleteManyArgs) => Promise export type Migration = { - down: ({ payload, req }: { payload: Payload; req: PayloadRequest }) => Promise - up: ({ payload, req }: { payload: Payload; req: PayloadRequest }) => Promise + down: (args: unknown) => Promise + up: (args: unknown) => Promise } & MigrationData export type MigrationData = { diff --git a/packages/payload/src/index.ts b/packages/payload/src/index.ts index 17c6c2594..5d6eb4efa 100644 --- a/packages/payload/src/index.ts +++ b/packages/payload/src/index.ts @@ -777,6 +777,7 @@ export { migrateStatus } from './database/migrations/migrateStatus.js' export { migrationTemplate } from './database/migrations/migrationTemplate.js' export { migrationsCollection } from './database/migrations/migrationsCollection.js' export { readMigrationFiles } from './database/migrations/readMigrationFiles.js' +export { writeMigrationIndex } from './database/migrations/writeMigrationIndex.js' export type * from './database/queryValidation/types.js' export type { EntityPolicies, PathToQuery } from './database/queryValidation/types.js' export { validateQueryPaths } from './database/queryValidation/validateQueryPaths.js'