Initial implementation with just the generate method (#7973)
This commit is contained in:
106
packages/core/utils/src/migrations/index.ts
Normal file
106
packages/core/utils/src/migrations/index.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { EventEmitter } from "events"
|
||||
import {
|
||||
MigrateOptions,
|
||||
MigrationResult,
|
||||
UmzugMigration,
|
||||
} from "@mikro-orm/migrations"
|
||||
import { MikroORM, MikroORMOptions } from "@mikro-orm/core"
|
||||
|
||||
/**
|
||||
* Events emitted by the migrations class
|
||||
*/
|
||||
export type MigrationsEvents = {
|
||||
migrating: [UmzugMigration]
|
||||
migrated: [UmzugMigration]
|
||||
reverting: [UmzugMigration]
|
||||
reverted: [UmzugMigration]
|
||||
}
|
||||
|
||||
/**
|
||||
* Exposes the API to programmatically manage Mikro ORM migrations
|
||||
*/
|
||||
export class Migrations extends EventEmitter<MigrationsEvents> {
|
||||
#config: Partial<MikroORMOptions>
|
||||
|
||||
constructor(config: Partial<MikroORMOptions>) {
|
||||
super()
|
||||
this.#config = config
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an existing connection or instantiates a new
|
||||
* one
|
||||
*/
|
||||
async #getConnection() {
|
||||
return await MikroORM.init({
|
||||
...this.#config,
|
||||
migrations: {
|
||||
...this.#config.migrations,
|
||||
silent: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates migrations for a collection of entities defined
|
||||
* in the config
|
||||
*/
|
||||
async generate(): Promise<MigrationResult> {
|
||||
const connection = await this.#getConnection()
|
||||
const migrator = connection.getMigrator()
|
||||
try {
|
||||
return await migrator.createMigration()
|
||||
} finally {
|
||||
await connection.close(true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run migrations for the provided entities
|
||||
*/
|
||||
async run(
|
||||
options?: string | string[] | MigrateOptions
|
||||
): Promise<UmzugMigration[]> {
|
||||
const connection = await this.#getConnection()
|
||||
const migrator = connection.getMigrator()
|
||||
|
||||
migrator["umzug"].on("migrating", (event: UmzugMigration) =>
|
||||
this.emit("migrating", event)
|
||||
)
|
||||
migrator["umzug"].on("migrated", (event: UmzugMigration) => {
|
||||
this.emit("migrated", event)
|
||||
})
|
||||
|
||||
try {
|
||||
const res = await migrator.up(options)
|
||||
return res
|
||||
} finally {
|
||||
migrator["umzug"].clearListeners()
|
||||
await connection.close(true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run migrations for the provided entities
|
||||
*/
|
||||
async revert(
|
||||
options?: string | string[] | MigrateOptions
|
||||
): Promise<UmzugMigration[]> {
|
||||
const connection = await this.#getConnection()
|
||||
const migrator = connection.getMigrator()
|
||||
|
||||
migrator["umzug"].on("reverting", (event: UmzugMigration) =>
|
||||
this.emit("reverting", event)
|
||||
)
|
||||
migrator["umzug"].on("reverted", (event: UmzugMigration) => {
|
||||
this.emit("reverted", event)
|
||||
})
|
||||
|
||||
try {
|
||||
return await migrator.down(options)
|
||||
} finally {
|
||||
migrator["umzug"].clearListeners()
|
||||
await connection.close(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
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"
|
||||
import { DmlEntity, model } from "../../../dml"
|
||||
import { defineMikroOrmCliConfig } from "../../../modules-sdk"
|
||||
|
||||
const DB_HOST = process.env.DB_HOST ?? "localhost"
|
||||
const DB_USERNAME = process.env.DB_USERNAME ?? ""
|
||||
const DB_PASSWORD = process.env.DB_PASSWORD ?? " "
|
||||
|
||||
const dbName = "my-test-service-generate"
|
||||
const moduleName = "myTestServiceGenerate"
|
||||
const fs = new FileSystem(join(__dirname, "./migrations/generate"))
|
||||
|
||||
const pgGodCredentials = {
|
||||
user: DB_USERNAME,
|
||||
password: DB_PASSWORD,
|
||||
host: DB_HOST,
|
||||
}
|
||||
|
||||
describe("Generate migrations", () => {
|
||||
beforeEach(async () => {
|
||||
await createDatabase({ databaseName: dbName }, pgGodCredentials)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await dropDatabase(
|
||||
{ databaseName: dbName, errorIfNonExist: false },
|
||||
pgGodCredentials
|
||||
)
|
||||
await fs.cleanup()
|
||||
MetadataStorage.clear()
|
||||
}, 300 * 1000)
|
||||
|
||||
test("generate migrations for a single entity", async () => {
|
||||
const User = model.define("User", {
|
||||
id: model.id().primaryKey(),
|
||||
email: model.text().unique(),
|
||||
fullName: model.text().nullable(),
|
||||
})
|
||||
|
||||
const config = defineMikroOrmCliConfig(moduleName, {
|
||||
entities: [User],
|
||||
dbName: dbName,
|
||||
migrations: {
|
||||
path: fs.basePath,
|
||||
},
|
||||
...pgGodCredentials,
|
||||
})
|
||||
|
||||
const migrations = new Migrations(config)
|
||||
const results = await migrations.generate()
|
||||
|
||||
expect(await fs.exists(results.fileName))
|
||||
expect(await fs.contents(results.fileName)).toMatch(
|
||||
/create table if not exists "user"/
|
||||
)
|
||||
})
|
||||
|
||||
test("generate migrations for multiple entities", async () => {
|
||||
const User = model
|
||||
.define("User", {
|
||||
id: model.id().primaryKey(),
|
||||
email: model.text().unique(),
|
||||
fullName: model.text().nullable(),
|
||||
cars: model.hasMany(() => Car),
|
||||
})
|
||||
.cascades({
|
||||
delete: ["cars"],
|
||||
})
|
||||
|
||||
const Car = model.define("Car", {
|
||||
id: model.id().primaryKey(),
|
||||
name: model.text(),
|
||||
user: model.belongsTo(() => User, { mappedBy: "cars" }),
|
||||
})
|
||||
|
||||
const config = defineMikroOrmCliConfig(moduleName, {
|
||||
entities: [User, Car],
|
||||
dbName: dbName,
|
||||
migrations: {
|
||||
path: fs.basePath,
|
||||
},
|
||||
...pgGodCredentials,
|
||||
})
|
||||
|
||||
const migrations = new Migrations(config)
|
||||
const results = await migrations.generate()
|
||||
|
||||
expect(await fs.exists(results.fileName))
|
||||
expect(await fs.contents(results.fileName)).toMatch(
|
||||
/create table if not exists "user"/
|
||||
)
|
||||
expect(await fs.contents(results.fileName)).toMatch(
|
||||
/create table if not exists "car"/
|
||||
)
|
||||
})
|
||||
|
||||
test("generate new file when entities are added", async () => {
|
||||
function run(entities: DmlEntity<any, any>[]) {
|
||||
const config = defineMikroOrmCliConfig(moduleName, {
|
||||
entities,
|
||||
dbName: dbName,
|
||||
migrations: {
|
||||
path: fs.basePath,
|
||||
},
|
||||
...pgGodCredentials,
|
||||
})
|
||||
|
||||
const migrations = new Migrations(config)
|
||||
return migrations.generate()
|
||||
}
|
||||
|
||||
const User = model.define("User", {
|
||||
id: model.id().primaryKey(),
|
||||
email: model.text().unique(),
|
||||
fullName: model.text().nullable(),
|
||||
})
|
||||
|
||||
const run1 = await run([User])
|
||||
expect(await fs.exists(run1.fileName))
|
||||
|
||||
const Car = model.define("Car", {
|
||||
id: model.id().primaryKey(),
|
||||
name: model.text(),
|
||||
})
|
||||
|
||||
await setTimeout(1000)
|
||||
|
||||
const run2 = await run([User, Car])
|
||||
expect(await fs.exists(run2.fileName))
|
||||
|
||||
expect(run1.fileName).not.toEqual(run2.fileName)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,173 @@
|
||||
import { join } from "path"
|
||||
import { MikroORM } from "@mikro-orm/postgresql"
|
||||
import { MetadataStorage } from "@mikro-orm/core"
|
||||
import { createDatabase, dropDatabase } from "pg-god"
|
||||
import { TSMigrationGenerator } from "@mikro-orm/migrations"
|
||||
|
||||
import { model } from "../../../dml"
|
||||
import { FileSystem } from "../../../common"
|
||||
import { Migrations, MigrationsEvents } from "../../index"
|
||||
import { defineMikroOrmCliConfig } from "../../../modules-sdk"
|
||||
|
||||
const DB_HOST = process.env.DB_HOST ?? "localhost"
|
||||
const DB_USERNAME = process.env.DB_USERNAME ?? ""
|
||||
const DB_PASSWORD = process.env.DB_PASSWORD ?? " "
|
||||
|
||||
const dbName = "my-test-service-revert"
|
||||
const moduleName = "myTestServiceRevert"
|
||||
const fs = new FileSystem(join(__dirname, "./migrations/revert"))
|
||||
|
||||
const migrationFileNameGenerator = (_: string, name?: string) => {
|
||||
return `Migration${new Date().getTime()}${name ? `_${name}` : ""}`
|
||||
}
|
||||
|
||||
const pgGodCredentials = {
|
||||
user: DB_USERNAME,
|
||||
password: DB_PASSWORD,
|
||||
host: DB_HOST,
|
||||
}
|
||||
|
||||
describe("Revert migrations", () => {
|
||||
beforeEach(async () => {
|
||||
await createDatabase({ databaseName: dbName }, pgGodCredentials)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await fs.cleanup()
|
||||
await dropDatabase(
|
||||
{ databaseName: dbName, errorIfNonExist: false },
|
||||
pgGodCredentials
|
||||
)
|
||||
MetadataStorage.clear()
|
||||
}, 300 * 1000)
|
||||
|
||||
test("revert migrations", async () => {
|
||||
const User = model.define("User", {
|
||||
id: model.id().primaryKey(),
|
||||
email: model.text().unique(),
|
||||
fullName: model.text().nullable(),
|
||||
})
|
||||
|
||||
const config = defineMikroOrmCliConfig(moduleName, {
|
||||
entities: [User],
|
||||
dbName: dbName,
|
||||
migrations: {
|
||||
path: fs.basePath,
|
||||
fileName: migrationFileNameGenerator,
|
||||
},
|
||||
...pgGodCredentials,
|
||||
})
|
||||
|
||||
const migrations = new Migrations(config)
|
||||
await migrations.generate()
|
||||
await migrations.run()
|
||||
const results = await migrations.revert()
|
||||
|
||||
const orm = await MikroORM.init(config)
|
||||
const usersTableExists = await orm.em.getKnex().schema.hasTable("user")
|
||||
await orm.close()
|
||||
|
||||
expect(results).toHaveLength(1)
|
||||
expect(results).toEqual([
|
||||
{
|
||||
name: expect.stringMatching(/Migration\d+/),
|
||||
path: expect.stringContaining(__dirname),
|
||||
},
|
||||
])
|
||||
expect(usersTableExists).toEqual(false)
|
||||
})
|
||||
|
||||
test("emit events during revert", async () => {
|
||||
let events: {
|
||||
event: keyof MigrationsEvents
|
||||
payload: MigrationsEvents[keyof MigrationsEvents][number]
|
||||
}[] = []
|
||||
|
||||
const User = model.define("User", {
|
||||
id: model.id().primaryKey(),
|
||||
email: model.text().unique(),
|
||||
fullName: model.text().nullable(),
|
||||
})
|
||||
|
||||
const config = defineMikroOrmCliConfig(moduleName, {
|
||||
entities: [User],
|
||||
dbName: dbName,
|
||||
migrations: {
|
||||
path: fs.basePath,
|
||||
fileName: migrationFileNameGenerator,
|
||||
},
|
||||
...pgGodCredentials,
|
||||
})
|
||||
|
||||
const migrations = new Migrations(config)
|
||||
|
||||
migrations.on("reverting", (event) => {
|
||||
events.push({ event: "reverting", payload: event })
|
||||
})
|
||||
migrations.on("reverted", (event) => {
|
||||
events.push({ event: "reverted", payload: event })
|
||||
})
|
||||
|
||||
await migrations.generate()
|
||||
await migrations.run()
|
||||
await migrations.revert()
|
||||
|
||||
expect(events).toHaveLength(2)
|
||||
|
||||
expect(events[0].event).toEqual("reverting")
|
||||
expect(events[0].payload).toEqual({
|
||||
name: expect.stringMatching(/Migration\d+/),
|
||||
path: expect.stringContaining(__dirname),
|
||||
context: {},
|
||||
})
|
||||
|
||||
expect(events[1].event).toEqual("reverted")
|
||||
expect(events[1].payload).toEqual({
|
||||
name: expect.stringMatching(/Migration\d+/),
|
||||
path: expect.stringContaining(__dirname),
|
||||
context: {},
|
||||
})
|
||||
})
|
||||
|
||||
test("throw error when migration fails during revert", async () => {
|
||||
/**
|
||||
* Custom strategy to output invalid SQL statement inside the
|
||||
* migration file
|
||||
*/
|
||||
class CustomTSMigrationGenerator extends TSMigrationGenerator {
|
||||
createStatement(sql: string, padLeft: number): string {
|
||||
let output = super.createStatement(sql, padLeft)
|
||||
return output.replace("drop table", "drop foo")
|
||||
}
|
||||
}
|
||||
|
||||
const User = model.define("User", {
|
||||
id: model.id().primaryKey(),
|
||||
email: model.text().unique(),
|
||||
fullName: model.text().nullable(),
|
||||
})
|
||||
|
||||
const config = defineMikroOrmCliConfig(moduleName, {
|
||||
entities: [User],
|
||||
dbName: dbName,
|
||||
migrations: {
|
||||
path: fs.basePath,
|
||||
generator: CustomTSMigrationGenerator,
|
||||
fileName: migrationFileNameGenerator,
|
||||
},
|
||||
...pgGodCredentials,
|
||||
})
|
||||
|
||||
const migrations = new Migrations(config)
|
||||
await migrations.generate()
|
||||
|
||||
await migrations.run()
|
||||
expect(migrations.revert()).rejects.toThrow(/.*Migration.*/)
|
||||
|
||||
const orm = await MikroORM.init(config)
|
||||
const usersTableExists = await orm.em.getKnex().schema.hasTable("user")
|
||||
await orm.close()
|
||||
|
||||
expect(usersTableExists).toEqual(true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,171 @@
|
||||
import { join } from "path"
|
||||
import { MikroORM } from "@mikro-orm/postgresql"
|
||||
import { MetadataStorage } from "@mikro-orm/core"
|
||||
import { createDatabase, dropDatabase } from "pg-god"
|
||||
import { TSMigrationGenerator } from "@mikro-orm/migrations"
|
||||
|
||||
import { model } from "../../../dml"
|
||||
import { FileSystem } from "../../../common"
|
||||
import { Migrations, MigrationsEvents } from "../../index"
|
||||
import { defineMikroOrmCliConfig } from "../../../modules-sdk"
|
||||
|
||||
const migrationFileNameGenerator = (_: string, name?: string) => {
|
||||
return `Migration${new Date().getTime()}${name ? `_${name}` : ""}`
|
||||
}
|
||||
|
||||
const DB_HOST = process.env.DB_HOST ?? "localhost"
|
||||
const DB_USERNAME = process.env.DB_USERNAME ?? ""
|
||||
const DB_PASSWORD = process.env.DB_PASSWORD ?? " "
|
||||
process.env.DB_PASSWORD = DB_PASSWORD
|
||||
|
||||
const dbName = "my-test-service-run"
|
||||
const moduleName = "myTestServiceRun"
|
||||
const fs = new FileSystem(join(__dirname, "./migrations/run"))
|
||||
|
||||
const pgGodCredentials = {
|
||||
user: DB_USERNAME,
|
||||
password: DB_PASSWORD,
|
||||
host: DB_HOST,
|
||||
}
|
||||
|
||||
describe("Run migrations", () => {
|
||||
beforeEach(async () => {
|
||||
await createDatabase({ databaseName: dbName }, pgGodCredentials)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await dropDatabase(
|
||||
{ databaseName: dbName, errorIfNonExist: false },
|
||||
pgGodCredentials
|
||||
)
|
||||
await fs.cleanup()
|
||||
MetadataStorage.clear()
|
||||
}, 300 * 1000)
|
||||
|
||||
test("run migrations after generating them", async () => {
|
||||
const User = model.define("User", {
|
||||
id: model.id().primaryKey(),
|
||||
email: model.text().unique(),
|
||||
fullName: model.text().nullable(),
|
||||
})
|
||||
|
||||
const config = defineMikroOrmCliConfig(moduleName, {
|
||||
entities: [User],
|
||||
dbName: dbName,
|
||||
migrations: {
|
||||
path: fs.basePath,
|
||||
fileName: migrationFileNameGenerator,
|
||||
},
|
||||
...pgGodCredentials,
|
||||
})
|
||||
|
||||
const migrations = new Migrations(config)
|
||||
await migrations.generate()
|
||||
const results = await migrations.run()
|
||||
|
||||
const orm = await MikroORM.init(config)
|
||||
const usersTableExists = await orm.em.getKnex().schema.hasTable("user")
|
||||
await orm.close()
|
||||
|
||||
expect(results).toHaveLength(1)
|
||||
expect(results).toEqual([
|
||||
{
|
||||
name: expect.stringMatching(/Migration\d+/),
|
||||
path: expect.stringContaining(__dirname),
|
||||
},
|
||||
])
|
||||
expect(usersTableExists).toEqual(true)
|
||||
})
|
||||
|
||||
test("emit events when running migrations", async () => {
|
||||
let events: {
|
||||
event: keyof MigrationsEvents
|
||||
payload: MigrationsEvents[keyof MigrationsEvents][number]
|
||||
}[] = []
|
||||
|
||||
const User = model.define("User", {
|
||||
id: model.id().primaryKey(),
|
||||
email: model.text().unique(),
|
||||
fullName: model.text().nullable(),
|
||||
})
|
||||
|
||||
const config = defineMikroOrmCliConfig(moduleName, {
|
||||
entities: [User],
|
||||
dbName: dbName,
|
||||
migrations: {
|
||||
path: fs.basePath,
|
||||
fileName: migrationFileNameGenerator,
|
||||
},
|
||||
...pgGodCredentials,
|
||||
})
|
||||
|
||||
const migrations = new Migrations(config)
|
||||
await migrations.generate()
|
||||
|
||||
migrations.on("migrating", (event) => {
|
||||
events.push({ event: "migrating", payload: event })
|
||||
})
|
||||
migrations.on("migrated", (event) => {
|
||||
events.push({ event: "migrated", payload: event })
|
||||
})
|
||||
|
||||
await migrations.run()
|
||||
expect(events).toHaveLength(2)
|
||||
|
||||
expect(events[0].event).toEqual("migrating")
|
||||
expect(events[0].payload).toEqual({
|
||||
name: expect.stringMatching(/Migration\d+/),
|
||||
path: expect.stringContaining(__dirname),
|
||||
context: {},
|
||||
})
|
||||
|
||||
expect(events[1].event).toEqual("migrated")
|
||||
expect(events[1].payload).toEqual({
|
||||
name: expect.stringMatching(/Migration\d+/),
|
||||
path: expect.stringContaining(__dirname),
|
||||
context: {},
|
||||
})
|
||||
})
|
||||
|
||||
test("throw error when migration fails during run", async () => {
|
||||
/**
|
||||
* Custom strategy to output invalid SQL statement inside the
|
||||
* migration file
|
||||
*/
|
||||
class CustomTSMigrationGenerator extends TSMigrationGenerator {
|
||||
createStatement(sql: string, padLeft: number): string {
|
||||
let output = super.createStatement(sql, padLeft)
|
||||
return output.replace('"user"', '"foo";')
|
||||
}
|
||||
}
|
||||
|
||||
const User = model.define("User", {
|
||||
id: model.id().primaryKey(),
|
||||
email: model.text().unique(),
|
||||
fullName: model.text().nullable(),
|
||||
})
|
||||
|
||||
const config = defineMikroOrmCliConfig(moduleName, {
|
||||
entities: [User],
|
||||
dbName: dbName,
|
||||
migrations: {
|
||||
path: fs.basePath,
|
||||
generator: CustomTSMigrationGenerator,
|
||||
fileName: migrationFileNameGenerator,
|
||||
},
|
||||
...pgGodCredentials,
|
||||
})
|
||||
|
||||
const migrations = new Migrations(config)
|
||||
await migrations.generate()
|
||||
|
||||
expect(migrations.run()).rejects.toThrow(/.*Migration.*/)
|
||||
|
||||
const orm = await MikroORM.init(config)
|
||||
const usersTableExists = await orm.em.getKnex().schema.hasTable("user")
|
||||
|
||||
await orm.close()
|
||||
|
||||
expect(usersTableExists).toEqual(false)
|
||||
})
|
||||
})
|
||||
@@ -14,13 +14,13 @@ describe("defineMikroOrmCliConfig", () => {
|
||||
test("should return the correct config", () => {
|
||||
const config = defineMikroOrmCliConfig(moduleName, {
|
||||
entities: [{} as any],
|
||||
databaseName: "medusa-fulfillment",
|
||||
dbName: "medusa-fulfillment",
|
||||
})
|
||||
|
||||
expect(config).toEqual({
|
||||
entities: [{}],
|
||||
clientUrl: "postgres://postgres@localhost/medusa-fulfillment",
|
||||
type: "postgresql",
|
||||
dbName: "medusa-fulfillment",
|
||||
migrations: {
|
||||
generator: expect.any(Function),
|
||||
},
|
||||
@@ -34,8 +34,8 @@ describe("defineMikroOrmCliConfig", () => {
|
||||
|
||||
expect(config).toEqual({
|
||||
entities: [{}],
|
||||
clientUrl: "postgres://postgres@localhost/medusa-my-test",
|
||||
type: "postgresql",
|
||||
dbName: "medusa-my-test",
|
||||
migrations: {
|
||||
generator: expect.any(Function),
|
||||
},
|
||||
|
||||
@@ -17,12 +17,10 @@ type Options = Partial<Omit<MikroORMOptions, "entities" | "entitiesTs">> & {
|
||||
| EntitySchema
|
||||
| DmlEntity<any, any>
|
||||
)[]
|
||||
databaseName?: string
|
||||
}
|
||||
|
||||
type ReturnedOptions = Partial<MikroORMOptions> & {
|
||||
entities: MikroORMOptions["entities"]
|
||||
clientUrl: string
|
||||
type: MikroORMOptions["type"]
|
||||
migrations: MikroORMOptions["migrations"]
|
||||
}
|
||||
@@ -53,17 +51,11 @@ export function defineMikroOrmCliConfig(
|
||||
) as MikroORMOptions["entities"]
|
||||
|
||||
const normalizedModuleName = kebabCase(moduleName.replace("Service", ""))
|
||||
let databaseName = `medusa-${normalizedModuleName}`
|
||||
|
||||
if (options.databaseName) {
|
||||
databaseName = options.databaseName
|
||||
// @ts-ignore
|
||||
delete options.databaseName
|
||||
}
|
||||
const databaseName = `medusa-${normalizedModuleName}`
|
||||
|
||||
return {
|
||||
clientUrl: `postgres://postgres@localhost/${databaseName}`,
|
||||
type: "postgresql",
|
||||
dbName: databaseName,
|
||||
...options,
|
||||
entities,
|
||||
migrations: {
|
||||
|
||||
Reference in New Issue
Block a user