feat(db-*): allows for running migrations in production automatically (#7563)
## Description Introduces a pattern for running migrations upon Payload init in production.
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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<void>
|
||||
name: string
|
||||
up: (args: MigrateUpArgs) => Promise<void>
|
||||
}[]
|
||||
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<void>
|
||||
name: string
|
||||
up: (args: MigrateUpArgs) => Promise<void>
|
||||
}[]
|
||||
sessions: Record<number | string, ClientSession>
|
||||
versions: {
|
||||
[slug: string]: CollectionModel
|
||||
@@ -107,6 +117,11 @@ declare module 'payload' {
|
||||
connection: Connection
|
||||
globals: GlobalModel
|
||||
mongoMemoryServer: MongoMemoryReplSet
|
||||
prodMigrations?: {
|
||||
down: (args: MigrateDownArgs) => Promise<void>
|
||||
name: string
|
||||
up: (args: MigrateUpArgs) => Promise<void>
|
||||
}[]
|
||||
sessions: Record<number | string, ClientSession>
|
||||
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,
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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` })
|
||||
}
|
||||
|
||||
@@ -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<PostgresAdapter>
|
||||
pgSchema: adapterSchema,
|
||||
pool: undefined,
|
||||
poolOptions: args.pool,
|
||||
prodMigrations: args.prodMigrations,
|
||||
push: args.push,
|
||||
relations: {},
|
||||
relationshipsSuffix: args.relationshipsSuffix || '_rels',
|
||||
|
||||
@@ -33,6 +33,11 @@ export type Args = {
|
||||
logger?: DrizzleConfig['logger']
|
||||
migrationDir?: string
|
||||
pool: PoolConfig
|
||||
prodMigrations?: {
|
||||
down: (args: MigrateDownArgs) => Promise<void>
|
||||
name: string
|
||||
up: (args: MigrateUpArgs) => Promise<void>
|
||||
}[]
|
||||
push?: boolean
|
||||
relationshipsSuffix?: string
|
||||
/**
|
||||
@@ -136,6 +141,11 @@ export type PostgresAdapter = {
|
||||
pgSchema?: Schema
|
||||
pool: Pool
|
||||
poolOptions: Args['pool']
|
||||
prodMigrations?: {
|
||||
down: (args: MigrateDownArgs) => Promise<void>
|
||||
name: string
|
||||
up: (args: MigrateUpArgs) => Promise<void>
|
||||
}[]
|
||||
push: boolean
|
||||
rejectInitializing: () => void
|
||||
relations: Record<string, GenericRelation>
|
||||
@@ -178,6 +188,11 @@ declare module 'payload' {
|
||||
pgSchema?: { table: PgTableFn } | PgSchema
|
||||
pool: Pool
|
||||
poolOptions: Args['pool']
|
||||
prodMigrations?: {
|
||||
down: (args: MigrateDownArgs) => Promise<void>
|
||||
name: string
|
||||
up: (args: MigrateUpArgs) => Promise<void>
|
||||
}[]
|
||||
push: boolean
|
||||
rejectInitializing: () => void
|
||||
relationshipsSuffix?: string
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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` })
|
||||
}
|
||||
|
||||
@@ -93,6 +93,7 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
|
||||
localesSuffix: args.localesSuffix || '_locales',
|
||||
logger: args.logger,
|
||||
operators,
|
||||
prodMigrations: args.prodMigrations,
|
||||
push: args.push,
|
||||
relations: {},
|
||||
relationshipsSuffix: args.relationshipsSuffix || '_rels',
|
||||
|
||||
@@ -18,6 +18,11 @@ export type Args = {
|
||||
localesSuffix?: string
|
||||
logger?: DrizzleConfig['logger']
|
||||
migrationDir?: string
|
||||
prodMigrations?: {
|
||||
down: (args: MigrateDownArgs) => Promise<void>
|
||||
name: string
|
||||
up: (args: MigrateUpArgs) => Promise<void>
|
||||
}[]
|
||||
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<void>
|
||||
name: string
|
||||
up: (args: MigrateUpArgs) => Promise<void>
|
||||
}[]
|
||||
push: boolean
|
||||
rejectInitializing: () => void
|
||||
relations: Record<string, GenericRelation>
|
||||
@@ -139,6 +149,11 @@ declare module 'payload' {
|
||||
initializing: Promise<void>
|
||||
localesSuffix?: string
|
||||
logger: DrizzleConfig['logger']
|
||||
prodMigrations?: {
|
||||
down: (args: MigrateDownArgs) => Promise<void>
|
||||
name: string
|
||||
up: (args: MigrateUpArgs) => Promise<void>
|
||||
}[]
|
||||
push: boolean
|
||||
rejectInitializing: () => void
|
||||
relationshipsSuffix?: string
|
||||
|
||||
@@ -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<void> {
|
||||
export const migrate: DrizzleAdapter['migrate'] = async function migrate(
|
||||
this: DrizzleAdapter,
|
||||
args,
|
||||
): Promise<void> {
|
||||
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<void> {
|
||||
|
||||
// If already ran, skip
|
||||
if (alreadyRan) {
|
||||
continue
|
||||
continue
|
||||
}
|
||||
|
||||
await runMigrationFile(payload, migration, newBatch)
|
||||
|
||||
@@ -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.`,
|
||||
})
|
||||
|
||||
@@ -133,7 +133,7 @@ export type Migration = {
|
||||
db?: DrizzleTransaction | LibSQLDatabase<Record<string, never>> | PostgresDB
|
||||
payload: Payload
|
||||
req: PayloadRequest
|
||||
}) => Promise<boolean>
|
||||
}) => Promise<void>
|
||||
up: ({
|
||||
db,
|
||||
payload,
|
||||
@@ -142,7 +142,7 @@ export type Migration = {
|
||||
db?: DrizzleTransaction | LibSQLDatabase | PostgresDB
|
||||
payload: Payload
|
||||
req: PayloadRequest
|
||||
}) => Promise<boolean>
|
||||
}) => Promise<void>
|
||||
} & MigrationData
|
||||
|
||||
export type CreateJSONQueryArgs = {
|
||||
|
||||
@@ -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}` })
|
||||
}
|
||||
|
||||
@@ -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<void> {
|
||||
export const migrate: BaseDatabaseAdapter['migrate'] = async function migrate(
|
||||
this: BaseDatabaseAdapter,
|
||||
args,
|
||||
): Promise<void> {
|
||||
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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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<void>
|
||||
migrate: (args?: { migrations?: Migration[] }) => Promise<void>
|
||||
|
||||
/**
|
||||
* 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<void>
|
||||
|
||||
/**
|
||||
* Run all migration down functions before running up
|
||||
*/
|
||||
migrateRefresh: () => Promise<void>
|
||||
|
||||
/**
|
||||
* Run all migrate down functions
|
||||
*/
|
||||
migrateReset: () => Promise<void>
|
||||
|
||||
/**
|
||||
* 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<void>
|
||||
}) => Promise<void> | void
|
||||
|
||||
export type Transaction = (
|
||||
callback: () => Promise<void>,
|
||||
@@ -396,8 +396,8 @@ export type DeleteManyArgs = {
|
||||
export type DeleteMany = (args: DeleteManyArgs) => Promise<void>
|
||||
|
||||
export type Migration = {
|
||||
down: ({ payload, req }: { payload: Payload; req: PayloadRequest }) => Promise<boolean>
|
||||
up: ({ payload, req }: { payload: Payload; req: PayloadRequest }) => Promise<boolean>
|
||||
down: (args: unknown) => Promise<void>
|
||||
up: (args: unknown) => Promise<void>
|
||||
} & MigrationData
|
||||
|
||||
export type MigrationData = {
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user