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:
committed by
GitHub
parent
5582bd2038
commit
0cfaab5bb1
6
.changeset/seven-gorillas-smile.md
Normal file
6
.changeset/seven-gorillas-smile.md
Normal file
@@ -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: "db:sync-links",
|
||||
desc: "Sync database schema with the links defined by your application and Medusa core",
|
||||
|
||||
@@ -40,7 +40,8 @@
|
||||
"watch": "tsc --build --watch",
|
||||
"build": "rimraf dist && tsc --build",
|
||||
"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": {
|
||||
"@medusajs/framework": "^2.2.0",
|
||||
|
||||
135
packages/medusa/src/commands/plugin/db/generate.ts
Normal file
135
packages/medusa/src/commands/plugin/db/generate.ts
Normal file
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
import { MedusaService, Module } from "@medusajs/framework/utils"
|
||||
|
||||
export const module1 = Module("module1", {
|
||||
service: class Module1Service extends MedusaService({}) {},
|
||||
})
|
||||
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
import { MedusaService, Module } from "@medusajs/framework/utils"
|
||||
|
||||
export default Module("module1", {
|
||||
service: class Module1Service extends MedusaService({}) {},
|
||||
})
|
||||
@@ -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
|
||||
@@ -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