Skip to content

Commit 9b49e8f

Browse files
adrien2pthetutlage
authored andcommitted
Feat(medusa, cli): plugin db generate (medusajs#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 <[email protected]>
1 parent 009a642 commit 9b49e8f

File tree

9 files changed

+272
-1
lines changed

9 files changed

+272
-1
lines changed

.changeset/seven-gorillas-smile.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@medusajs/medusa": patch
3+
"@medusajs/cli": patch
4+
---
5+
6+
Feat(medusa, cli): plugin db generate

packages/cli/medusa-cli/src/create-cli.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,16 @@ function buildLocalCommands(cli, isLocalProject) {
238238
})
239239
),
240240
})
241+
.command({
242+
command: "plugin:db:generate",
243+
desc: "Generate migrations for a given module",
244+
handler: handlerP(
245+
getCommandHandler("plugin/db/generate", (args, cmd) => {
246+
process.env.NODE_ENV = process.env.NODE_ENV || `development`
247+
return cmd(args)
248+
})
249+
),
250+
})
241251
.command({
242252
command: "db:sync-links",
243253
desc: "Sync database schema with the links defined by your application and Medusa core",

packages/medusa/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@
4040
"watch": "tsc --build --watch",
4141
"build": "rimraf dist && tsc --build",
4242
"serve": "node dist/app.js",
43-
"test": "jest --silent=false --bail --maxWorkers=50% --forceExit"
43+
"test": "jest --runInBand --bail --forceExit --testPathIgnorePatterns='/integration-tests/' -- src/**/__tests__/**/*.ts",
44+
"test:integration": "jest --forceExit -- src/**/integration-tests/**/__tests__/**/*.ts"
4445
},
4546
"devDependencies": {
4647
"@medusajs/framework": "^2.2.0",
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { logger } from "@medusajs/framework/logger"
2+
import {
3+
defineMikroOrmCliConfig,
4+
DmlEntity,
5+
dynamicImport,
6+
} from "@medusajs/framework/utils"
7+
import { dirname, join } from "path"
8+
9+
import { MetadataStorage } from "@mikro-orm/core"
10+
import { MikroORM } from "@mikro-orm/postgresql"
11+
import { glob } from "glob"
12+
13+
const TERMINAL_SIZE = process.stdout.columns
14+
15+
/**
16+
* Generate migrations for all scanned modules in a plugin
17+
*/
18+
const main = async function ({ directory }) {
19+
try {
20+
const moduleDescriptors = [] as {
21+
serviceName: string
22+
migrationsPath: string
23+
entities: any[]
24+
}[]
25+
26+
const modulePaths = glob.sync(
27+
join(directory, "src", "modules", "*", "index.ts")
28+
)
29+
30+
for (const path of modulePaths) {
31+
const moduleDirname = dirname(path)
32+
const serviceName = await getModuleServiceName(path)
33+
const entities = await getEntitiesForModule(moduleDirname)
34+
35+
moduleDescriptors.push({
36+
serviceName,
37+
migrationsPath: join(moduleDirname, "migrations"),
38+
entities,
39+
})
40+
}
41+
42+
/**
43+
* Generating migrations
44+
*/
45+
logger.info("Generating migrations...")
46+
47+
await generateMigrations(moduleDescriptors)
48+
49+
console.log(new Array(TERMINAL_SIZE).join("-"))
50+
logger.info("Migrations generated")
51+
52+
process.exit()
53+
} catch (error) {
54+
console.log(new Array(TERMINAL_SIZE).join("-"))
55+
56+
logger.error(error.message, error)
57+
process.exit(1)
58+
}
59+
}
60+
61+
async function getEntitiesForModule(path: string) {
62+
const entities = [] as any[]
63+
64+
const entityPaths = glob.sync(join(path, "models", "*.ts"), {
65+
ignore: ["**/index.{js,ts}"],
66+
})
67+
68+
for (const entityPath of entityPaths) {
69+
const entityExports = await dynamicImport(entityPath)
70+
71+
const validEntities = Object.values(entityExports).filter(
72+
(potentialEntity) => {
73+
return (
74+
DmlEntity.isDmlEntity(potentialEntity) ||
75+
!!MetadataStorage.getMetadataFromDecorator(potentialEntity as any)
76+
)
77+
}
78+
)
79+
entities.push(...validEntities)
80+
}
81+
82+
return entities
83+
}
84+
85+
async function getModuleServiceName(path: string) {
86+
const moduleExport = await dynamicImport(path)
87+
if (!moduleExport.default) {
88+
throw new Error("The module should default export the `Module()`")
89+
}
90+
return (moduleExport.default.service as any).prototype.__joinerConfig()
91+
.serviceName
92+
}
93+
94+
async function generateMigrations(
95+
moduleDescriptors: {
96+
serviceName: string
97+
migrationsPath: string
98+
entities: any[]
99+
}[] = []
100+
) {
101+
const DB_HOST = process.env.DB_HOST ?? "localhost"
102+
const DB_USERNAME = process.env.DB_USERNAME ?? ""
103+
const DB_PASSWORD = process.env.DB_PASSWORD ?? ""
104+
105+
for (const moduleDescriptor of moduleDescriptors) {
106+
logger.info(
107+
`Generating migrations for module ${moduleDescriptor.serviceName}...`
108+
)
109+
110+
const mikroOrmConfig = defineMikroOrmCliConfig(
111+
moduleDescriptor.serviceName,
112+
{
113+
entities: moduleDescriptor.entities,
114+
host: DB_HOST,
115+
user: DB_USERNAME,
116+
password: DB_PASSWORD,
117+
migrations: {
118+
path: moduleDescriptor.migrationsPath,
119+
},
120+
}
121+
)
122+
123+
const orm = await MikroORM.init(mikroOrmConfig)
124+
const migrator = orm.getMigrator()
125+
const result = await migrator.createMigration()
126+
127+
if (result.fileName) {
128+
logger.info(`Migration created: ${result.fileName}`)
129+
} else {
130+
logger.info(`No migration created`)
131+
}
132+
}
133+
}
134+
135+
export default main
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { MedusaService, Module } from "@medusajs/framework/utils"
2+
3+
export const module1 = Module("module1", {
4+
service: class Module1Service extends MedusaService({}) {},
5+
})
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { model } from "@medusajs/framework/utils"
2+
3+
const model1 = model.define("module_model_1", {
4+
id: model.id().primaryKey(),
5+
name: model.text(),
6+
})
7+
8+
export default model1
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { MedusaService, Module } from "@medusajs/framework/utils"
2+
3+
export default Module("module1", {
4+
service: class Module1Service extends MedusaService({}) {},
5+
})
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { model } from "@medusajs/framework/utils"
2+
3+
const model1 = model.define("module_model_1", {
4+
id: model.id().primaryKey(),
5+
name: model.text(),
6+
})
7+
8+
export default model1
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { logger } from "@medusajs/framework/logger"
2+
import { FileSystem } from "@medusajs/framework/utils"
3+
import { join } from "path"
4+
import main from "../../generate"
5+
6+
jest.mock("@medusajs/framework/logger")
7+
8+
describe("plugin-generate", () => {
9+
beforeEach(() => {
10+
jest.clearAllMocks()
11+
jest
12+
.spyOn(process, "exit")
13+
.mockImplementation((code?: string | number | null) => {
14+
return code as never
15+
})
16+
})
17+
18+
afterEach(async () => {
19+
const module1 = new FileSystem(
20+
join(
21+
__dirname,
22+
"..",
23+
"__fixtures__",
24+
"plugins-1",
25+
"src",
26+
"modules",
27+
"module-1"
28+
)
29+
)
30+
await module1.remove("migrations")
31+
})
32+
33+
describe("main function", () => {
34+
it("should successfully generate migrations when valid modules are found", async () => {
35+
await main({
36+
directory: join(__dirname, "..", "__fixtures__", "plugins-1"),
37+
})
38+
39+
expect(logger.info).toHaveBeenNthCalledWith(1, "Generating migrations...")
40+
expect(logger.info).toHaveBeenNthCalledWith(
41+
2,
42+
"Generating migrations for module module1..."
43+
)
44+
expect(logger.info).toHaveBeenNthCalledWith(
45+
3,
46+
expect.stringContaining("Migration created")
47+
)
48+
expect(logger.info).toHaveBeenNthCalledWith(4, "Migrations generated")
49+
expect(process.exit).toHaveBeenCalledWith()
50+
})
51+
52+
it("should handle case when no migrations are needed", async () => {
53+
await main({
54+
directory: join(__dirname, "..", "__fixtures__", "plugins-1"),
55+
})
56+
57+
jest.clearAllMocks()
58+
59+
await main({
60+
directory: join(__dirname, "..", "__fixtures__", "plugins-1"),
61+
})
62+
63+
expect(logger.info).toHaveBeenNthCalledWith(1, "Generating migrations...")
64+
expect(logger.info).toHaveBeenNthCalledWith(
65+
2,
66+
"Generating migrations for module module1..."
67+
)
68+
expect(logger.info).toHaveBeenNthCalledWith(
69+
3,
70+
expect.stringContaining("No migration created")
71+
)
72+
expect(logger.info).toHaveBeenNthCalledWith(4, "Migrations generated")
73+
expect(process.exit).toHaveBeenCalledWith()
74+
})
75+
76+
it("should handle error when module has no default export", async () => {
77+
await main({
78+
directory: join(
79+
__dirname,
80+
"..",
81+
"__fixtures__",
82+
"plugins-1-no-default"
83+
),
84+
})
85+
expect(logger.error).toHaveBeenCalledWith(
86+
"The module should default export the `Module()`",
87+
new Error("The module should default export the `Module()`")
88+
)
89+
90+
expect(process.exit).toHaveBeenCalledWith(1)
91+
})
92+
})
93+
})

0 commit comments

Comments
 (0)