chore(): Accept an extra agument 'all-or-nothing' on the migrate command (#14262)

* chore(): Accept an extra agument 'all-or-nothing' on the migrate command

* Create rich-camels-brush.md

* chore(): Accept an extra agument 'all-or-nothing' on the migrate command

* chore(): Accept an extra agument 'all-or-nothing' on the migrate command

* chore(): Accept an extra agument 'all-or-nothing' on the migrate command

* chore(): fix broken down migrations

* chore(): update changeset
This commit is contained in:
Adrien de Peretti authored and GitHub committed 2025-12-10 09:23:41 +01:00
1 parent 9bcfb990bf
commit 356283c359
13 files changed
+131 -48

No files matched your search

+11
View File
@@ -0,0 +1,11 @@
---
"@medusajs/medusa": patch
"@medusajs/framework": patch
"@medusajs/modules-sdk": patch
"@medusajs/cli": patch
"@medusajs/product": patch
"@medusajs/customer": patch
"@medusajs/promotion": patch
---
chore(): Accept an extra agument 'all-or-nothing' on the migrate command
+11 -3
View File
@@ -1,5 +1,5 @@
import { sync as existsSync } from "fs-exists-cached"
import { setTelemetryEnabled } from "@medusajs/telemetry" import { setTelemetryEnabled } from "@medusajs/telemetry"
import { sync as existsSync } from "fs-exists-cached"
import path from "path" import path from "path"
import resolveCwd from "resolve-cwd" import resolveCwd from "resolve-cwd"
import { newStarter } from "./commands/new" import { newStarter } from "./commands/new"
@@ -200,6 +200,12 @@ function buildLocalCommands(cli, isLocalProject) {
type: "number", type: "number",
describe: "Number of concurrent migrations to run", describe: "Number of concurrent migrations to run",
}) })
builder.option("all-or-nothing", {
type: "boolean",
describe:
"If set, the command will fail if any migration fails and revert the migrations that were applied so far",
default: false,
})
}, },
handler: handlerP( handler: handlerP(
getCommandHandler("db/migrate", (args, cmd) => { getCommandHandler("db/migrate", (args, cmd) => {
@@ -431,12 +437,14 @@ function buildLocalCommands(cli, isLocalProject) {
.option("workers", { .option("workers", {
type: "string", type: "string",
default: "0", default: "0",
describe: "Number of worker processes in cluster mode or a percentage of cluster size (e.g., 25%).", describe:
"Number of worker processes in cluster mode or a percentage of cluster size (e.g., 25%).",
}) })
.option("servers", { .option("servers", {
type: "string", type: "string",
default: "0", default: "0",
describe: "Number of server processes in cluster mode or a percentage of cluster size (e.g., 25%).", describe:
"Number of server processes in cluster mode or a percentage of cluster size (e.g., 25%).",
}), }),
handler: handlerP( handler: handlerP(
getCommandHandler(`start`, (args, cmd) => { getCommandHandler(`start`, (args, cmd) => {
@@ -157,17 +157,15 @@ export class MedusaAppLoader {
* @param action * @param action
*/ */
async runModulesMigrations( async runModulesMigrations(
{ options:
moduleNames,
action = "run",
}:
| { | {
moduleNames?: never
action: "run" action: "run"
allOrNothing?: boolean
} }
| { | {
moduleNames: string[]
action: "revert" | "generate" action: "revert" | "generate"
moduleNames: string[]
allOrNothing?: never
} = { } = {
action: "run", action: "run",
} }
@@ -185,14 +183,15 @@ export class MedusaAppLoader {
injectedDependencies, injectedDependencies,
medusaConfigPath: this.#medusaConfigPath, medusaConfigPath: this.#medusaConfigPath,
cwd: this.#cwd, cwd: this.#cwd,
allOrNothing: options.allOrNothing,
} }
if (action === "revert") { if (options.action === "revert") {
await MedusaAppMigrateDown(moduleNames!, migrationOptions) await MedusaAppMigrateDown(options.moduleNames!, migrationOptions)
} else if (action === "run") { } else if (options.action === "run") {
await MedusaAppMigrateUp(migrationOptions) await MedusaAppMigrateUp(migrationOptions)
} else { } else if (options.action === "generate") {
await MedusaAppMigrateGenerate(moduleNames!, migrationOptions) await MedusaAppMigrateGenerate(options.moduleNames!, migrationOptions)
} }
} }
@@ -48,6 +48,14 @@ type ModuleResource = {
type MigrationFunction = ( type MigrationFunction = (
options: LoaderOptions<any>, options: LoaderOptions<any>,
moduleDeclaration?: InternalModuleDeclaration moduleDeclaration?: InternalModuleDeclaration
) => Promise<{ name: string; path: string }[]>
type RevertMigrationFunction = (
options: LoaderOptions<any> & { migrationNames?: string[] },
moduleDeclaration?: InternalModuleDeclaration
) => Promise<void>
type GenerateMigrationFunction = (
options: LoaderOptions<any>,
moduleDeclaration?: InternalModuleDeclaration
) => Promise<void> ) => Promise<void>
type ResolvedModule = ModuleExports & { type ResolvedModule = ModuleExports & {
@@ -390,8 +398,8 @@ export async function loadModuleMigrations(
moduleExports?: ModuleExports moduleExports?: ModuleExports
): Promise<{ ): Promise<{
runMigrations?: MigrationFunction runMigrations?: MigrationFunction
revertMigration?: MigrationFunction revertMigration?: RevertMigrationFunction
generateMigration?: MigrationFunction generateMigration?: GenerateMigrationFunction
}> { }> {
const runMigrationsFn: ((...args) => Promise<any>)[] = [] const runMigrationsFn: ((...args) => Promise<any>)[] = []
const revertMigrationFn: ((...args) => Promise<any>)[] = [] const revertMigrationFn: ((...args) => Promise<any>)[] = []
@@ -488,9 +496,12 @@ export async function loadModuleMigrations(
} }
const runMigrations = async (...args) => { const runMigrations = async (...args) => {
let result: { name: string; path: string }[] = []
for (const migration of runMigrationsFn.filter(Boolean)) { for (const migration of runMigrationsFn.filter(Boolean)) {
await migration.apply(migration, args) const res = await migration.apply(migration, args)
result.push(...res)
} }
return result
} }
const revertMigration = async (...args) => { const revertMigration = async (...args) => {
for (const migration of revertMigrationFn.filter(Boolean)) { for (const migration of revertMigrationFn.filter(Boolean)) {
+57 -9
View File
@@ -47,7 +47,9 @@ import { MODULE_SCOPE } from "./types"
const LinkModulePackage = MODULE_PACKAGE_NAMES[Modules.LINK] const LinkModulePackage = MODULE_PACKAGE_NAMES[Modules.LINK]
export type RunMigrationFn = () => Promise<void> export type RunMigrationFn = (options?: {
allOrNothing?: boolean
}) => Promise<void>
export type RevertMigrationFn = (moduleNames: string[]) => Promise<void> export type RevertMigrationFn = (moduleNames: string[]) => Promise<void>
export type GenerateMigrations = (moduleNames: string[]) => Promise<void> export type GenerateMigrations = (moduleNames: string[]) => Promise<void>
export type GetLinkExecutionPlanner = () => ILinkMigrationsPlanner export type GetLinkExecutionPlanner = () => ILinkMigrationsPlanner
@@ -498,10 +500,12 @@ async function MedusaApp_({
const applyMigration = async ({ const applyMigration = async ({
modulesNames, modulesNames,
action = "run", action = "run",
allOrNothing = false,
}: { }: {
modulesNames: string[] modulesNames: string[]
action?: "run" | "revert" | "generate" action?: "run" | "revert" | "generate"
}) => { allOrNothing?: boolean
}): Promise<{ name: string; path: string }[] | void> => {
const moduleResolutions = Array.from(new Set(modulesNames)).map( const moduleResolutions = Array.from(new Set(modulesNames)).map(
(moduleName) => { (moduleName) => {
return { return {
@@ -527,7 +531,11 @@ async function MedusaApp_({
throw error throw error
} }
const run = async ({ resolution: moduleResolution }) => { let executedResolutions: [any, string[]][] = [] // [moduleResolution, migration names[]]
const run = async (
{ resolution: moduleResolution },
migrationNames?: string[]
) => {
if ( if (
!moduleResolution.options?.database && !moduleResolution.options?.database &&
moduleResolution.moduleDeclaration?.scope === MODULE_SCOPE.INTERNAL moduleResolution.moduleDeclaration?.scope === MODULE_SCOPE.INTERNAL
@@ -550,24 +558,62 @@ async function MedusaApp_({
} }
if (action === "revert") { if (action === "revert") {
await MedusaModule.migrateDown(migrationOptions) await MedusaModule.migrateDown(migrationOptions, migrationNames)
} else if (action === "run") { } else if (action === "run") {
await MedusaModule.migrateUp(migrationOptions) const ranMigrationsResult = await MedusaModule.migrateUp(
migrationOptions
)
// Store for revert if anything goes wrong later
executedResolutions.push([
moduleResolution,
ranMigrationsResult?.map((r) => r.name) ?? [],
])
} else { } else {
await MedusaModule.migrateGenerate(migrationOptions) await MedusaModule.migrateGenerate(migrationOptions)
} }
} }
const concurrency = parseInt(process.env.DB_MIGRATION_CONCURRENCY ?? "1") const concurrency = parseInt(process.env.DB_MIGRATION_CONCURRENCY ?? "1")
await executeWithConcurrency( try {
const results = await executeWithConcurrency(
moduleResolutions.map((a) => () => run(a)), moduleResolutions.map((a) => () => run(a)),
concurrency concurrency
) )
const rejections = results.filter(
(result) => result.status === "rejected"
)
if (rejections.length) {
throw new Error(
`Some migrations failed to ${action}: ${rejections
.map((r) => r.reason)
.join(", ")}`
)
}
} catch (error) {
if (allOrNothing) {
action = "revert"
await executeWithConcurrency(
executedResolutions.map(
([resolution, migrationNames]) =>
() =>
run({ resolution }, migrationNames)
),
concurrency
)
}
throw error
}
} }
const runMigrations: RunMigrationFn = async (): Promise<void> => { const runMigrations: RunMigrationFn = async (
{ allOrNothing = false }: { allOrNothing?: boolean } = {
allOrNothing: false,
}
): Promise<void> => {
await applyMigration({ await applyMigration({
modulesNames: Object.keys(allModules), modulesNames: Object.keys(allModules),
allOrNothing,
}) })
} }
@@ -638,7 +684,7 @@ export async function MedusaApp(
} }
export async function MedusaAppMigrateUp( export async function MedusaAppMigrateUp(
options: MedusaAppOptions = {} options: MedusaAppOptions & { allOrNothing?: boolean } = {}
): Promise<void> { ): Promise<void> {
const migrationOnly = true const migrationOnly = true
@@ -647,7 +693,9 @@ export async function MedusaAppMigrateUp(
migrationOnly, migrationOnly,
}) })
await runMigrations().finally(MedusaModule.clearInstances) await runMigrations({ allOrNothing: options.allOrNothing }).finally(
MedusaModule.clearInstances
)
} }
export async function MedusaAppMigrateDown( export async function MedusaAppMigrateDown(
+13 -5
View File
@@ -828,7 +828,7 @@ class MedusaModule {
moduleKey, moduleKey,
modulePath, modulePath,
cwd, cwd,
}: MigrationOptions): Promise<void> { }: MigrationOptions): Promise<{ name: string; path: string }[]> {
const moduleResolutions = registerMedusaModule({ const moduleResolutions = registerMedusaModule({
moduleKey, moduleKey,
moduleDeclaration: { moduleDeclaration: {
@@ -846,6 +846,7 @@ class MedusaModule {
container ??= createMedusaContainer() container ??= createMedusaContainer()
let result: { name: string; path: string }[] = []
for (const mod in moduleResolutions) { for (const mod in moduleResolutions) {
const { runMigrations } = await loadModuleMigrations( const { runMigrations } = await loadModuleMigrations(
container, container,
@@ -854,23 +855,29 @@ class MedusaModule {
) )
if (typeof runMigrations === "function") { if (typeof runMigrations === "function") {
await runMigrations({ const res = await runMigrations({
options, options,
container: container!, container: container!,
logger: logger_, logger: logger_,
}) })
} result.push(...res)
} }
} }
public static async migrateDown({ return result
}
public static async migrateDown(
{
options, options,
container, container,
moduleExports, moduleExports,
moduleKey, moduleKey,
modulePath, modulePath,
cwd, cwd,
}: MigrationOptions): Promise<void> { }: MigrationOptions,
migrationNames?: string[]
): Promise<void> {
const moduleResolutions = registerMedusaModule({ const moduleResolutions = registerMedusaModule({
moduleKey, moduleKey,
moduleDeclaration: { moduleDeclaration: {
@@ -900,6 +907,7 @@ class MedusaModule {
options, options,
container: container!, container: container!,
logger: logger_, logger: logger_,
migrationNames,
}) })
} }
} }
+3 -2
View File
@@ -267,10 +267,11 @@ export type ModuleExports<T = Constructor<any>> = {
runMigrations?( runMigrations?(
options: LoaderOptions<any>, options: LoaderOptions<any>,
moduleDeclaration?: InternalModuleDeclaration moduleDeclaration?: InternalModuleDeclaration
): Promise<void> ): Promise<{ name: string; path: string }[]>
revertMigration?( revertMigration?(
options: LoaderOptions<any>, options: LoaderOptions<any>,
moduleDeclaration?: InternalModuleDeclaration moduleDeclaration?: InternalModuleDeclaration,
migrationNames?: string[]
): Promise<void> ): Promise<void>
generateMigration?( generateMigration?(
options: LoaderOptions<any>, options: LoaderOptions<any>,
@@ -23,10 +23,11 @@ export function buildRevertMigrationScript({ moduleName, pathToMigrations }) {
return async function ({ return async function ({
options, options,
logger, logger,
migrationNames,
}: Pick< }: Pick<
LoaderOptions<ModulesSdkTypes.ModuleServiceInitializeOptions>, LoaderOptions<ModulesSdkTypes.ModuleServiceInitializeOptions>,
"options" | "logger" "options" | "logger"
> = {}) { > & { migrationNames?: string[] }) {
logger ??= console as unknown as Logger logger ??= console as unknown as Logger
logger.info(new Array(TERMINAL_SIZE).join("-")) logger.info(new Array(TERMINAL_SIZE).join("-"))
@@ -48,7 +49,10 @@ export function buildRevertMigrationScript({ moduleName, pathToMigrations }) {
}) })
try { try {
const result = await migrations.revert() const revertOptions = migrationNames?.length
? { step: migrationNames.length }
: undefined
const result = await migrations.revert(revertOptions as any)
if (result.length) { if (result.length) {
logger.info("Reverted successfully") logger.info("Reverted successfully")
} else { } else {
@@ -51,6 +51,7 @@ export function buildMigrationScript({ moduleName, pathToMigrations }) {
} else { } else {
logger.info(`Skipped. Database is up-to-date for module.`) logger.info(`Skipped. Database is up-to-date for module.`)
} }
return result
} catch (error) { } catch (error) {
logger.error(`Failed with error ${error.message}`, error) logger.error(`Failed with error ${error.message}`, error)
throw new MedusaError(MedusaError.Types.DB_ERROR, error.message) throw new MedusaError(MedusaError.Types.DB_ERROR, error.message)
@@ -27,6 +27,7 @@ export async function migrate({
skipScripts, skipScripts,
executeAllLinks, executeAllLinks,
executeSafeLinks, executeSafeLinks,
allOrNothing,
concurrency, concurrency,
logger, logger,
container, container,
@@ -36,6 +37,7 @@ export async function migrate({
skipScripts: boolean skipScripts: boolean
executeAllLinks: boolean executeAllLinks: boolean
executeSafeLinks: boolean executeSafeLinks: boolean
allOrNothing?: boolean
concurrency?: number concurrency?: number
logger: Logger logger: Logger
container: MedusaContainer container: MedusaContainer
@@ -79,6 +81,7 @@ export async function migrate({
await medusaAppLoader.runModulesMigrations({ await medusaAppLoader.runModulesMigrations({
action: "run", action: "run",
allOrNothing,
}) })
logger.log(new Array(TERMINAL_SIZE).join("-")) logger.log(new Array(TERMINAL_SIZE).join("-"))
logger.info("Migrations completed") logger.info("Migrations completed")
@@ -127,6 +130,7 @@ const main = async function ({
executeAllLinks, executeAllLinks,
executeSafeLinks, executeSafeLinks,
concurrency, concurrency,
allOrNothing,
}) { }) {
process.env.MEDUSA_WORKER_MODE = "server" process.env.MEDUSA_WORKER_MODE = "server"
const container = await initializeContainer(directory) const container = await initializeContainer(directory)
@@ -140,6 +144,7 @@ const main = async function ({
executeAllLinks, executeAllLinks,
executeSafeLinks, executeSafeLinks,
concurrency, concurrency,
allOrNothing,
logger, logger,
container, container,
}) })
@@ -62,12 +62,6 @@ export class Migration20241211074630 extends Migration {
this.addSql( this.addSql(
'alter table if exists "customer_group_customer" drop column if exists "deleted_at";' 'alter table if exists "customer_group_customer" drop column if exists "deleted_at";'
) )
this.addSql(
'alter table if exists "customer_group_customer" add constraint "customer_group_customer_customer_group_id_foreign" foreign key ("customer_group_id") references "customer_group" ("id") on delete cascade;'
)
this.addSql(
'alter table if exists "customer_group_customer" add constraint "customer_group_customer_customer_id_foreign" foreign key ("customer_id") references "customer" ("id") on delete cascade;'
)
this.addSql( this.addSql(
'create index if not exists "IDX_customer_group_customer_group_id" on "customer_group_customer" ("customer_group_id");' 'create index if not exists "IDX_customer_group_customer_group_id" on "customer_group_customer" ("customer_group_id");'
) )
@@ -28,9 +28,6 @@ export class Migration20250910154539 extends Migration {
this.addSql(`drop index if exists "IDX_product_image_url";`) this.addSql(`drop index if exists "IDX_product_image_url";`)
this.addSql(`drop index if exists "IDX_product_image_rank";`) this.addSql(`drop index if exists "IDX_product_image_rank";`)
this.addSql(`drop index if exists "IDX_product_image_url_rank_product_id";`) this.addSql(`drop index if exists "IDX_product_image_url_rank_product_id";`)
this.addSql(
`alter table if exists "image" drop constraint if exists "image_pkey";`
)
this.addSql(`drop index if exists "IDX_product_image_rank_product_id";`) this.addSql(`drop index if exists "IDX_product_image_rank_product_id";`)
} }
} }
@@ -21,9 +21,5 @@ export class Migration20250226130616 extends Migration {
this.addSql( this.addSql(
`CREATE INDEX IF NOT EXISTS "IDX_promotion_code" ON "promotion" (code) WHERE deleted_at IS NULL;` `CREATE INDEX IF NOT EXISTS "IDX_promotion_code" ON "promotion" (code) WHERE deleted_at IS NULL;`
) )
this.addSql(
'alter table if exists "promotion" add constraint "IDX_promotion_code_unique" unique ("code");'
)
} }
} }