feature: add sync links command (#8775)
This commit is contained in:
1 parent
965c3c99db
commit
98b4c76ece
4 files changed
+230
-170
No files matched your search
@@ -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({
|
||||||
command: `telemetry`,
|
command: `telemetry`,
|
||||||
describe: `Enable or disable collection of anonymous usage data.`,
|
describe: `Enable or disable collection of anonymous usage data.`,
|
||||||
|
|||||||
@@ -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 syncLinks from "./db/sync-links"
|
||||||
import chalk from "chalk"
|
|
||||||
import checkbox from "@inquirer/checkbox"
|
|
||||||
|
|
||||||
import { LinkLoader, logger, MedusaAppLoader } from "@medusajs/framework"
|
const main = async function (argv) {
|
||||||
import { initializeContainer } from "../loaders"
|
if (argv.action !== "sync") {
|
||||||
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") {
|
|
||||||
return process.exit()
|
return process.exit()
|
||||||
}
|
}
|
||||||
|
await syncLinks(argv)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default main
|
export default main
|
||||||
@@ -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