feature: add sync links command (#8775)
This commit is contained in:
@@ -142,6 +142,26 @@ function buildLocalCommands(cli, isLocalProject) {
|
||||
})
|
||||
),
|
||||
})
|
||||
.command({
|
||||
command: "db:sync-links",
|
||||
desc: "Sync database schema with the links defined by your application and Medusa core",
|
||||
builder: (builder) => {
|
||||
builder.option("execute-all", {
|
||||
type: "boolean",
|
||||
describe: "Skip prompts and execute all (including unsafe) actions",
|
||||
})
|
||||
builder.option("execute-safe", {
|
||||
type: "boolean",
|
||||
describe: "Skip prompts and execute only safe actions",
|
||||
})
|
||||
},
|
||||
handler: handlerP(
|
||||
getCommandHandler("db/sync-links", (args, cmd) => {
|
||||
process.env.NODE_ENV = process.env.NODE_ENV || `development`
|
||||
return cmd(args)
|
||||
})
|
||||
),
|
||||
})
|
||||
.command({
|
||||
command: `telemetry`,
|
||||
describe: `Enable or disable collection of anonymous usage data.`,
|
||||
|
||||
185
packages/medusa/src/commands/db/sync-links.ts
Normal file
185
packages/medusa/src/commands/db/sync-links.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import boxen from "boxen"
|
||||
import chalk from "chalk"
|
||||
import { join } from "path"
|
||||
import checkbox from "@inquirer/checkbox"
|
||||
import { ContainerRegistrationKeys } from "@medusajs/utils"
|
||||
import { LinkMigrationsPlannerAction } from "@medusajs/types"
|
||||
import { LinkLoader, logger, MedusaAppLoader } from "@medusajs/framework"
|
||||
|
||||
import { ensureDbExists } from "../utils"
|
||||
import { initializeContainer } from "../../loaders"
|
||||
import { getResolvedPlugins } from "../../loaders/helpers/resolve-plugins"
|
||||
|
||||
/**
|
||||
* Groups action tables by their "action" property
|
||||
* @param actionPlan LinkMigrationsPlannerAction
|
||||
*/
|
||||
function groupByActionPlan(actionPlan: LinkMigrationsPlannerAction[]) {
|
||||
return actionPlan.reduce((acc, action) => {
|
||||
acc[action.action] ??= []
|
||||
acc[action.action].push(action)
|
||||
return acc
|
||||
}, {} as Record<"noop" | "notify" | "create" | "update" | "delete", LinkMigrationsPlannerAction[]>)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the link description for printing it to the
|
||||
* console
|
||||
*
|
||||
* @param action LinkMigrationsPlannerAction
|
||||
*/
|
||||
function buildLinkDescription(action: LinkMigrationsPlannerAction) {
|
||||
const { linkDescriptor } = action
|
||||
const from = chalk.yellow(
|
||||
`${linkDescriptor.fromModule}.${linkDescriptor.fromModel}`
|
||||
)
|
||||
const to = chalk.yellow(
|
||||
`${linkDescriptor.toModule}.${linkDescriptor.toModel}`
|
||||
)
|
||||
const table = chalk.dim(`(${action.tableName})`)
|
||||
|
||||
return `${from} <> ${to} ${table}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs the actions of a given action type with a nice border and
|
||||
* a title
|
||||
*/
|
||||
function logActions(
|
||||
title: string,
|
||||
actionsOrContext: LinkMigrationsPlannerAction[]
|
||||
) {
|
||||
const actionsList = actionsOrContext
|
||||
.map((action) => ` - ${buildLinkDescription(action)}`)
|
||||
.join("\n")
|
||||
|
||||
console.log(boxen(`${title}\n${actionsList}`, { padding: 1 }))
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a prompt to select tables that must be impacted with
|
||||
* action
|
||||
*/
|
||||
async function askForLinkActionsToPerform(
|
||||
message: string,
|
||||
actions: LinkMigrationsPlannerAction[]
|
||||
) {
|
||||
console.log(boxen(message, { borderColor: "red", padding: 1 }))
|
||||
|
||||
return await checkbox({
|
||||
message: "Select tables to act upon",
|
||||
instructions: chalk.dim(
|
||||
" <space> select, <a> select all, <i> inverse, <enter> submit"
|
||||
),
|
||||
choices: actions.map((action) => {
|
||||
return {
|
||||
name: buildLinkDescription(action),
|
||||
value: action,
|
||||
checked: false,
|
||||
}
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
const main = async function ({ directory, executeSafe, executeAll }) {
|
||||
try {
|
||||
const container = await initializeContainer(directory)
|
||||
await ensureDbExists(container)
|
||||
|
||||
const configModule = container.resolve(
|
||||
ContainerRegistrationKeys.CONFIG_MODULE
|
||||
)
|
||||
|
||||
const medusaAppLoader = new MedusaAppLoader()
|
||||
|
||||
const plugins = getResolvedPlugins(directory, configModule, true) || []
|
||||
const linksSourcePaths = plugins.map((plugin) =>
|
||||
join(plugin.resolve, "links")
|
||||
)
|
||||
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")
|
||||
}
|
||||
process.exit()
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
export default main
|
||||
@@ -1,176 +1,10 @@
|
||||
import boxen from "boxen"
|
||||
import chalk from "chalk"
|
||||
import checkbox from "@inquirer/checkbox"
|
||||
import syncLinks from "./db/sync-links"
|
||||
|
||||
import { LinkLoader, logger, MedusaAppLoader } from "@medusajs/framework"
|
||||
import { initializeContainer } from "../loaders"
|
||||
import { ContainerRegistrationKeys } from "@medusajs/utils"
|
||||
import { getResolvedPlugins } from "../loaders/helpers/resolve-plugins"
|
||||
import { LinkMigrationsPlannerAction } from "@medusajs/types"
|
||||
import { join } from "path"
|
||||
|
||||
type Action = "sync"
|
||||
|
||||
/**
|
||||
* Groups action tables by their "action" property
|
||||
* @param actionPlan LinkMigrationsPlannerAction
|
||||
*/
|
||||
function groupByActionPlan(actionPlan: LinkMigrationsPlannerAction[]) {
|
||||
return actionPlan.reduce((acc, action) => {
|
||||
acc[action.action] ??= []
|
||||
acc[action.action].push(action)
|
||||
return acc
|
||||
}, {} as Record<"noop" | "notify" | "create" | "update" | "delete", LinkMigrationsPlannerAction[]>)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the link description for printing it to the
|
||||
* console
|
||||
*
|
||||
* @param action LinkMigrationsPlannerAction
|
||||
*/
|
||||
function buildLinkDescription(action: LinkMigrationsPlannerAction) {
|
||||
const { linkDescriptor } = action
|
||||
const from = chalk.yellow(
|
||||
`${linkDescriptor.fromModule}.${linkDescriptor.fromModel}`
|
||||
)
|
||||
const to = chalk.yellow(
|
||||
`${linkDescriptor.toModule}.${linkDescriptor.toModel}`
|
||||
)
|
||||
const table = chalk.dim(`(${action.tableName})`)
|
||||
|
||||
return `${from} <> ${to} ${table}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs the actions of a given action type with a nice border and
|
||||
* a title
|
||||
*/
|
||||
function logActions(
|
||||
title: string,
|
||||
actionsOrContext: LinkMigrationsPlannerAction[]
|
||||
) {
|
||||
const actionsList = actionsOrContext
|
||||
.map((action) => ` - ${buildLinkDescription(action)}`)
|
||||
.join("\n")
|
||||
|
||||
console.log(boxen(`${title}\n${actionsList}`, { padding: 1 }))
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a prompt to select tables that must be impacted with
|
||||
* action
|
||||
*/
|
||||
async function askForLinkActionsToPerform(
|
||||
message: string,
|
||||
actions: LinkMigrationsPlannerAction[]
|
||||
) {
|
||||
console.log(boxen(message, { borderColor: "red", padding: 1 }))
|
||||
|
||||
return await checkbox({
|
||||
message: "Select tables to act upon",
|
||||
instructions: chalk.dim(
|
||||
" <space> select, <a> select all, <i> inverse, <enter> submit"
|
||||
),
|
||||
choices: actions.map((action) => {
|
||||
return {
|
||||
name: buildLinkDescription(action),
|
||||
value: action,
|
||||
checked: false,
|
||||
}
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
const main = async function ({ directory }) {
|
||||
const args = process.argv
|
||||
args.shift()
|
||||
args.shift()
|
||||
args.shift()
|
||||
|
||||
const action = args[0] as Action
|
||||
|
||||
if (action !== "sync") {
|
||||
const main = async function (argv) {
|
||||
if (argv.action !== "sync") {
|
||||
return process.exit()
|
||||
}
|
||||
|
||||
try {
|
||||
const container = await initializeContainer(directory)
|
||||
|
||||
const configModule = container.resolve(
|
||||
ContainerRegistrationKeys.CONFIG_MODULE
|
||||
)
|
||||
|
||||
const medusaAppLoader = new MedusaAppLoader()
|
||||
|
||||
const plugins = getResolvedPlugins(directory, configModule, true) || []
|
||||
const linksSourcePaths = plugins.map((plugin) =>
|
||||
join(plugin.resolve, "links")
|
||||
)
|
||||
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) {
|
||||
groupActionPlan.delete = await askForLinkActionsToPerform(
|
||||
`Select the tables to ${chalk.red(
|
||||
"DELETE"
|
||||
)}. The following links have been removed`,
|
||||
groupActionPlan.delete
|
||||
)
|
||||
}
|
||||
|
||||
if (groupActionPlan.notify?.length) {
|
||||
const 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")
|
||||
}
|
||||
process.exit()
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
process.exit(1)
|
||||
}
|
||||
await syncLinks(argv)
|
||||
}
|
||||
|
||||
export default main
|
||||
|
||||
21
packages/medusa/src/commands/utils/index.ts
Normal file
21
packages/medusa/src/commands/utils/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { logger } from "@medusajs/framework"
|
||||
import { MedusaContainer } from "@medusajs/types"
|
||||
import { ContainerRegistrationKeys } from "@medusajs/utils"
|
||||
|
||||
export async function ensureDbExists(container: MedusaContainer) {
|
||||
const pgConnection = container.resolve(
|
||||
ContainerRegistrationKeys.PG_CONNECTION
|
||||
)
|
||||
|
||||
try {
|
||||
await pgConnection.raw("SELECT 1 + 1;")
|
||||
} catch (error) {
|
||||
if (error.code === "3D000") {
|
||||
logger.error(`Cannot sync links. ${error.message.replace("error: ", "")}`)
|
||||
logger.info(`Run command "db:create" to create the database`)
|
||||
} else {
|
||||
logger.error(error)
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user