Skip to content

Commit

Permalink
Feat(medusa, cli): plugin db generate (medusajs#10988)
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
adrien2p and thetutlage authored Jan 17, 2025
1 parent 5582bd2 commit 0cfaab5
Show file tree
Hide file tree
Showing 9 changed files with 272 additions and 1 deletion.
6 changes: 6 additions & 0 deletions .changeset/seven-gorillas-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@medusajs/medusa": patch
"@medusajs/cli": patch
---

Feat(medusa, cli): plugin db generate
10 changes: 10 additions & 0 deletions packages/cli/medusa-cli/src/create-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion packages/medusa/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
135 changes: 135 additions & 0 deletions packages/medusa/src/commands/plugin/db/generate.ts
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { MedusaService, Module } from "@medusajs/framework/utils"

export const module1 = Module("module1", {
service: class Module1Service extends MedusaService({}) {},
})
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { MedusaService, Module } from "@medusajs/framework/utils"

export default Module("module1", {
service: class Module1Service extends MedusaService({}) {},
})
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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)
})
})
})

0 comments on commit 0cfaab5

Please sign in to comment.