From 356283c359e5c30e0d31e6c6fd0fb8e16c025b78 Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Wed, 10 Dec 2025 09:23:41 +0100 Subject: [PATCH] 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 --- .changeset/rich-camels-brush.md | 11 +++ packages/cli/medusa-cli/src/create-cli.ts | 14 +++- .../core/framework/src/medusa-app-loader.ts | 21 +++--- .../src/loaders/utils/load-internal.ts | 17 ++++- packages/core/modules-sdk/src/medusa-app.ts | 72 +++++++++++++++---- .../core/modules-sdk/src/medusa-module.ts | 28 +++++--- packages/core/types/src/modules-sdk/index.ts | 5 +- .../migration-scripts/migration-down.ts | 8 ++- .../migration-scripts/migration-up.ts | 1 + packages/medusa/src/commands/db/migrate.ts | 5 ++ .../src/migrations/Migration20241211074630.ts | 6 -- .../src/migrations/Migration20250910154539.ts | 3 - .../src/migrations/Migration20250226130616.ts | 4 -- 13 files changed, 139 insertions(+), 56 deletions(-) create mode 100644 .changeset/rich-camels-brush.md diff --git a/.changeset/rich-camels-brush.md b/.changeset/rich-camels-brush.md new file mode 100644 index 0000000000..78a3d0e8ba --- /dev/null +++ b/.changeset/rich-camels-brush.md @@ -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 diff --git a/packages/cli/medusa-cli/src/create-cli.ts b/packages/cli/medusa-cli/src/create-cli.ts index ddee9be4f9..7cd573219c 100644 --- a/packages/cli/medusa-cli/src/create-cli.ts +++ b/packages/cli/medusa-cli/src/create-cli.ts @@ -1,5 +1,5 @@ -import { sync as existsSync } from "fs-exists-cached" import { setTelemetryEnabled } from "@medusajs/telemetry" +import { sync as existsSync } from "fs-exists-cached" import path from "path" import resolveCwd from "resolve-cwd" import { newStarter } from "./commands/new" @@ -200,6 +200,12 @@ function buildLocalCommands(cli, isLocalProject) { type: "number", 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( getCommandHandler("db/migrate", (args, cmd) => { @@ -431,12 +437,14 @@ function buildLocalCommands(cli, isLocalProject) { .option("workers", { type: "string", 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", { type: "string", 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( getCommandHandler(`start`, (args, cmd) => { diff --git a/packages/core/framework/src/medusa-app-loader.ts b/packages/core/framework/src/medusa-app-loader.ts index 9e28397708..8b5788efbb 100644 --- a/packages/core/framework/src/medusa-app-loader.ts +++ b/packages/core/framework/src/medusa-app-loader.ts @@ -157,17 +157,15 @@ export class MedusaAppLoader { * @param action */ async runModulesMigrations( - { - moduleNames, - action = "run", - }: + options: | { - moduleNames?: never action: "run" + allOrNothing?: boolean } | { - moduleNames: string[] action: "revert" | "generate" + moduleNames: string[] + allOrNothing?: never } = { action: "run", } @@ -185,14 +183,15 @@ export class MedusaAppLoader { injectedDependencies, medusaConfigPath: this.#medusaConfigPath, cwd: this.#cwd, + allOrNothing: options.allOrNothing, } - if (action === "revert") { - await MedusaAppMigrateDown(moduleNames!, migrationOptions) - } else if (action === "run") { + if (options.action === "revert") { + await MedusaAppMigrateDown(options.moduleNames!, migrationOptions) + } else if (options.action === "run") { await MedusaAppMigrateUp(migrationOptions) - } else { - await MedusaAppMigrateGenerate(moduleNames!, migrationOptions) + } else if (options.action === "generate") { + await MedusaAppMigrateGenerate(options.moduleNames!, migrationOptions) } } diff --git a/packages/core/modules-sdk/src/loaders/utils/load-internal.ts b/packages/core/modules-sdk/src/loaders/utils/load-internal.ts index f37d2c7f8a..4ea7dd197e 100644 --- a/packages/core/modules-sdk/src/loaders/utils/load-internal.ts +++ b/packages/core/modules-sdk/src/loaders/utils/load-internal.ts @@ -48,6 +48,14 @@ type ModuleResource = { type MigrationFunction = ( options: LoaderOptions, moduleDeclaration?: InternalModuleDeclaration +) => Promise<{ name: string; path: string }[]> +type RevertMigrationFunction = ( + options: LoaderOptions & { migrationNames?: string[] }, + moduleDeclaration?: InternalModuleDeclaration +) => Promise +type GenerateMigrationFunction = ( + options: LoaderOptions, + moduleDeclaration?: InternalModuleDeclaration ) => Promise type ResolvedModule = ModuleExports & { @@ -390,8 +398,8 @@ export async function loadModuleMigrations( moduleExports?: ModuleExports ): Promise<{ runMigrations?: MigrationFunction - revertMigration?: MigrationFunction - generateMigration?: MigrationFunction + revertMigration?: RevertMigrationFunction + generateMigration?: GenerateMigrationFunction }> { const runMigrationsFn: ((...args) => Promise)[] = [] const revertMigrationFn: ((...args) => Promise)[] = [] @@ -488,9 +496,12 @@ export async function loadModuleMigrations( } const runMigrations = async (...args) => { + let result: { name: string; path: string }[] = [] 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) => { for (const migration of revertMigrationFn.filter(Boolean)) { diff --git a/packages/core/modules-sdk/src/medusa-app.ts b/packages/core/modules-sdk/src/medusa-app.ts index 5ec57558b1..1092a55925 100644 --- a/packages/core/modules-sdk/src/medusa-app.ts +++ b/packages/core/modules-sdk/src/medusa-app.ts @@ -47,7 +47,9 @@ import { MODULE_SCOPE } from "./types" const LinkModulePackage = MODULE_PACKAGE_NAMES[Modules.LINK] -export type RunMigrationFn = () => Promise +export type RunMigrationFn = (options?: { + allOrNothing?: boolean +}) => Promise export type RevertMigrationFn = (moduleNames: string[]) => Promise export type GenerateMigrations = (moduleNames: string[]) => Promise export type GetLinkExecutionPlanner = () => ILinkMigrationsPlanner @@ -498,10 +500,12 @@ async function MedusaApp_({ const applyMigration = async ({ modulesNames, action = "run", + allOrNothing = false, }: { modulesNames: string[] action?: "run" | "revert" | "generate" - }) => { + allOrNothing?: boolean + }): Promise<{ name: string; path: string }[] | void> => { const moduleResolutions = Array.from(new Set(modulesNames)).map( (moduleName) => { return { @@ -527,7 +531,11 @@ async function MedusaApp_({ throw error } - const run = async ({ resolution: moduleResolution }) => { + let executedResolutions: [any, string[]][] = [] // [moduleResolution, migration names[]] + const run = async ( + { resolution: moduleResolution }, + migrationNames?: string[] + ) => { if ( !moduleResolution.options?.database && moduleResolution.moduleDeclaration?.scope === MODULE_SCOPE.INTERNAL @@ -550,24 +558,62 @@ async function MedusaApp_({ } if (action === "revert") { - await MedusaModule.migrateDown(migrationOptions) + await MedusaModule.migrateDown(migrationOptions, migrationNames) } 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 { await MedusaModule.migrateGenerate(migrationOptions) } } const concurrency = parseInt(process.env.DB_MIGRATION_CONCURRENCY ?? "1") - await executeWithConcurrency( - moduleResolutions.map((a) => () => run(a)), - concurrency - ) + try { + const results = await executeWithConcurrency( + moduleResolutions.map((a) => () => run(a)), + 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 => { + const runMigrations: RunMigrationFn = async ( + { allOrNothing = false }: { allOrNothing?: boolean } = { + allOrNothing: false, + } + ): Promise => { await applyMigration({ modulesNames: Object.keys(allModules), + allOrNothing, }) } @@ -638,7 +684,7 @@ export async function MedusaApp( } export async function MedusaAppMigrateUp( - options: MedusaAppOptions = {} + options: MedusaAppOptions & { allOrNothing?: boolean } = {} ): Promise { const migrationOnly = true @@ -647,7 +693,9 @@ export async function MedusaAppMigrateUp( migrationOnly, }) - await runMigrations().finally(MedusaModule.clearInstances) + await runMigrations({ allOrNothing: options.allOrNothing }).finally( + MedusaModule.clearInstances + ) } export async function MedusaAppMigrateDown( diff --git a/packages/core/modules-sdk/src/medusa-module.ts b/packages/core/modules-sdk/src/medusa-module.ts index abdd2147a1..cc8c2c68f9 100644 --- a/packages/core/modules-sdk/src/medusa-module.ts +++ b/packages/core/modules-sdk/src/medusa-module.ts @@ -828,7 +828,7 @@ class MedusaModule { moduleKey, modulePath, cwd, - }: MigrationOptions): Promise { + }: MigrationOptions): Promise<{ name: string; path: string }[]> { const moduleResolutions = registerMedusaModule({ moduleKey, moduleDeclaration: { @@ -846,6 +846,7 @@ class MedusaModule { container ??= createMedusaContainer() + let result: { name: string; path: string }[] = [] for (const mod in moduleResolutions) { const { runMigrations } = await loadModuleMigrations( container, @@ -854,23 +855,29 @@ class MedusaModule { ) if (typeof runMigrations === "function") { - await runMigrations({ + const res = await runMigrations({ options, container: container!, logger: logger_, }) + result.push(...res) } } + + return result } - public static async migrateDown({ - options, - container, - moduleExports, - moduleKey, - modulePath, - cwd, - }: MigrationOptions): Promise { + public static async migrateDown( + { + options, + container, + moduleExports, + moduleKey, + modulePath, + cwd, + }: MigrationOptions, + migrationNames?: string[] + ): Promise { const moduleResolutions = registerMedusaModule({ moduleKey, moduleDeclaration: { @@ -900,6 +907,7 @@ class MedusaModule { options, container: container!, logger: logger_, + migrationNames, }) } } diff --git a/packages/core/types/src/modules-sdk/index.ts b/packages/core/types/src/modules-sdk/index.ts index 3166a552d3..953788792a 100644 --- a/packages/core/types/src/modules-sdk/index.ts +++ b/packages/core/types/src/modules-sdk/index.ts @@ -267,10 +267,11 @@ export type ModuleExports> = { runMigrations?( options: LoaderOptions, moduleDeclaration?: InternalModuleDeclaration - ): Promise + ): Promise<{ name: string; path: string }[]> revertMigration?( options: LoaderOptions, - moduleDeclaration?: InternalModuleDeclaration + moduleDeclaration?: InternalModuleDeclaration, + migrationNames?: string[] ): Promise generateMigration?( options: LoaderOptions, diff --git a/packages/core/utils/src/modules-sdk/migration-scripts/migration-down.ts b/packages/core/utils/src/modules-sdk/migration-scripts/migration-down.ts index 35cb9c39d5..29bd5644e4 100644 --- a/packages/core/utils/src/modules-sdk/migration-scripts/migration-down.ts +++ b/packages/core/utils/src/modules-sdk/migration-scripts/migration-down.ts @@ -23,10 +23,11 @@ export function buildRevertMigrationScript({ moduleName, pathToMigrations }) { return async function ({ options, logger, + migrationNames, }: Pick< LoaderOptions, "options" | "logger" - > = {}) { + > & { migrationNames?: string[] }) { logger ??= console as unknown as Logger logger.info(new Array(TERMINAL_SIZE).join("-")) @@ -48,7 +49,10 @@ export function buildRevertMigrationScript({ moduleName, pathToMigrations }) { }) 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) { logger.info("Reverted successfully") } else { diff --git a/packages/core/utils/src/modules-sdk/migration-scripts/migration-up.ts b/packages/core/utils/src/modules-sdk/migration-scripts/migration-up.ts index e09af4c917..714f0ebc88 100644 --- a/packages/core/utils/src/modules-sdk/migration-scripts/migration-up.ts +++ b/packages/core/utils/src/modules-sdk/migration-scripts/migration-up.ts @@ -51,6 +51,7 @@ export function buildMigrationScript({ moduleName, pathToMigrations }) { } else { logger.info(`Skipped. Database is up-to-date for module.`) } + return result } catch (error) { logger.error(`Failed with error ${error.message}`, error) throw new MedusaError(MedusaError.Types.DB_ERROR, error.message) diff --git a/packages/medusa/src/commands/db/migrate.ts b/packages/medusa/src/commands/db/migrate.ts index 6a78770e91..b2b749e8b8 100644 --- a/packages/medusa/src/commands/db/migrate.ts +++ b/packages/medusa/src/commands/db/migrate.ts @@ -27,6 +27,7 @@ export async function migrate({ skipScripts, executeAllLinks, executeSafeLinks, + allOrNothing, concurrency, logger, container, @@ -36,6 +37,7 @@ export async function migrate({ skipScripts: boolean executeAllLinks: boolean executeSafeLinks: boolean + allOrNothing?: boolean concurrency?: number logger: Logger container: MedusaContainer @@ -79,6 +81,7 @@ export async function migrate({ await medusaAppLoader.runModulesMigrations({ action: "run", + allOrNothing, }) logger.log(new Array(TERMINAL_SIZE).join("-")) logger.info("Migrations completed") @@ -127,6 +130,7 @@ const main = async function ({ executeAllLinks, executeSafeLinks, concurrency, + allOrNothing, }) { process.env.MEDUSA_WORKER_MODE = "server" const container = await initializeContainer(directory) @@ -140,6 +144,7 @@ const main = async function ({ executeAllLinks, executeSafeLinks, concurrency, + allOrNothing, logger, container, }) diff --git a/packages/modules/customer/src/migrations/Migration20241211074630.ts b/packages/modules/customer/src/migrations/Migration20241211074630.ts index e0bb2ccd58..1aa591c2bf 100644 --- a/packages/modules/customer/src/migrations/Migration20241211074630.ts +++ b/packages/modules/customer/src/migrations/Migration20241211074630.ts @@ -62,12 +62,6 @@ export class Migration20241211074630 extends Migration { this.addSql( '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( 'create index if not exists "IDX_customer_group_customer_group_id" on "customer_group_customer" ("customer_group_id");' ) diff --git a/packages/modules/product/src/migrations/Migration20250910154539.ts b/packages/modules/product/src/migrations/Migration20250910154539.ts index f496cd103d..b5ce633545 100644 --- a/packages/modules/product/src/migrations/Migration20250910154539.ts +++ b/packages/modules/product/src/migrations/Migration20250910154539.ts @@ -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_rank";`) 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";`) } } diff --git a/packages/modules/promotion/src/migrations/Migration20250226130616.ts b/packages/modules/promotion/src/migrations/Migration20250226130616.ts index 41a38cf627..a8e4417323 100644 --- a/packages/modules/promotion/src/migrations/Migration20250226130616.ts +++ b/packages/modules/promotion/src/migrations/Migration20250226130616.ts @@ -21,9 +21,5 @@ export class Migration20250226130616 extends Migration { this.addSql( `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");' - ) } }