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:
James Mikrut
2024-08-07 13:57:12 -04:00
committed by GitHub
parent e0699838e1
commit 1cb1e5e8b3
19 changed files with 143 additions and 21 deletions

View File

@@ -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)

View File

@@ -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,

View File

@@ -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 })
}
}

View File

@@ -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` })
}

View File

@@ -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',

View File

@@ -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

View File

@@ -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 })
}
}

View File

@@ -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` })
}

View File

@@ -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',

View File

@@ -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

View File

@@ -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.' })

View File

@@ -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.`,
})

View File

@@ -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 = {

View File

@@ -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}` })
}

View File

@@ -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

View File

@@ -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)

View 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)
}

View File

@@ -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 = {

View File

@@ -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'