diff --git a/packages/cli/medusa-cli/src/create-cli.ts b/packages/cli/medusa-cli/src/create-cli.ts index 68831ad27f..e43be85783 100644 --- a/packages/cli/medusa-cli/src/create-cli.ts +++ b/packages/cli/medusa-cli/src/create-cli.ts @@ -142,6 +142,64 @@ function buildLocalCommands(cli, isLocalProject) { }) ), }) + .command({ + command: "db:migrate", + desc: "Migrate the database by executing pending migrations", + builder: (builder) => { + builder.option("skip-links", { + type: "boolean", + describe: "Do not sync links", + }) + builder.option("execute-all-links", { + type: "boolean", + describe: + "Skip prompts and execute all (including unsafe) actions from sync links", + }) + builder.option("execute-safe-links", { + type: "boolean", + describe: + "Skip prompts and execute only safe actions from sync links", + }) + }, + handler: handlerP( + getCommandHandler("db/migrate", (args, cmd) => { + process.env.NODE_ENV = process.env.NODE_ENV || `development` + return cmd(args) + }) + ), + }) + .command({ + command: "db:rollback [modules...]", + desc: "Rollback last batch of executed migrations for a given module", + builder: { + modules: { + description: "Modules for which to rollback migrations", + demand: true, + }, + }, + handler: handlerP( + getCommandHandler("db/rollback", (args, cmd) => { + process.env.NODE_ENV = process.env.NODE_ENV || `development` + return cmd(args) + }) + ), + }) + .command({ + command: "db:generate [modules...]", + desc: "Generate migrations for a given module", + builder: { + modules: { + description: "Modules for which to generate migration files", + demand: true, + }, + }, + handler: handlerP( + getCommandHandler("db/generate", (args, cmd) => { + process.env.NODE_ENV = process.env.NODE_ENV || `development` + return cmd(args) + }) + ), + }) .command({ command: "db:sync-links", desc: "Sync database schema with the links defined by your application and Medusa core", diff --git a/packages/medusa/src/commands/db/generate.ts b/packages/medusa/src/commands/db/generate.ts new file mode 100644 index 0000000000..c832f345bc --- /dev/null +++ b/packages/medusa/src/commands/db/generate.ts @@ -0,0 +1,59 @@ +import { join } from "path" +import { ContainerRegistrationKeys, MedusaError } from "@medusajs/utils" +import { LinkLoader, logger, MedusaAppLoader } from "@medusajs/framework" + +import { ensureDbExists } from "../utils" +import { initializeContainer } from "../../loaders" +import { getResolvedPlugins } from "../../loaders/helpers/resolve-plugins" + +const TERMINAL_SIZE = process.stdout.columns + +const main = async function ({ directory, modules }) { + try { + /** + * Setup + */ + const container = await initializeContainer(directory) + await ensureDbExists(container) + + const medusaAppLoader = new MedusaAppLoader() + const configModule = container.resolve( + ContainerRegistrationKeys.CONFIG_MODULE + ) + + const plugins = getResolvedPlugins(directory, configModule, true) || [] + const linksSourcePaths = plugins.map((plugin) => + join(plugin.resolve, "links") + ) + await new LinkLoader(linksSourcePaths).load() + + /** + * Generating migrations + */ + logger.info("Generating migrations...") + + await medusaAppLoader.runModulesMigrations({ + moduleNames: modules, + action: "generate", + }) + + console.log(new Array(TERMINAL_SIZE).join("-")) + logger.info("Migrations generated") + + 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) + } +} + +export default main diff --git a/packages/medusa/src/commands/db/migrate.ts b/packages/medusa/src/commands/db/migrate.ts new file mode 100644 index 0000000000..677af52eea --- /dev/null +++ b/packages/medusa/src/commands/db/migrate.ts @@ -0,0 +1,64 @@ +import { join } from "path" +import { ContainerRegistrationKeys } from "@medusajs/utils" +import { LinkLoader, logger, MedusaAppLoader } from "@medusajs/framework" + +import { syncLinks } from "./sync-links" +import { ensureDbExists } from "../utils" +import { initializeContainer } from "../../loaders" +import { getResolvedPlugins } from "../../loaders/helpers/resolve-plugins" + +const TERMINAL_SIZE = process.stdout.columns + +const main = async function ({ + directory, + skipLinks, + executeAllLinks, + executeSafeLinks, +}) { + try { + /** + * Setup + */ + const container = await initializeContainer(directory) + await ensureDbExists(container) + + const medusaAppLoader = new MedusaAppLoader() + const configModule = container.resolve( + ContainerRegistrationKeys.CONFIG_MODULE + ) + + const plugins = getResolvedPlugins(directory, configModule, true) || [] + const linksSourcePaths = plugins.map((plugin) => + join(plugin.resolve, "links") + ) + await new LinkLoader(linksSourcePaths).load() + + /** + * Run migrations + */ + logger.info("Running migrations...") + await medusaAppLoader.runModulesMigrations({ + action: "run", + }) + console.log(new Array(TERMINAL_SIZE).join("-")) + logger.info("Migrations completed") + + /** + * Sync links + */ + if (!skipLinks) { + console.log(new Array(TERMINAL_SIZE).join("-")) + await syncLinks(medusaAppLoader, { + executeAll: executeAllLinks, + executeSafe: executeSafeLinks, + }) + } + + process.exit() + } catch (error) { + logger.error(error) + process.exit(1) + } +} + +export default main diff --git a/packages/medusa/src/commands/db/rollback.ts b/packages/medusa/src/commands/db/rollback.ts new file mode 100644 index 0000000000..28e7de66a2 --- /dev/null +++ b/packages/medusa/src/commands/db/rollback.ts @@ -0,0 +1,57 @@ +import { join } from "path" +import { ContainerRegistrationKeys, MedusaError } from "@medusajs/utils" +import { LinkLoader, logger, MedusaAppLoader } from "@medusajs/framework" + +import { ensureDbExists } from "../utils" +import { initializeContainer } from "../../loaders" +import { getResolvedPlugins } from "../../loaders/helpers/resolve-plugins" + +const TERMINAL_SIZE = process.stdout.columns + +const main = async function ({ directory, modules }) { + try { + /** + * Setup + */ + const container = await initializeContainer(directory) + await ensureDbExists(container) + + const medusaAppLoader = new MedusaAppLoader() + const configModule = container.resolve( + ContainerRegistrationKeys.CONFIG_MODULE + ) + + const plugins = getResolvedPlugins(directory, configModule, true) || [] + const linksSourcePaths = plugins.map((plugin) => + join(plugin.resolve, "links") + ) + await new LinkLoader(linksSourcePaths).load() + + /** + * Reverting migrations + */ + logger.info("Reverting migrations...") + await medusaAppLoader.runModulesMigrations({ + moduleNames: modules, + action: "revert", + }) + 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) + } +} + +export default main diff --git a/packages/medusa/src/commands/db/sync-links.ts b/packages/medusa/src/commands/db/sync-links.ts index 6f9bfac917..cd6eb366ad 100644 --- a/packages/medusa/src/commands/db/sync-links.ts +++ b/packages/medusa/src/commands/db/sync-links.ts @@ -81,6 +81,99 @@ async function askForLinkActionsToPerform( }) } +/** + * Low-level utility to sync links. This utility is used + * by the migrate command as-well. + */ +export async function syncLinks( + medusaAppLoader: MedusaAppLoader, + { + executeAll, + executeSafe, + }: { + executeSafe: boolean + executeAll: boolean + } +) { + const planner = await medusaAppLoader.getLinksExecutionPlanner() + + logger.info("Syncing links...") + + const actionPlan = await planner.createPlan() + const groupActionPlan = groupByActionPlan(actionPlan) + + if (groupActionPlan.delete?.length) { + /** + * Do not delete anything when "--execute-safe" flag + * is used. And only prompt when "--execute-all" + * flag isn't used either + */ + if (executeSafe) { + groupActionPlan.delete = [] + } else if (!executeAll) { + groupActionPlan.delete = await askForLinkActionsToPerform( + `Select the tables to ${chalk.red( + "DELETE" + )}. The following links have been removed`, + groupActionPlan.delete + ) + } + } + + if (groupActionPlan.notify?.length) { + let answer = groupActionPlan.notify + + /** + * Do not update anything when "--execute-safe" flag + * is used. And only prompt when "--execute-all" + * flag isn't used either. + */ + if (executeSafe) { + answer = [] + } else if (!executeAll) { + answer = await askForLinkActionsToPerform( + `Select the tables to ${chalk.red( + "UPDATE" + )}. The following links have been updated`, + groupActionPlan.notify + ) + } + + groupActionPlan.update ??= [] + groupActionPlan.update.push( + ...answer.map((action) => { + return { + ...action, + action: "update", + } as LinkMigrationsPlannerAction + }) + ) + } + + const toCreate = groupActionPlan.create ?? [] + const toUpdate = groupActionPlan.update ?? [] + const toDelete = groupActionPlan.delete ?? [] + const actionsToExecute = [...toCreate, ...toUpdate, ...toDelete] + + await planner.executePlan(actionsToExecute) + + if (toCreate.length) { + logActions("Created following links tables", toCreate) + } + if (toUpdate.length) { + logActions("Updated following links tables", toUpdate) + } + if (toDelete.length) { + logActions("Deleted following links tables", toDelete) + } + + if (actionsToExecute.length) { + logger.info("Links sync completed") + } else { + logger.info("Database already up-to-date") + } +} + const main = async function ({ directory, executeSafe, executeAll }) { try { const container = await initializeContainer(directory) @@ -98,83 +191,7 @@ const main = async function ({ directory, executeSafe, executeAll }) { ) await new LinkLoader(linksSourcePaths).load() - const planner = await medusaAppLoader.getLinksExecutionPlanner() - - logger.info("Syncing links...") - - const actionPlan = await planner.createPlan() - const groupActionPlan = groupByActionPlan(actionPlan) - - if (groupActionPlan.delete?.length) { - /** - * Do not delete anything when "--execute-safe" flag - * is used. And only prompt when "--execute-all" - * flag isn't used either - */ - if (executeSafe) { - groupActionPlan.delete = [] - } else if (!executeAll) { - groupActionPlan.delete = await askForLinkActionsToPerform( - `Select the tables to ${chalk.red( - "DELETE" - )}. The following links have been removed`, - groupActionPlan.delete - ) - } - } - - if (groupActionPlan.notify?.length) { - let answer = groupActionPlan.notify - - /** - * Do not update anything when "--execute-safe" flag - * is used. And only prompt when "--execute-all" - * flag isn't used either. - */ - if (executeSafe) { - answer = [] - } else if (!executeAll) { - answer = await askForLinkActionsToPerform( - `Select the tables to ${chalk.red( - "UPDATE" - )}. The following links have been updated`, - groupActionPlan.notify - ) - } - - groupActionPlan.update ??= [] - groupActionPlan.update.push( - ...answer.map((action) => { - return { - ...action, - action: "update", - } as LinkMigrationsPlannerAction - }) - ) - } - - const toCreate = groupActionPlan.create ?? [] - const toUpdate = groupActionPlan.update ?? [] - const toDelete = groupActionPlan.delete ?? [] - const actionsToExecute = [...toCreate, ...toUpdate, ...toDelete] - - await planner.executePlan(actionsToExecute) - - if (toCreate.length) { - logActions("Created following links tables", toCreate) - } - if (toUpdate.length) { - logActions("Updated following links tables", toUpdate) - } - if (toDelete.length) { - logActions("Deleted following links tables", toDelete) - } - - if (actionsToExecute.length) { - logger.info("Links sync completed") - } else { - logger.info("Database already up-to-date") - } + await syncLinks(medusaAppLoader, { executeAll, executeSafe }) process.exit() } catch (e) { logger.error(e) diff --git a/packages/medusa/src/commands/links.ts b/packages/medusa/src/commands/links.ts index 9dfe8391f0..5217768626 100644 --- a/packages/medusa/src/commands/links.ts +++ b/packages/medusa/src/commands/links.ts @@ -1,10 +1,10 @@ -import syncLinks from "./db/sync-links" +import syncLinksCmd from "./db/sync-links" const main = async function (argv) { if (argv.action !== "sync") { return process.exit() } - await syncLinks(argv) + await syncLinksCmd(argv) } export default main