diff --git a/packages/cli/medusa-cli/src/create-cli.ts b/packages/cli/medusa-cli/src/create-cli.ts index bb29bf5cf2..9022c50106 100644 --- a/packages/cli/medusa-cli/src/create-cli.ts +++ b/packages/cli/medusa-cli/src/create-cli.ts @@ -170,13 +170,18 @@ function buildLocalCommands(cli, isLocalProject) { ), }) .command({ - command: `migrations [action]`, + command: `migrations [action] [modules...]`, desc: `Manage migrations from the core and your own project`, builder: { action: { demand: true, + description: "The action to perform on migrations", choices: ["run", "revert", "show"], }, + modules: { + description: "Revert migrations for defined modules", + demand: false, + }, }, handler: handlerP( getCommandHandler(`migrate`, (args, cmd) => { 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 cc4f9f7618..11d35640c8 100644 --- a/packages/core/modules-sdk/src/loaders/utils/load-internal.ts +++ b/packages/core/modules-sdk/src/loaders/utils/load-internal.ts @@ -180,7 +180,6 @@ export async function loadModuleMigrations( let runMigrations = loadedModule.runMigrations let revertMigration = loadedModule.revertMigration - // Generate migration scripts if they are not present if (!runMigrations || !revertMigration) { const moduleResources = await loadResources( resolution, @@ -190,8 +189,7 @@ export async function loadModuleMigrations( const migrationScriptOptions = { moduleName: resolution.definition.key, - models: moduleResources.models, - pathToMigrations: moduleResources.normalizedPath + "/migrations", + pathToMigrations: join(moduleResources.normalizedPath, "migrations"), } runMigrations ??= ModulesSdkUtils.buildMigrationScript( diff --git a/packages/core/modules-sdk/src/medusa-app.ts b/packages/core/modules-sdk/src/medusa-app.ts index 535782b6b1..bd68bd9305 100644 --- a/packages/core/modules-sdk/src/medusa-app.ts +++ b/packages/core/modules-sdk/src/medusa-app.ts @@ -8,6 +8,7 @@ import type { LoadedModule, Logger, MedusaContainer, + ModuleBootstrapDeclaration, ModuleDefinition, ModuleExports, ModuleJoinerConfig, @@ -21,6 +22,7 @@ import { createMedusaContainer, isObject, isString, + MedusaError, ModuleRegistrationName, Modules, ModulesSdkUtils, @@ -47,10 +49,8 @@ declare module "@medusajs/types" { } } -export type RunMigrationFn = ( - options?: ModuleServiceInitializeOptions, - injectedDependencies?: Record -) => Promise +export type RunMigrationFn = () => Promise +export type RevertMigrationFn = (moduleNames: string[]) => Promise export type MedusaModuleConfig = { [key: string | Modules]: @@ -176,11 +176,11 @@ async function initializeLinks({ } } catch (err) { console.warn("Error initializing link modules.", err) - return { remoteLink: undefined, linkResolution: undefined, - runMigrations: undefined, + runMigrations: () => void 0, + revertMigrations: () => void 0, } } } @@ -224,7 +224,7 @@ export type MedusaAppOutput = { entitiesMap?: Record notFound?: Record> runMigrations: RunMigrationFn - revertMigrations: RunMigrationFn + revertMigrations: RevertMigrationFn onApplicationShutdown: () => Promise onApplicationPrepareShutdown: () => Promise sharedContainer?: MedusaContainer @@ -317,10 +317,12 @@ async function MedusaApp_({ delete modules[LinkModulePackage] delete modules[Modules.LINK] - let linkModuleOptions = {} + let linkModuleOrOptions: + | Partial + | Partial = {} if (isObject(linkModule)) { - linkModuleOptions = linkModule + linkModuleOrOptions = linkModule } for (const injectedDependency of Object.keys(injectedDependencies)) { @@ -380,7 +382,7 @@ async function MedusaApp_({ runMigrations: linkModuleMigration, revertMigrations: revertLinkModuleMigration, } = await initializeLinks({ - config: linkModuleOptions, + config: linkModuleOrOptions, linkModules, injectedDependencies, moduleExports: isMedusaModule(linkModule) ? linkModule : undefined, @@ -402,10 +404,38 @@ async function MedusaApp_({ return await remoteQuery.query(query, variables, options) } - const applyMigration = async (linkModuleOptions, revert = false) => { - for (const moduleName of Object.keys(allModules)) { - const moduleResolution = MedusaModule.getModuleResolutions(moduleName) + const applyMigration = async ({ + modulesNames, + revert = false, + }: { + modulesNames: string[] + revert?: boolean + }) => { + const moduleResolutions = modulesNames.map((moduleName) => { + return { + moduleName, + resolution: MedusaModule.getModuleResolutions(moduleName), + } + }) + const missingModules = moduleResolutions + .filter(({ resolution }) => !resolution) + .map(({ moduleName }) => moduleName) + + if (missingModules.length) { + const action = revert ? "revert" : "run" + const error = new MedusaError( + MedusaError.Types.UNKNOWN_MODULES, + `Cannot ${action} migrations for unknown module(s) ${missingModules.join( + "," + )}`, + MedusaError.Codes.UNKNOWN_MODULES + ) + error["allModules"] = Object.keys(allModules) + throw error + } + + for (const { resolution: moduleResolution } of moduleResolutions) { if (!moduleResolution.options?.database) { moduleResolution.options ??= {} moduleResolution.options.database = { @@ -417,6 +447,7 @@ async function MedusaApp_({ await MedusaModule.migrateDown( moduleResolution.definition.key, moduleResolution.resolutionPath as string, + sharedContainer, moduleResolution.options, moduleResolution.moduleExports ) @@ -424,48 +455,65 @@ async function MedusaApp_({ await MedusaModule.migrateUp( moduleResolution.definition.key, moduleResolution.resolutionPath as string, + sharedContainer, moduleResolution.options, moduleResolution.moduleExports ) } } - - const linkModuleOpt = { ...(linkModuleOptions ?? {}) } - linkModuleOpt.database ??= { - ...(sharedResourcesConfig?.database ?? {}), - } - - if (revert) { - revertLinkModuleMigration && - (await revertLinkModuleMigration( - { - options: linkModuleOpt, - injectedDependencies, - }, - linkModules - )) - } else { - linkModuleMigration && - (await linkModuleMigration( - { - options: linkModuleOpt, - injectedDependencies, - }, - linkModules - )) - } } - const runMigrations: RunMigrationFn = async ( - linkModuleOptions - ): Promise => { - await applyMigration(linkModuleOptions) + const runMigrations: RunMigrationFn = async (): Promise => { + await applyMigration({ + modulesNames: Object.keys(allModules), + }) + + const options: Partial = + "scope" in linkModuleOrOptions + ? { ...linkModuleOrOptions.options } + : { + ...(linkModuleOrOptions as Partial), + } + + options.database ??= { + ...sharedResourcesConfig?.database, + } + + await linkModuleMigration( + { + options, + injectedDependencies, + }, + linkModules + ) } - const revertMigrations: RunMigrationFn = async ( - linkModuleOptions + const revertMigrations: RevertMigrationFn = async ( + modulesNames ): Promise => { - await applyMigration(linkModuleOptions, true) + await applyMigration({ + modulesNames, + revert: true, + }) + + const options: Partial = + "scope" in linkModuleOrOptions + ? { ...linkModuleOrOptions.options } + : { + ...(linkModuleOrOptions as Partial), + } + + options.database ??= { + ...sharedResourcesConfig?.database, + } + + await revertLinkModuleMigration( + { + options, + injectedDependencies, + }, + linkModules + ) } return { @@ -506,6 +554,7 @@ export async function MedusaAppMigrateUp( } export async function MedusaAppMigrateDown( + moduleNames: string[], options: MedusaAppOptions = {} ): Promise { const migrationOnly = true @@ -515,5 +564,5 @@ export async function MedusaAppMigrateDown( migrationOnly, }) - await revertMigrations().finally(MedusaModule.clearInstances) + await revertMigrations(moduleNames).finally(MedusaModule.clearInstances) } diff --git a/packages/core/modules-sdk/src/medusa-module.ts b/packages/core/modules-sdk/src/medusa-module.ts index 8d0be67e6f..bac1fc147e 100644 --- a/packages/core/modules-sdk/src/medusa-module.ts +++ b/packages/core/modules-sdk/src/medusa-module.ts @@ -12,6 +12,7 @@ import { ModuleResolution, } from "@medusajs/types" import { + ContainerRegistrationKeys, createMedusaContainer, promiseAll, simpleHash, @@ -344,7 +345,9 @@ class MedusaModule { ) const logger_ = - container.resolve("logger", { allowUnregistered: true }) ?? logger + container.resolve(ContainerRegistrationKeys.LOGGER, { + allowUnregistered: true, + }) ?? logger try { await moduleLoader({ @@ -475,7 +478,9 @@ class MedusaModule { ) const logger_ = - container.resolve("logger", { allowUnregistered: true }) ?? logger + container.resolve(ContainerRegistrationKeys.LOGGER, { + allowUnregistered: true, + }) ?? logger try { await moduleLoader({ @@ -534,6 +539,7 @@ class MedusaModule { public static async migrateUp( moduleKey: string, modulePath: string, + container?: MedusaContainer, options?: Record, moduleExports?: ModuleExports ): Promise { @@ -544,6 +550,11 @@ class MedusaModule { options, }) + const logger_ = + container?.resolve(ContainerRegistrationKeys.LOGGER, { + allowUnregistered: true, + }) ?? logger + for (const mod in moduleResolutions) { const [migrateUp] = await loadModuleMigrations( moduleResolutions[mod], @@ -553,7 +564,7 @@ class MedusaModule { if (typeof migrateUp === "function") { await migrateUp({ options, - logger, + logger: logger_, }) } } @@ -562,6 +573,7 @@ class MedusaModule { public static async migrateDown( moduleKey: string, modulePath: string, + container?: MedusaContainer, options?: Record, moduleExports?: ModuleExports ): Promise { @@ -572,6 +584,11 @@ class MedusaModule { options, }) + const logger_ = + container?.resolve(ContainerRegistrationKeys.LOGGER, { + allowUnregistered: true, + }) ?? logger + for (const mod in moduleResolutions) { const [, migrateDown] = await loadModuleMigrations( moduleResolutions[mod], @@ -581,7 +598,7 @@ class MedusaModule { if (typeof migrateDown === "function") { await migrateDown({ options, - logger, + logger: logger_, }) } } diff --git a/packages/core/utils/src/common/errors.ts b/packages/core/utils/src/common/errors.ts index 0dc1b3a646..31c50ddb6b 100644 --- a/packages/core/utils/src/common/errors.ts +++ b/packages/core/utils/src/common/errors.ts @@ -13,6 +13,7 @@ export const MedusaErrorTypes = { NOT_ALLOWED: "not_allowed", UNEXPECTED_STATE: "unexpected_state", CONFLICT: "conflict", + UNKNOWN_MODULES: "unknown_modules", PAYMENT_AUTHORIZATION_ERROR: "payment_authorization_error", PAYMENT_REQUIRES_MORE_ERROR: "payment_requires_more_error", } @@ -20,6 +21,7 @@ export const MedusaErrorTypes = { export const MedusaErrorCodes = { INSUFFICIENT_INVENTORY: "insufficient_inventory", CART_INCOMPATIBLE_STATE: "cart_incompatible_state", + UNKNOWN_MODULES: "unknown_modules", } /** diff --git a/packages/core/utils/src/migrations/index.ts b/packages/core/utils/src/migrations/index.ts index d8112ab404..3affd373af 100644 --- a/packages/core/utils/src/migrations/index.ts +++ b/packages/core/utils/src/migrations/index.ts @@ -5,6 +5,7 @@ import { UmzugMigration, } from "@mikro-orm/migrations" import { MikroORM, MikroORMOptions } from "@mikro-orm/core" +import { PostgreSqlDriver } from "@mikro-orm/postgresql" /** * Events emitted by the migrations class @@ -20,11 +21,13 @@ export type MigrationsEvents = { * Exposes the API to programmatically manage Mikro ORM migrations */ export class Migrations extends EventEmitter { - #config: Partial + #configOrConnection: Partial | MikroORM - constructor(config: Partial) { + constructor( + configOrConnection: Partial | MikroORM + ) { super() - this.#config = config + this.#configOrConnection = configOrConnection } /** @@ -32,10 +35,14 @@ export class Migrations extends EventEmitter { * one */ async #getConnection() { + if ("connect" in this.#configOrConnection) { + return this.#configOrConnection as MikroORM + } + return await MikroORM.init({ - ...this.#config, + ...this.#configOrConnection, migrations: { - ...this.#config.migrations, + ...this.#configOrConnection.migrations, silent: true, }, }) 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 cd2bbbb12f..9663643c55 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 @@ -1,23 +1,17 @@ import { LoaderOptions, Logger, ModulesSdkTypes } from "@medusajs/types" - -import { EntitySchema } from "@mikro-orm/core" -import { upperCaseFirst } from "../../common" import { mikroOrmCreateConnection } from "../../dal" -import { DmlEntity, toMikroORMEntity } from "../../dml" import { loadDatabaseConfig } from "../load-module-database-config" +import { Migrations } from "../../migrations" + +const TERMINAL_SIZE = process.stdout.columns /** * Utility function to build a migration script that will revert the migrations. * Only used in mikro orm based modules. * @param moduleName - * @param models * @param pathToMigrations */ -export function buildRevertMigrationScript({ - moduleName, - models, - pathToMigrations, -}) { +export function buildRevertMigrationScript({ moduleName, pathToMigrations }) { /** * This script is only valid for mikro orm managers. If a user provide a custom manager * he is in charge of reverting the migrations. @@ -34,34 +28,30 @@ export function buildRevertMigrationScript({ > = {}) { logger ??= console as unknown as Logger + console.log(new Array(TERMINAL_SIZE).join("-")) + console.log("") + logger.info(`MODULE: ${moduleName}`) + const dbData = loadDatabaseConfig(moduleName, options)! - const entities = Object.values(models).map((model) => { - if (DmlEntity.isDmlEntity(model)) { - return toMikroORMEntity(model) - } + const orm = await mikroOrmCreateConnection(dbData, [], pathToMigrations) + const migrations = new Migrations(orm) - return model - }) as unknown as EntitySchema[] - - const orm = await mikroOrmCreateConnection( - dbData, - entities, - pathToMigrations - ) + migrations.on("reverting", (migration) => { + logger.info(` ● Reverting ${migration.name}`) + }) + migrations.on("reverted", (migration) => { + logger.info(` ✔ Reverted ${migration.name}`) + }) try { - const migrator = orm.getMigrator() - await migrator.down() - - logger?.info(`${upperCaseFirst(moduleName)} module migration executed`) + const result = await migrations.revert() + if (result.length) { + logger.info("Reverted successfully") + } else { + logger.info("Skipped. Nothing to revert") + } } catch (error) { - logger?.error( - `${upperCaseFirst( - moduleName - )} module migration failed to run - Error: ${error.errros ?? error}` - ) + logger.error(`Failed with error ${error.message}`, error) } - - await orm.close() } } 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 5ef1f9e7bb..3a041cc9b8 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 @@ -1,18 +1,17 @@ import { LoaderOptions, Logger, ModulesSdkTypes } from "@medusajs/types" -import { EntitySchema } from "@mikro-orm/core" -import { upperCaseFirst } from "../../common" import { mikroOrmCreateConnection } from "../../dal" -import { DmlEntity, toMikroORMEntity } from "../../dml" import { loadDatabaseConfig } from "../load-module-database-config" +import { Migrations } from "../../migrations" + +const TERMINAL_SIZE = process.stdout.columns /** * Utility function to build a migration script that will run the migrations. * Only used in mikro orm based modules. * @param moduleName - * @param models * @param pathToMigrations */ -export function buildMigrationScript({ moduleName, models, pathToMigrations }) { +export function buildMigrationScript({ moduleName, pathToMigrations }) { /** * This script is only valid for mikro orm managers. If a user provide a custom manager * he is in charge of running the migrations. @@ -29,48 +28,30 @@ export function buildMigrationScript({ moduleName, models, pathToMigrations }) { > = {}) { logger ??= console as unknown as Logger + console.log(new Array(TERMINAL_SIZE).join("-")) + console.log("") + logger.info(`MODULE: ${moduleName}`) + const dbData = loadDatabaseConfig(moduleName, options)! - const entities = Object.values(models).map((model) => { - if (DmlEntity.isDmlEntity(model)) { - return toMikroORMEntity(model) - } + const orm = await mikroOrmCreateConnection(dbData, [], pathToMigrations) + const migrations = new Migrations(orm) - return model - }) as unknown as EntitySchema[] - - const orm = await mikroOrmCreateConnection( - dbData, - entities, - pathToMigrations - ) + migrations.on("migrating", (migration) => { + logger.info(` ● Migrating ${migration.name}`) + }) + migrations.on("migrated", (migration) => { + logger.info(` ✔ Migrated ${migration.name}`) + }) try { - const migrator = orm.getMigrator() - const pendingMigrations = await migrator.getPendingMigrations() - - if (pendingMigrations.length) { - logger.info( - `Pending migrations: ${JSON.stringify(pendingMigrations, null, 2)}` - ) - - await migrator.up({ - migrations: pendingMigrations.map((m) => m.name), - }) - - logger.info( - `${upperCaseFirst(moduleName)} module: ${ - pendingMigrations.length - } migration files executed` - ) + const result = await migrations.run() + if (result.length) { + logger.info("Completed successfully") + } else { + logger.info("Skipped. Database is upto-date") } } catch (error) { - logger.error( - `${upperCaseFirst( - moduleName - )} module migration failed to run - Error: ${error.errros ?? error}` - ) + logger.error(`Failed with error ${error.message}`, error) } - - await orm.close() } } diff --git a/packages/medusa/src/commands/migrate.ts b/packages/medusa/src/commands/migrate.ts index aa5752fa5b..c8cb131b41 100644 --- a/packages/medusa/src/commands/migrate.ts +++ b/packages/medusa/src/commands/migrate.ts @@ -1,16 +1,19 @@ import Logger from "../loaders/logger" import { migrateMedusaApp, revertMedusaApp } from "../loaders/medusa-app" import { initializeContainer } from "../loaders" -import { ContainerRegistrationKeys } from "@medusajs/utils" +import { ContainerRegistrationKeys, MedusaError } from "@medusajs/utils" import { getResolvedPlugins } from "../loaders/helpers/resolve-plugins" import { resolvePluginsLinks } from "../loaders/helpers/resolve-plugins-links" +const TERMINAL_SIZE = process.stdout.columns + const main = async function ({ directory }) { const args = process.argv args.shift() args.shift() args.shift() + const action = args[0] const container = await initializeContainer(directory) const configModule = container.resolve( @@ -20,21 +23,55 @@ const main = async function ({ directory }) { const plugins = getResolvedPlugins(directory, configModule, true) || [] const pluginLinks = await resolvePluginsLinks(plugins, container) - if (args[0] === "run") { + if (action === "run") { + Logger.info("Running migrations...") + await migrateMedusaApp({ configModule, linkModules: pluginLinks, container, }) - Logger.info("Migrations completed.") + console.log(new Array(TERMINAL_SIZE).join("-")) + Logger.info("Migrations completed") process.exit() - } else if (args[0] === "revert") { - await revertMedusaApp({ configModule, linkModules: pluginLinks, container }) + } else if (action === "revert") { + const modulesToRevert = args.slice(1) + if (!modulesToRevert.length) { + Logger.error( + "Please provide the modules for which you want to revert migrations" + ) + Logger.error(`For example: "npx medusa migration revert "`) + process.exit(1) + } - Logger.info("Migrations reverted.") - } else if (args[0] === "show") { - Logger.info("not supported") + Logger.info("Reverting migrations...") + + try { + await revertMedusaApp({ + modulesToRevert, + configModule, + linkModules: pluginLinks, + container, + }) + console.log(new Array(TERMINAL_SIZE).join("-")) + Logger.info("Migrations reverted") + process.exit() + } catch (error) { + console.log(new Array(TERMINAL_SIZE).join("-")) + if (error.code && error.code === MedusaError.Codes.UNKNOWN_MODULES) { + Logger.error(error.message) + const modulesList = error.allModules.map( + (name: string) => ` - ${name}` + ) + Logger.error(`Available modules:\n${modulesList.join("\n")}`) + } else { + Logger.error(error.message, error) + } + process.exit(1) + } + } else if (action === "show") { + Logger.info("Action not supported yet") process.exit(0) } } diff --git a/packages/medusa/src/loaders/medusa-app.ts b/packages/medusa/src/loaders/medusa-app.ts index 0bb78812ed..06e661b0bc 100644 --- a/packages/medusa/src/loaders/medusa-app.ts +++ b/packages/medusa/src/loaders/medusa-app.ts @@ -60,6 +60,7 @@ export function mergeDefaultModules( async function runMedusaAppMigrations({ configModule, container, + moduleNames, revert = false, linkModules, }: { @@ -69,8 +70,16 @@ async function runMedusaAppMigrations({ } linkModules?: MedusaAppOptions["linkModules"] container: MedusaContainer - revert?: boolean -}): Promise { +} & ( + | { + moduleNames?: never + revert: false + } + | { + moduleNames: string[] + revert: true + } +)): Promise { const injectedDependencies = { [ContainerRegistrationKeys.PG_CONNECTION]: container.resolve( ContainerRegistrationKeys.PG_CONNECTION @@ -93,7 +102,7 @@ async function runMedusaAppMigrations({ const configModules = mergeDefaultModules(configModule.modules) if (revert) { - await MedusaAppMigrateDown({ + await MedusaAppMigrateDown(moduleNames!, { modulesConfig: configModules, sharedContainer: container, linkModules, @@ -111,6 +120,12 @@ async function runMedusaAppMigrations({ } } +/** + * + * @param configModule The config module + * @param linkModules Custom links from the plugins + * @param container The medusa container + */ export async function migrateMedusaApp({ configModule, linkModules, @@ -127,14 +142,25 @@ export async function migrateMedusaApp({ configModule, container, linkModules, + revert: false, }) } +/** + * + * @param modulesToRevert An array of modules for which you want to revert + * migrations + * @param configModule The config module + * @param linkModules Custom links from the plugins + * @param container The medusa container + */ export async function revertMedusaApp({ + modulesToRevert, configModule, linkModules, container, }: { + modulesToRevert: string[] configModule: { modules?: CommonTypes.ConfigModule["modules"] projectConfig: CommonTypes.ConfigModule["projectConfig"] @@ -143,6 +169,7 @@ export async function revertMedusaApp({ linkModules?: MedusaAppOptions["linkModules"] }): Promise { await runMedusaAppMigrations({ + moduleNames: modulesToRevert, configModule, container, revert: true,