forked from medusajs/medusa
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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]>
- Loading branch information
1 parent
5582bd2
commit 0cfaab5
Showing
9 changed files
with
272 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
5 changes: 5 additions & 0 deletions
5
...ugin/db/integration-tests/__fixtures__/plugins-1-no-default/src/modules/module-1/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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({}) {}, | ||
}) |
8 changes: 8 additions & 0 deletions
8
...ion-tests/__fixtures__/plugins-1-no-default/src/modules/module-1/models/module-model-1.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
5 changes: 5 additions & 0 deletions
5
...commands/plugin/db/integration-tests/__fixtures__/plugins-1/src/modules/module-1/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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({}) {}, | ||
}) |
8 changes: 8 additions & 0 deletions
8
...db/integration-tests/__fixtures__/plugins-1/src/modules/module-1/models/module-model-1.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
93 changes: 93 additions & 0 deletions
93
packages/medusa/src/commands/plugin/db/integration-tests/__tests__/plugin-generate.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
}) | ||
}) |