feat(migrations): CLI generate command (#8103)
This commit is contained in:
@@ -39,9 +39,9 @@ export async function initDb({
|
||||
|
||||
try {
|
||||
const {
|
||||
migrateMedusaApp,
|
||||
runMedusaAppMigrations,
|
||||
} = require("@medusajs/medusa/dist/loaders/medusa-app")
|
||||
await migrateMedusaApp({ configModule, container })
|
||||
await runMedusaAppMigrations({ configModule, container })
|
||||
} catch (err) {
|
||||
console.error("Something went wrong while running the migrations")
|
||||
throw err
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
Constructor,
|
||||
IModuleService,
|
||||
InternalModuleDeclaration,
|
||||
LoaderOptions,
|
||||
Logger,
|
||||
MedusaContainer,
|
||||
ModuleExports,
|
||||
@@ -32,6 +33,11 @@ type ModuleResource = {
|
||||
normalizedPath: string
|
||||
}
|
||||
|
||||
type MigrationFunction = (
|
||||
options: LoaderOptions<any>,
|
||||
moduleDeclaration?: InternalModuleDeclaration
|
||||
) => Promise<void>
|
||||
|
||||
export async function loadInternalModule(
|
||||
container: MedusaContainer,
|
||||
resolution: ModuleResolution,
|
||||
@@ -171,7 +177,11 @@ export async function loadInternalModule(
|
||||
export async function loadModuleMigrations(
|
||||
resolution: ModuleResolution,
|
||||
moduleExports?: ModuleExports
|
||||
): Promise<[Function | undefined, Function | undefined]> {
|
||||
): Promise<{
|
||||
runMigrations?: MigrationFunction
|
||||
revertMigration?: MigrationFunction
|
||||
generateMigration?: MigrationFunction
|
||||
}> {
|
||||
let loadedModule: ModuleExports
|
||||
try {
|
||||
loadedModule =
|
||||
@@ -179,6 +189,7 @@ export async function loadModuleMigrations(
|
||||
|
||||
let runMigrations = loadedModule.runMigrations
|
||||
let revertMigration = loadedModule.revertMigration
|
||||
let generateMigration = loadedModule.generateMigration
|
||||
|
||||
if (!runMigrations || !revertMigration) {
|
||||
const moduleResources = await loadResources(
|
||||
@@ -189,6 +200,7 @@ export async function loadModuleMigrations(
|
||||
|
||||
const migrationScriptOptions = {
|
||||
moduleName: resolution.definition.key,
|
||||
models: moduleResources.models,
|
||||
pathToMigrations: join(moduleResources.normalizedPath, "migrations"),
|
||||
}
|
||||
|
||||
@@ -199,11 +211,15 @@ export async function loadModuleMigrations(
|
||||
revertMigration ??= ModulesSdkUtils.buildRevertMigrationScript(
|
||||
migrationScriptOptions
|
||||
)
|
||||
|
||||
generateMigration ??= ModulesSdkUtils.buildGenerateMigrationScript(
|
||||
migrationScriptOptions
|
||||
)
|
||||
}
|
||||
|
||||
return [runMigrations, revertMigration]
|
||||
return { runMigrations, revertMigration, generateMigration }
|
||||
} catch {
|
||||
return [undefined, undefined]
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,11 @@ import {
|
||||
import { asValue } from "awilix"
|
||||
import type { Knex } from "knex"
|
||||
import { MODULE_PACKAGE_NAMES } from "./definitions"
|
||||
import { MedusaModule, RegisterModuleJoinerConfig } from "./medusa-module"
|
||||
import {
|
||||
MedusaModule,
|
||||
MigrationOptions,
|
||||
RegisterModuleJoinerConfig,
|
||||
} from "./medusa-module"
|
||||
import { RemoteLink } from "./remote-link"
|
||||
import { RemoteQuery } from "./remote-query"
|
||||
import { MODULE_RESOURCE_TYPE, MODULE_SCOPE } from "./types"
|
||||
@@ -51,6 +55,7 @@ declare module "@medusajs/types" {
|
||||
|
||||
export type RunMigrationFn = () => Promise<void>
|
||||
export type RevertMigrationFn = (moduleNames: string[]) => Promise<void>
|
||||
export type GenerateMigrations = (moduleNames: string[]) => Promise<void>
|
||||
|
||||
export type MedusaModuleConfig = {
|
||||
[key: string | Modules]:
|
||||
@@ -225,6 +230,7 @@ export type MedusaAppOutput = {
|
||||
notFound?: Record<string, Record<string, string>>
|
||||
runMigrations: RunMigrationFn
|
||||
revertMigrations: RevertMigrationFn
|
||||
generateMigrations: GenerateMigrations
|
||||
onApplicationShutdown: () => Promise<void>
|
||||
onApplicationPrepareShutdown: () => Promise<void>
|
||||
sharedContainer?: MedusaContainer
|
||||
@@ -354,6 +360,9 @@ async function MedusaApp_({
|
||||
revertMigrations: async () => {
|
||||
throw new Error("Revert migrations not allowed in loaderOnly mode")
|
||||
},
|
||||
generateMigrations: async () => {
|
||||
throw new Error("Generate migrations not allowed in loaderOnly mode")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -406,10 +415,10 @@ async function MedusaApp_({
|
||||
|
||||
const applyMigration = async ({
|
||||
modulesNames,
|
||||
revert = false,
|
||||
action = "run",
|
||||
}: {
|
||||
modulesNames: string[]
|
||||
revert?: boolean
|
||||
action?: "run" | "revert" | "generate"
|
||||
}) => {
|
||||
const moduleResolutions = modulesNames.map((moduleName) => {
|
||||
return {
|
||||
@@ -423,7 +432,6 @@ async function MedusaApp_({
|
||||
.map(({ moduleName }) => moduleName)
|
||||
|
||||
if (missingModules.length) {
|
||||
const action = revert ? "revert" : "run"
|
||||
const error = new MedusaError(
|
||||
MedusaError.Types.UNKNOWN_MODULES,
|
||||
`Cannot ${action} migrations for unknown module(s) ${missingModules.join(
|
||||
@@ -443,22 +451,20 @@ async function MedusaApp_({
|
||||
}
|
||||
}
|
||||
|
||||
if (revert) {
|
||||
await MedusaModule.migrateDown(
|
||||
moduleResolution.definition.key,
|
||||
moduleResolution.resolutionPath as string,
|
||||
sharedContainer,
|
||||
moduleResolution.options,
|
||||
moduleResolution.moduleExports
|
||||
)
|
||||
const migrationOptions: MigrationOptions = {
|
||||
moduleKey: moduleResolution.definition.key,
|
||||
modulePath: moduleResolution.resolutionPath as string,
|
||||
container: sharedContainer,
|
||||
options: moduleResolution.options,
|
||||
moduleExports: moduleResolution.moduleExports,
|
||||
}
|
||||
|
||||
if (action === "revert") {
|
||||
await MedusaModule.migrateDown(migrationOptions)
|
||||
} else if (action === "run") {
|
||||
await MedusaModule.migrateUp(migrationOptions)
|
||||
} else {
|
||||
await MedusaModule.migrateUp(
|
||||
moduleResolution.definition.key,
|
||||
moduleResolution.resolutionPath as string,
|
||||
sharedContainer,
|
||||
moduleResolution.options,
|
||||
moduleResolution.moduleExports
|
||||
)
|
||||
await MedusaModule.migrateGenerate(migrationOptions)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -493,7 +499,7 @@ async function MedusaApp_({
|
||||
): Promise<void> => {
|
||||
await applyMigration({
|
||||
modulesNames,
|
||||
revert: true,
|
||||
action: "revert",
|
||||
})
|
||||
|
||||
const options: Partial<ModuleServiceInitializeOptions> =
|
||||
@@ -516,6 +522,15 @@ async function MedusaApp_({
|
||||
)
|
||||
}
|
||||
|
||||
const generateMigrations: GenerateMigrations = async (
|
||||
modulesNames
|
||||
): Promise<void> => {
|
||||
await applyMigration({
|
||||
modulesNames,
|
||||
action: "generate",
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
onApplicationShutdown,
|
||||
onApplicationPrepareShutdown,
|
||||
@@ -526,6 +541,7 @@ async function MedusaApp_({
|
||||
notFound,
|
||||
runMigrations,
|
||||
revertMigrations,
|
||||
generateMigrations,
|
||||
sharedContainer: sharedContainer_,
|
||||
}
|
||||
}
|
||||
@@ -566,3 +582,17 @@ export async function MedusaAppMigrateDown(
|
||||
|
||||
await revertMigrations(moduleNames).finally(MedusaModule.clearInstances)
|
||||
}
|
||||
|
||||
export async function MedusaAppMigrateGenerate(
|
||||
moduleNames: string[],
|
||||
options: MedusaAppOptions = {}
|
||||
): Promise<void> {
|
||||
const migrationOnly = true
|
||||
|
||||
const { generateMigrations } = await MedusaApp_({
|
||||
...options,
|
||||
migrationOnly,
|
||||
})
|
||||
|
||||
await generateMigrations(moduleNames).finally(MedusaModule.clearInstances)
|
||||
}
|
||||
|
||||
@@ -52,6 +52,14 @@ type ModuleAlias = {
|
||||
main?: boolean
|
||||
}
|
||||
|
||||
export type MigrationOptions = {
|
||||
moduleKey: string
|
||||
modulePath: string
|
||||
container?: MedusaContainer
|
||||
options?: Record<string, any>
|
||||
moduleExports?: ModuleExports
|
||||
}
|
||||
|
||||
export type ModuleBootstrapOptions = {
|
||||
moduleKey: string
|
||||
defaultPath: string
|
||||
@@ -536,13 +544,13 @@ class MedusaModule {
|
||||
return services
|
||||
}
|
||||
|
||||
public static async migrateUp(
|
||||
moduleKey: string,
|
||||
modulePath: string,
|
||||
container?: MedusaContainer,
|
||||
options?: Record<string, any>,
|
||||
moduleExports?: ModuleExports
|
||||
): Promise<void> {
|
||||
public static async migrateGenerate({
|
||||
options,
|
||||
container,
|
||||
moduleExports,
|
||||
moduleKey,
|
||||
modulePath,
|
||||
}: MigrationOptions): Promise<void> {
|
||||
const moduleResolutions = registerMedusaModule(moduleKey, {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.ISOLATED,
|
||||
@@ -555,28 +563,31 @@ class MedusaModule {
|
||||
allowUnregistered: true,
|
||||
}) ?? logger
|
||||
|
||||
container ??= createMedusaContainer()
|
||||
|
||||
for (const mod in moduleResolutions) {
|
||||
const [migrateUp] = await loadModuleMigrations(
|
||||
const { generateMigration } = await loadModuleMigrations(
|
||||
moduleResolutions[mod],
|
||||
moduleExports
|
||||
)
|
||||
|
||||
if (typeof migrateUp === "function") {
|
||||
await migrateUp({
|
||||
if (typeof generateMigration === "function") {
|
||||
await generateMigration({
|
||||
options,
|
||||
container: container!,
|
||||
logger: logger_,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static async migrateDown(
|
||||
moduleKey: string,
|
||||
modulePath: string,
|
||||
container?: MedusaContainer,
|
||||
options?: Record<string, any>,
|
||||
moduleExports?: ModuleExports
|
||||
): Promise<void> {
|
||||
public static async migrateUp({
|
||||
options,
|
||||
container,
|
||||
moduleExports,
|
||||
moduleKey,
|
||||
modulePath,
|
||||
}: MigrationOptions): Promise<void> {
|
||||
const moduleResolutions = registerMedusaModule(moduleKey, {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.ISOLATED,
|
||||
@@ -589,15 +600,55 @@ class MedusaModule {
|
||||
allowUnregistered: true,
|
||||
}) ?? logger
|
||||
|
||||
container ??= createMedusaContainer()
|
||||
|
||||
for (const mod in moduleResolutions) {
|
||||
const [, migrateDown] = await loadModuleMigrations(
|
||||
const { runMigrations } = await loadModuleMigrations(
|
||||
moduleResolutions[mod],
|
||||
moduleExports
|
||||
)
|
||||
|
||||
if (typeof migrateDown === "function") {
|
||||
await migrateDown({
|
||||
if (typeof runMigrations === "function") {
|
||||
await runMigrations({
|
||||
options,
|
||||
container: container!,
|
||||
logger: logger_,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static async migrateDown({
|
||||
options,
|
||||
container,
|
||||
moduleExports,
|
||||
moduleKey,
|
||||
modulePath,
|
||||
}: MigrationOptions): Promise<void> {
|
||||
const moduleResolutions = registerMedusaModule(moduleKey, {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.ISOLATED,
|
||||
resolve: modulePath,
|
||||
options,
|
||||
})
|
||||
|
||||
const logger_ =
|
||||
container?.resolve(ContainerRegistrationKeys.LOGGER, {
|
||||
allowUnregistered: true,
|
||||
}) ?? logger
|
||||
|
||||
container ??= createMedusaContainer()
|
||||
|
||||
for (const mod in moduleResolutions) {
|
||||
const { revertMigration } = await loadModuleMigrations(
|
||||
moduleResolutions[mod],
|
||||
moduleExports
|
||||
)
|
||||
|
||||
if (typeof revertMigration === "function") {
|
||||
await revertMigration({
|
||||
options,
|
||||
container: container!,
|
||||
logger: logger_,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -235,6 +235,10 @@ export type ModuleExports<T = Constructor<any>> = {
|
||||
options: LoaderOptions<any>,
|
||||
moduleDeclaration?: InternalModuleDeclaration
|
||||
): Promise<void>
|
||||
generateMigration?(
|
||||
options: LoaderOptions<any>,
|
||||
moduleDeclaration?: InternalModuleDeclaration
|
||||
): Promise<void>
|
||||
}
|
||||
|
||||
export interface ModuleServiceInitializeOptions {
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
} from "@mikro-orm/migrations"
|
||||
import { MikroORM, MikroORMOptions } from "@mikro-orm/core"
|
||||
import { PostgreSqlDriver } from "@mikro-orm/postgresql"
|
||||
import { dirname } from "path"
|
||||
import { access, mkdir, writeFile } from "fs/promises"
|
||||
|
||||
/**
|
||||
* Events emitted by the migrations class
|
||||
@@ -56,7 +58,9 @@ export class Migrations extends EventEmitter<MigrationsEvents> {
|
||||
async generate(): Promise<MigrationResult> {
|
||||
const connection = await this.#getConnection()
|
||||
const migrator = connection.getMigrator()
|
||||
|
||||
try {
|
||||
await this.ensureSnapshot(migrator["snapshotPath"])
|
||||
return await migrator.createMigration()
|
||||
} finally {
|
||||
await connection.close(true)
|
||||
@@ -134,4 +138,35 @@ export class Migrations extends EventEmitter<MigrationsEvents> {
|
||||
await connection.close(true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a default snapshot file if it does not already exists. This
|
||||
* prevent from creating a database to manage the migrations and instead
|
||||
* rely on the snapshot.
|
||||
*
|
||||
* @param snapshotPath
|
||||
* @protected
|
||||
*/
|
||||
protected async ensureSnapshot(snapshotPath: string): Promise<void> {
|
||||
await mkdir(dirname(snapshotPath), { recursive: true })
|
||||
|
||||
const doesFileExists = await access(snapshotPath)
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
|
||||
if (doesFileExists) {
|
||||
return
|
||||
}
|
||||
|
||||
const emptySnapshotContent = JSON.stringify(
|
||||
{
|
||||
tables: [],
|
||||
namespaces: [],
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
|
||||
await writeFile(snapshotPath, emptySnapshotContent, "utf-8")
|
||||
}
|
||||
}
|
||||
|
||||
+1
-12
@@ -1,7 +1,6 @@
|
||||
import { join } from "path"
|
||||
import { setTimeout } from "timers/promises"
|
||||
import { MetadataStorage } from "@mikro-orm/core"
|
||||
import { createDatabase, dropDatabase } from "pg-god"
|
||||
|
||||
import { Migrations } from "../../index"
|
||||
import { FileSystem } from "../../../common"
|
||||
@@ -22,22 +21,12 @@ const pgGodCredentials = {
|
||||
host: DB_HOST,
|
||||
}
|
||||
|
||||
// TODO: Reenable once flakiness is taken care of
|
||||
describe.skip("Generate migrations", () => {
|
||||
describe("Generate migrations", () => {
|
||||
beforeEach(async () => {
|
||||
await dropDatabase(
|
||||
{ databaseName: dbName, errorIfNonExist: false },
|
||||
pgGodCredentials
|
||||
)
|
||||
await fs.cleanup()
|
||||
await createDatabase({ databaseName: dbName }, pgGodCredentials)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await dropDatabase(
|
||||
{ databaseName: dbName, errorIfNonExist: false },
|
||||
pgGodCredentials
|
||||
)
|
||||
await fs.cleanup()
|
||||
MetadataStorage.clear()
|
||||
}, 300 * 1000)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./migration-down"
|
||||
export * from "./migration-up"
|
||||
export * from "./migration-generate"
|
||||
export * from "./seed"
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { LoaderOptions, Logger, ModulesSdkTypes } from "@medusajs/types"
|
||||
import { mikroOrmCreateConnection } from "../../dal"
|
||||
import { loadDatabaseConfig } from "../load-module-database-config"
|
||||
import { Migrations } from "../../migrations"
|
||||
import { toMikroOrmEntities } from "../../dml"
|
||||
|
||||
const TERMINAL_SIZE = process.stdout.columns
|
||||
|
||||
/**
|
||||
* Utility function to build a migration generation script that will generate the migrations.
|
||||
* Only used in mikro orm based modules.
|
||||
* @param moduleName
|
||||
* @param models
|
||||
* @param pathToMigrations
|
||||
*/
|
||||
export function buildGenerateMigrationScript({
|
||||
moduleName,
|
||||
models,
|
||||
pathToMigrations,
|
||||
}) {
|
||||
/**
|
||||
* This script is only valid for mikro orm managers. If a user provide a custom manager
|
||||
* he is in charge of running the migrations.
|
||||
* @param options
|
||||
* @param logger
|
||||
* @param moduleDeclaration
|
||||
*/
|
||||
return async function ({
|
||||
options,
|
||||
logger,
|
||||
}: Pick<
|
||||
LoaderOptions<ModulesSdkTypes.ModuleServiceInitializeOptions>,
|
||||
"options" | "logger"
|
||||
> = {}) {
|
||||
logger ??= console as unknown as Logger
|
||||
|
||||
console.log(new Array(TERMINAL_SIZE).join("-"))
|
||||
console.log("")
|
||||
logger.info(`MODULE: ${moduleName}`)
|
||||
|
||||
const dbData = loadDatabaseConfig(moduleName, options)!
|
||||
|
||||
const normalizedModels = toMikroOrmEntities(models)
|
||||
const orm = await mikroOrmCreateConnection(
|
||||
dbData,
|
||||
normalizedModels,
|
||||
pathToMigrations
|
||||
)
|
||||
|
||||
const migrations = new Migrations(orm)
|
||||
|
||||
try {
|
||||
const { fileName } = await migrations.generate()
|
||||
if (fileName) {
|
||||
logger.info(`Generated successfully (${fileName}).`)
|
||||
} else {
|
||||
logger.info(`Skipped. No changes detected in your models.`)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Failed with error ${error.message}`, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user