Initial implementation with just the generate method (#7973)

This commit is contained in:
Harminder Virk
2024-07-09 12:32:02 +05:30
committed by GitHub
parent 366231f658
commit 4b391fc3cf
6 changed files with 594 additions and 13 deletions

View 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)
}
}
}

View File

@@ -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)
})
})

View File

@@ -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)
})
})

View File

@@ -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)
})
})

View File

@@ -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),
},

View File

@@ -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: {