feature: add sync links command (#8775)

This commit is contained in:
Harminder Virk
2024-08-27 13:43:54 +05:30
committed by GitHub
parent 965c3c99db
commit 98b4c76ece
4 changed files with 230 additions and 170 deletions

View File

@@ -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.`,

View 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

View File

@@ -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

View 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)
}
}