Feat(medusa, cli): plugin db generate (#10988)
RESOLVES FRMW-2875 **What** Allow to generate migration for plugins. Migration generation defer from project migration generation and therefore we choose to separate responsibility entirely. The flow is fairly simple, the user run `npx medusa plugin:db:generate` and the script will scan for all available modules in the plugins, gather their models information and generate the appropriate migrations and snapshot (for later generation) Co-authored-by: Harminder Virk <1706381+thetutlage@users.noreply.github.com>
This commit is contained in:
co-authored by
Harminder Virk
parent
5582bd2038
commit
0cfaab5bb1
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/medusa": patch
|
||||||
|
"@medusajs/cli": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Feat(medusa, cli): plugin db generate
|
||||||
@@ -238,6 +238,16 @@ function buildLocalCommands(cli, isLocalProject) {
|
|||||||
})
|
})
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
.command({
|
||||||
|
command: "plugin:db:generate",
|
||||||
|
desc: "Generate migrations for a given module",
|
||||||
|
handler: handlerP(
|
||||||
|
getCommandHandler("plugin/db/generate", (args, cmd) => {
|
||||||
|
process.env.NODE_ENV = process.env.NODE_ENV || `development`
|
||||||
|
return cmd(args)
|
||||||
|
})
|
||||||
|
),
|
||||||
|
})
|
||||||
.command({
|
.command({
|
||||||
command: "db:sync-links",
|
command: "db:sync-links",
|
||||||
desc: "Sync database schema with the links defined by your application and Medusa core",
|
desc: "Sync database schema with the links defined by your application and Medusa core",
|
||||||
|
|||||||
@@ -40,7 +40,8 @@
|
|||||||
"watch": "tsc --build --watch",
|
"watch": "tsc --build --watch",
|
||||||
"build": "rimraf dist && tsc --build",
|
"build": "rimraf dist && tsc --build",
|
||||||
"serve": "node dist/app.js",
|
"serve": "node dist/app.js",
|
||||||
"test": "jest --silent=false --bail --maxWorkers=50% --forceExit"
|
"test": "jest --runInBand --bail --forceExit --testPathIgnorePatterns='/integration-tests/' -- src/**/__tests__/**/*.ts",
|
||||||
|
"test:integration": "jest --forceExit -- src/**/integration-tests/**/__tests__/**/*.ts"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@medusajs/framework": "^2.2.0",
|
"@medusajs/framework": "^2.2.0",
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import { logger } from "@medusajs/framework/logger"
|
||||||
|
import {
|
||||||
|
defineMikroOrmCliConfig,
|
||||||
|
DmlEntity,
|
||||||
|
dynamicImport,
|
||||||
|
} from "@medusajs/framework/utils"
|
||||||
|
import { dirname, join } from "path"
|
||||||
|
|
||||||
|
import { MetadataStorage } from "@mikro-orm/core"
|
||||||
|
import { MikroORM } from "@mikro-orm/postgresql"
|
||||||
|
import { glob } from "glob"
|
||||||
|
|
||||||
|
const TERMINAL_SIZE = process.stdout.columns
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate migrations for all scanned modules in a plugin
|
||||||
|
*/
|
||||||
|
const main = async function ({ directory }) {
|
||||||
|
try {
|
||||||
|
const moduleDescriptors = [] as {
|
||||||
|
serviceName: string
|
||||||
|
migrationsPath: string
|
||||||
|
entities: any[]
|
||||||
|
}[]
|
||||||
|
|
||||||
|
const modulePaths = glob.sync(
|
||||||
|
join(directory, "src", "modules", "*", "index.ts")
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const path of modulePaths) {
|
||||||
|
const moduleDirname = dirname(path)
|
||||||
|
const serviceName = await getModuleServiceName(path)
|
||||||
|
const entities = await getEntitiesForModule(moduleDirname)
|
||||||
|
|
||||||
|
moduleDescriptors.push({
|
||||||
|
serviceName,
|
||||||
|
migrationsPath: join(moduleDirname, "migrations"),
|
||||||
|
entities,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generating migrations
|
||||||
|
*/
|
||||||
|
logger.info("Generating migrations...")
|
||||||
|
|
||||||
|
await generateMigrations(moduleDescriptors)
|
||||||
|
|
||||||
|
console.log(new Array(TERMINAL_SIZE).join("-"))
|
||||||
|
logger.info("Migrations generated")
|
||||||
|
|
||||||
|
process.exit()
|
||||||
|
} catch (error) {
|
||||||
|
console.log(new Array(TERMINAL_SIZE).join("-"))
|
||||||
|
|
||||||
|
logger.error(error.message, error)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getEntitiesForModule(path: string) {
|
||||||
|
const entities = [] as any[]
|
||||||
|
|
||||||
|
const entityPaths = glob.sync(join(path, "models", "*.ts"), {
|
||||||
|
ignore: ["**/index.{js,ts}"],
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const entityPath of entityPaths) {
|
||||||
|
const entityExports = await dynamicImport(entityPath)
|
||||||
|
|
||||||
|
const validEntities = Object.values(entityExports).filter(
|
||||||
|
(potentialEntity) => {
|
||||||
|
return (
|
||||||
|
DmlEntity.isDmlEntity(potentialEntity) ||
|
||||||
|
!!MetadataStorage.getMetadataFromDecorator(potentialEntity as any)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
entities.push(...validEntities)
|
||||||
|
}
|
||||||
|
|
||||||
|
return entities
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getModuleServiceName(path: string) {
|
||||||
|
const moduleExport = await dynamicImport(path)
|
||||||
|
if (!moduleExport.default) {
|
||||||
|
throw new Error("The module should default export the `Module()`")
|
||||||
|
}
|
||||||
|
return (moduleExport.default.service as any).prototype.__joinerConfig()
|
||||||
|
.serviceName
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateMigrations(
|
||||||
|
moduleDescriptors: {
|
||||||
|
serviceName: string
|
||||||
|
migrationsPath: string
|
||||||
|
entities: any[]
|
||||||
|
}[] = []
|
||||||
|
) {
|
||||||
|
const DB_HOST = process.env.DB_HOST ?? "localhost"
|
||||||
|
const DB_USERNAME = process.env.DB_USERNAME ?? ""
|
||||||
|
const DB_PASSWORD = process.env.DB_PASSWORD ?? ""
|
||||||
|
|
||||||
|
for (const moduleDescriptor of moduleDescriptors) {
|
||||||
|
logger.info(
|
||||||
|
`Generating migrations for module ${moduleDescriptor.serviceName}...`
|
||||||
|
)
|
||||||
|
|
||||||
|
const mikroOrmConfig = defineMikroOrmCliConfig(
|
||||||
|
moduleDescriptor.serviceName,
|
||||||
|
{
|
||||||
|
entities: moduleDescriptor.entities,
|
||||||
|
host: DB_HOST,
|
||||||
|
user: DB_USERNAME,
|
||||||
|
password: DB_PASSWORD,
|
||||||
|
migrations: {
|
||||||
|
path: moduleDescriptor.migrationsPath,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const orm = await MikroORM.init(mikroOrmConfig)
|
||||||
|
const migrator = orm.getMigrator()
|
||||||
|
const result = await migrator.createMigration()
|
||||||
|
|
||||||
|
if (result.fileName) {
|
||||||
|
logger.info(`Migration created: ${result.fileName}`)
|
||||||
|
} else {
|
||||||
|
logger.info(`No migration created`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default main
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
import { MedusaService, Module } from "@medusajs/framework/utils"
|
||||||
|
|
||||||
|
export const module1 = Module("module1", {
|
||||||
|
service: class Module1Service extends MedusaService({}) {},
|
||||||
|
})
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
import { model } from "@medusajs/framework/utils"
|
||||||
|
|
||||||
|
const model1 = model.define("module_model_1", {
|
||||||
|
id: model.id().primaryKey(),
|
||||||
|
name: model.text(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export default model1
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
import { MedusaService, Module } from "@medusajs/framework/utils"
|
||||||
|
|
||||||
|
export default Module("module1", {
|
||||||
|
service: class Module1Service extends MedusaService({}) {},
|
||||||
|
})
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
import { model } from "@medusajs/framework/utils"
|
||||||
|
|
||||||
|
const model1 = model.define("module_model_1", {
|
||||||
|
id: model.id().primaryKey(),
|
||||||
|
name: model.text(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export default model1
|
||||||
+93
@@ -0,0 +1,93 @@
|
|||||||
|
import { logger } from "@medusajs/framework/logger"
|
||||||
|
import { FileSystem } from "@medusajs/framework/utils"
|
||||||
|
import { join } from "path"
|
||||||
|
import main from "../../generate"
|
||||||
|
|
||||||
|
jest.mock("@medusajs/framework/logger")
|
||||||
|
|
||||||
|
describe("plugin-generate", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
jest
|
||||||
|
.spyOn(process, "exit")
|
||||||
|
.mockImplementation((code?: string | number | null) => {
|
||||||
|
return code as never
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
const module1 = new FileSystem(
|
||||||
|
join(
|
||||||
|
__dirname,
|
||||||
|
"..",
|
||||||
|
"__fixtures__",
|
||||||
|
"plugins-1",
|
||||||
|
"src",
|
||||||
|
"modules",
|
||||||
|
"module-1"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await module1.remove("migrations")
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("main function", () => {
|
||||||
|
it("should successfully generate migrations when valid modules are found", async () => {
|
||||||
|
await main({
|
||||||
|
directory: join(__dirname, "..", "__fixtures__", "plugins-1"),
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(logger.info).toHaveBeenNthCalledWith(1, "Generating migrations...")
|
||||||
|
expect(logger.info).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
"Generating migrations for module module1..."
|
||||||
|
)
|
||||||
|
expect(logger.info).toHaveBeenNthCalledWith(
|
||||||
|
3,
|
||||||
|
expect.stringContaining("Migration created")
|
||||||
|
)
|
||||||
|
expect(logger.info).toHaveBeenNthCalledWith(4, "Migrations generated")
|
||||||
|
expect(process.exit).toHaveBeenCalledWith()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle case when no migrations are needed", async () => {
|
||||||
|
await main({
|
||||||
|
directory: join(__dirname, "..", "__fixtures__", "plugins-1"),
|
||||||
|
})
|
||||||
|
|
||||||
|
jest.clearAllMocks()
|
||||||
|
|
||||||
|
await main({
|
||||||
|
directory: join(__dirname, "..", "__fixtures__", "plugins-1"),
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(logger.info).toHaveBeenNthCalledWith(1, "Generating migrations...")
|
||||||
|
expect(logger.info).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
"Generating migrations for module module1..."
|
||||||
|
)
|
||||||
|
expect(logger.info).toHaveBeenNthCalledWith(
|
||||||
|
3,
|
||||||
|
expect.stringContaining("No migration created")
|
||||||
|
)
|
||||||
|
expect(logger.info).toHaveBeenNthCalledWith(4, "Migrations generated")
|
||||||
|
expect(process.exit).toHaveBeenCalledWith()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle error when module has no default export", async () => {
|
||||||
|
await main({
|
||||||
|
directory: join(
|
||||||
|
__dirname,
|
||||||
|
"..",
|
||||||
|
"__fixtures__",
|
||||||
|
"plugins-1-no-default"
|
||||||
|
),
|
||||||
|
})
|
||||||
|
expect(logger.error).toHaveBeenCalledWith(
|
||||||
|
"The module should default export the `Module()`",
|
||||||
|
new Error("The module should default export the `Module()`")
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(process.exit).toHaveBeenCalledWith(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user