From 5ef3a1e343cd32c967634916b0be9cc04b64dcc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ad=C3=A1mek?= Date: Sat, 30 Jul 2022 21:20:56 +0200 Subject: [PATCH] feat(mongo): add support for migrations in mongo driver Adds new `@mikro-orm/migrations-mongodb` package that needs to be explicitly installed. Current CLI commands will work with it transparently. Mongo migrations have some limitations: - no nested transaction support - no schema diffing - only blank migrations are generated - use `this.driver` or `this.getCollection()` to manipulate with the database Closes #295 --- docs/docs/migrations.md | 49 +++- packages/cli/package.json | 4 + .../src/commands/MigrationCommandFactory.ts | 11 +- packages/core/package.json | 4 + packages/core/src/drivers/DatabaseDriver.ts | 4 +- packages/core/src/typings.ts | 4 +- packages/migrations-mongodb/.npmignore | 8 + packages/migrations-mongodb/package.json | 73 +++++ .../src/JSMigrationGenerator.ts | 31 ++ packages/migrations-mongodb/src/Migration.ts | 34 +++ .../src/MigrationGenerator.ts | 47 +++ .../migrations-mongodb/src/MigrationRunner.ts | 37 +++ .../src/MigrationStorage.ts | 58 ++++ packages/migrations-mongodb/src/Migrator.ts | 209 ++++++++++++++ .../src/TSMigrationGenerator.ts | 28 ++ packages/migrations-mongodb/src/index.ts | 12 + packages/migrations-mongodb/src/typings.ts | 6 + .../migrations-mongodb/tsconfig.build.json | 7 + packages/migrations-mongodb/tsconfig.json | 4 + packages/mongodb/package.json | 4 + packages/mongodb/src/MongoDriver.ts | 9 +- packages/mongodb/src/MongoPlatform.ts | 6 + packages/mongodb/src/MongoSchemaGenerator.ts | 2 +- tests/DatabaseDriver.test.ts | 11 +- tests/EntityManager.mongo.test.ts | 2 +- tests/bootstrap.ts | 1 + .../migrations/Migrator.mongo.test.ts | 267 +++++++++++++++++- .../migrations/Migrator.postgres.test.ts | 12 +- .../__snapshots__/Migrator.mongo.test.ts.snap | 138 +++++++++ 29 files changed, 1050 insertions(+), 32 deletions(-) create mode 100644 packages/migrations-mongodb/.npmignore create mode 100644 packages/migrations-mongodb/package.json create mode 100644 packages/migrations-mongodb/src/JSMigrationGenerator.ts create mode 100644 packages/migrations-mongodb/src/Migration.ts create mode 100644 packages/migrations-mongodb/src/MigrationGenerator.ts create mode 100644 packages/migrations-mongodb/src/MigrationRunner.ts create mode 100644 packages/migrations-mongodb/src/MigrationStorage.ts create mode 100644 packages/migrations-mongodb/src/Migrator.ts create mode 100644 packages/migrations-mongodb/src/TSMigrationGenerator.ts create mode 100644 packages/migrations-mongodb/src/index.ts create mode 100644 packages/migrations-mongodb/src/typings.ts create mode 100644 packages/migrations-mongodb/tsconfig.build.json create mode 100644 packages/migrations-mongodb/tsconfig.json create mode 100644 tests/features/migrations/__snapshots__/Migrator.mongo.test.ts.snap diff --git a/docs/docs/migrations.md b/docs/docs/migrations.md index 2a79f770de8a..f962198866d6 100644 --- a/docs/docs/migrations.md +++ b/docs/docs/migrations.md @@ -2,10 +2,9 @@ title: Migrations --- -> To use migrations we need to first install `@mikro-orm/migrations` package. +> To use migrations we need to first install `@mikro-orm/migrations` package for SQL driver or `@mikro-orm/migrations-mongodb` for MongoDB. -MikroORM has integrated support for migrations via [umzug](https://github.com/sequelize/umzug). -It allows us to generate migrations with current schema differences. +MikroORM has integrated support for migrations via [umzug](https://github.com/sequelize/umzug). It allows us to generate migrations with current schema differences. > Since v5, migrations are stored without extension. @@ -284,6 +283,44 @@ await MikroORM.init({ }); ``` +## MongoDB support + +Support for migrations in MongoDB has been added in v5.3. It uses its own package: `@mikro-orm/migrations-mongodb`, and should be otherwise compatible with the current CLI commands. Use `this.driver` or `this.getCollection()` to manipulate with the database. + +### Transactions + +The default options for `Migrator` will use transactions, and those impose some additional requirements in mongo, namely the collections need to exist upfront and we need to run a replicaset. You might want to disable transactions for `migrations: { transactional: false }`. + +```ts +await this.driver.nativeDelete('Book', { foo: true }, { ctx: this.ctx }); +``` + +You need to provide the transaction context manually to your queries, either via the `ctx` option of the driver methods, or via the MongoDB `session` option when using the `this.getCollection()` method. + +```ts +await this.getCollection('Book').updateMany({}, { $set: { updatedAt: new Date() } }, { session: this.ctx }); +``` + +### Migration class + +Example migration in mongo: + +```ts +import { Migration } from '@mikro-orm/migrations-mongodb'; + +export class MigrationTest1 extends Migration { + + async up(): Promise { + // use `this.getCollection()` to work with the mongodb collection directly + await this.getCollection('Book').updateMany({}, { $set: { updatedAt: new Date() } }, { session: this.ctx }); + + // or use `this.driver` to work with the `MongoDriver` API instead + await this.driver.nativeDelete('Book', { foo: true }, { ctx: this.ctx }); + } + +} +``` + ## Limitations ### MySQL @@ -293,3 +330,9 @@ queries automatically, so transactions are not working as expected. - https://github.com/mikro-orm/mikro-orm/issues/217 - https://dev.mysql.com/doc/refman/5.7/en/implicit-commit.html + +### MongoDB + +- no nested transaction support +- no schema diffing +- only blank migrations are generated diff --git a/packages/cli/package.json b/packages/cli/package.json index 641d173cc19d..c25f0bd42523 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -79,6 +79,7 @@ "@mikro-orm/entity-generator": "^5.0.0", "@mikro-orm/mariadb": "^5.0.0", "@mikro-orm/migrations": "^5.0.0", + "@mikro-orm/migrations-mongodb": "^5.0.0", "@mikro-orm/mongodb": "^5.0.0", "@mikro-orm/mysql": "^5.0.0", "@mikro-orm/postgresql": "^5.0.0", @@ -92,6 +93,9 @@ "@mikro-orm/migrations": { "optional": true }, + "@mikro-orm/migrations-mongodb": { + "optional": true + }, "@mikro-orm/seeder": { "optional": true }, diff --git a/packages/cli/src/commands/MigrationCommandFactory.ts b/packages/cli/src/commands/MigrationCommandFactory.ts index 47d3fbd346ff..5ff4d31d9fa1 100644 --- a/packages/cli/src/commands/MigrationCommandFactory.ts +++ b/packages/cli/src/commands/MigrationCommandFactory.ts @@ -1,8 +1,6 @@ import type { ArgumentsCamelCase, Argv, CommandModule } from 'yargs'; import type { Configuration, MikroORM, MikroORMOptions, IMigrator } from '@mikro-orm/core'; import { Utils, colors } from '@mikro-orm/core'; -import type { AbstractSqlDriver } from '@mikro-orm/knex'; -import { SchemaGenerator } from '@mikro-orm/knex'; import type { MigrateOptions } from '@mikro-orm/migrations'; import { CLIHelper } from '../CLIHelper'; @@ -85,9 +83,8 @@ export class MigrationCommandFactory { static async handleMigrationCommand(args: ArgumentsCamelCase, method: MigratorMethod): Promise { const options = { pool: { min: 1, max: 1 } } as Partial; - const orm = await CLIHelper.getORM(undefined, options) as MikroORM; - const { Migrator } = await import('@mikro-orm/migrations'); - const migrator = new Migrator(orm.em); + const orm = await CLIHelper.getORM(undefined, options); + const migrator = orm.getMigrator(); switch (method) { case 'create': @@ -167,8 +164,8 @@ export class MigrationCommandFactory { CLIHelper.dump(colors.green(`${ret.fileName} successfully created`)); } - private static async handleFreshCommand(args: ArgumentsCamelCase, migrator: IMigrator, orm: MikroORM) { - const generator = new SchemaGenerator(orm.em); + private static async handleFreshCommand(args: ArgumentsCamelCase, migrator: IMigrator, orm: MikroORM) { + const generator = orm.getSchemaGenerator(); await generator.dropSchema({ dropMigrationsTable: true }); CLIHelper.dump(colors.green('Dropped schema successfully')); const opts = MigrationCommandFactory.getUpDownOptions(args); diff --git a/packages/core/package.json b/packages/core/package.json index 19ccafc03029..71a067d639c9 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -71,6 +71,7 @@ "@mikro-orm/entity-generator": "^5.0.0", "@mikro-orm/mariadb": "^5.0.0", "@mikro-orm/migrations": "^5.0.0", + "@mikro-orm/migrations-mongodb": "^5.0.0", "@mikro-orm/mongodb": "^5.0.0", "@mikro-orm/mysql": "^5.0.0", "@mikro-orm/postgresql": "^5.0.0", @@ -87,6 +88,9 @@ "@mikro-orm/migrations": { "optional": true }, + "@mikro-orm/migrations-mongodb": { + "optional": true + }, "@mikro-orm/mongodb": { "optional": true }, diff --git a/packages/core/src/drivers/DatabaseDriver.ts b/packages/core/src/drivers/DatabaseDriver.ts index 55e8da57c15a..8719e913d613 100644 --- a/packages/core/src/drivers/DatabaseDriver.ts +++ b/packages/core/src/drivers/DatabaseDriver.ts @@ -63,9 +63,9 @@ export abstract class DatabaseDriver implements IDatabaseD await this.nativeUpdate(coll.owner.constructor.name, coll.owner.__helper!.getPrimaryKey() as FilterQuery, data, options); } - mapResult(result: EntityDictionary, meta: EntityMetadata, populate: PopulateOptions[] = []): EntityData | null { + mapResult(result: EntityDictionary, meta?: EntityMetadata, populate: PopulateOptions[] = []): EntityData | null { if (!result || !meta) { - return null; + return result ?? null; } return this.comparator.mapResult(meta.className, result); diff --git a/packages/core/src/typings.ts b/packages/core/src/typings.ts index 638d00679103..562fae130432 100644 --- a/packages/core/src/typings.ts +++ b/packages/core/src/typings.ts @@ -497,11 +497,11 @@ export interface IMigratorStorage { logMigration(params: Dictionary): Promise; unlogMigration(params: Dictionary): Promise; getExecutedMigrations(): Promise; - ensureTable(): Promise; + ensureTable?(): Promise; setMasterMigration(trx: Transaction): void; unsetMasterMigration(): void; getMigrationName(name: string): string; - getTableName(): { schemaName: string; tableName: string }; + getTableName?(): { schemaName?: string; tableName: string }; } export interface IMigrator { diff --git a/packages/migrations-mongodb/.npmignore b/packages/migrations-mongodb/.npmignore new file mode 100644 index 000000000000..7bfad9eea621 --- /dev/null +++ b/packages/migrations-mongodb/.npmignore @@ -0,0 +1,8 @@ +node_modules +src +tests +coverage +temp +yarn-error.log +data +tsconfig.* diff --git a/packages/migrations-mongodb/package.json b/packages/migrations-mongodb/package.json new file mode 100644 index 000000000000..475cb2326ea2 --- /dev/null +++ b/packages/migrations-mongodb/package.json @@ -0,0 +1,73 @@ +{ + "name": "@mikro-orm/migrations-mongodb", + "version": "5.2.4", + "description": "TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JavaScript.", + "main": "dist/index.js", + "module": "dist/index.mjs", + "typings": "dist/index.d.ts", + "exports": { + "./package.json": "./package.json", + ".": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.mjs" + }, + "require": "./dist/index.js" + } + }, + "repository": { + "type": "git", + "url": "git+ssh://git@github.com/mikro-orm/mikro-orm.git" + }, + "keywords": [ + "orm", + "mongo", + "mongodb", + "mysql", + "mariadb", + "postgresql", + "sqlite", + "sqlite3", + "ts", + "typescript", + "js", + "javascript", + "entity", + "ddd", + "mikro-orm", + "unit-of-work", + "data-mapper", + "identity-map" + ], + "author": "Martin Adámek", + "license": "MIT", + "bugs": { + "url": "https://github.com/mikro-orm/mikro-orm/issues" + }, + "homepage": "https://mikro-orm.io", + "engines": { + "node": ">= 14.0.0" + }, + "scripts": { + "build": "yarn clean && yarn compile && yarn copy", + "postbuild": "yarn gen-esm-wrapper dist/index.js dist/index.mjs", + "clean": "rimraf ./dist", + "compile": "tsc -p tsconfig.build.json", + "copy": "ts-node -T ../../scripts/copy.ts" + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@mikro-orm/mongodb": "^5.2.4", + "fs-extra": "10.1.0", + "mongodb": "^4.8.1", + "umzug": "3.1.1" + }, + "devDependencies": { + "@mikro-orm/core": "^5.2.4" + }, + "peerDependencies": { + "@mikro-orm/core": "^5.0.0" + } +} diff --git a/packages/migrations-mongodb/src/JSMigrationGenerator.ts b/packages/migrations-mongodb/src/JSMigrationGenerator.ts new file mode 100644 index 000000000000..8ea0433f26dd --- /dev/null +++ b/packages/migrations-mongodb/src/JSMigrationGenerator.ts @@ -0,0 +1,31 @@ +import { MigrationGenerator } from './MigrationGenerator'; + +export class JSMigrationGenerator extends MigrationGenerator { + + /** + * @inheritDoc + */ + generateMigrationFile(className: string, diff: { up: string[]; down: string[] }): string { + let ret = `'use strict';\n`; + ret += `Object.defineProperty(exports, '__esModule', { value: true });\n`; + ret += `const { Migration } = require('@mikro-orm/migrations-mongodb');\n\n`; + ret += `class ${className} extends Migration {\n\n`; + ret += ` async up() {\n`; + /* istanbul ignore next */ + diff.up.forEach(sql => ret += this.createStatement(sql, 4)); + ret += ` }\n\n`; + + /* istanbul ignore next */ + if (diff.down.length > 0) { + ret += ` async down() {\n`; + diff.down.forEach(sql => ret += this.createStatement(sql, 4)); + ret += ` }\n\n`; + } + + ret += `}\n`; + ret += `exports.${className} = ${className};\n`; + + return ret; + } + +} diff --git a/packages/migrations-mongodb/src/Migration.ts b/packages/migrations-mongodb/src/Migration.ts new file mode 100644 index 000000000000..eaaf27eb6446 --- /dev/null +++ b/packages/migrations-mongodb/src/Migration.ts @@ -0,0 +1,34 @@ +import type { Configuration, Transaction, EntityName } from '@mikro-orm/core'; +import type { MongoDriver } from '@mikro-orm/mongodb'; +import type { Collection, ClientSession } from 'mongodb'; + +export abstract class Migration { + + protected ctx?: Transaction; + + constructor(protected readonly driver: MongoDriver, + protected readonly config: Configuration) { } + + abstract up(): Promise; + + async down(): Promise { + throw new Error('This migration cannot be reverted'); + } + + isTransactional(): boolean { + return true; + } + + reset(): void { + this.ctx = undefined; + } + + setTransactionContext(ctx: Transaction): void { + this.ctx = ctx; + } + + getCollection(entityName: EntityName): Collection { + return this.driver.getConnection().getCollection(entityName); + } + +} diff --git a/packages/migrations-mongodb/src/MigrationGenerator.ts b/packages/migrations-mongodb/src/MigrationGenerator.ts new file mode 100644 index 000000000000..19fdc56ef79d --- /dev/null +++ b/packages/migrations-mongodb/src/MigrationGenerator.ts @@ -0,0 +1,47 @@ +import { ensureDir, writeFile } from 'fs-extra'; +import type { IMigrationGenerator, MigrationsOptions, NamingStrategy } from '@mikro-orm/core'; +import { Utils } from '@mikro-orm/core'; +import type { MongoDriver } from '@mikro-orm/mongodb'; + +/* istanbul ignore next */ +export abstract class MigrationGenerator implements IMigrationGenerator { + + constructor(protected readonly driver: MongoDriver, + protected readonly namingStrategy: NamingStrategy, + protected readonly options: MigrationsOptions) { } + + /** + * @inheritDoc + */ + async generate(diff: { up: string[]; down: string[] }, path?: string): Promise<[string, string]> { + /* istanbul ignore next */ + const defaultPath = this.options.emit === 'ts' && this.options.pathTs ? this.options.pathTs : this.options.path!; + path = Utils.normalizePath(this.driver.config.get('baseDir'), path ?? defaultPath); + await ensureDir(path); + const timestamp = new Date().toISOString().replace(/[-T:]|\.\d{3}z$/ig, ''); + const className = this.namingStrategy.classToMigrationName(timestamp); + const fileName = `${this.options.fileName!(timestamp)}.${this.options.emit}`; + const ret = this.generateMigrationFile(className, diff); + await writeFile(path + '/' + fileName, ret); + + return [ret, fileName]; + } + + /** + * @inheritDoc + */ + createStatement(query: string, padLeft: number): string { + if (query) { + const padding = ' '.repeat(padLeft); + return `${padding}console.log('${query}');\n`; + } + + return '\n'; + } + + /** + * @inheritDoc + */ + abstract generateMigrationFile(className: string, diff: { up: string[]; down: string[] }): string; + +} diff --git a/packages/migrations-mongodb/src/MigrationRunner.ts b/packages/migrations-mongodb/src/MigrationRunner.ts new file mode 100644 index 000000000000..862d7021b1af --- /dev/null +++ b/packages/migrations-mongodb/src/MigrationRunner.ts @@ -0,0 +1,37 @@ +import type { MigrationsOptions, Transaction } from '@mikro-orm/core'; +import type { MongoDriver } from '@mikro-orm/mongodb'; +import type { Migration } from './Migration'; + +export class MigrationRunner { + + private readonly connection = this.driver.getConnection(); + private masterTransaction?: Transaction; + + constructor(protected readonly driver: MongoDriver, + protected readonly options: MigrationsOptions) { } + + async run(migration: Migration, method: 'up' | 'down'): Promise { + migration.reset(); + + if (!this.options.transactional || !migration.isTransactional()) { + await migration[method](); + } else if (this.masterTransaction) { + migration.setTransactionContext(this.masterTransaction); + await migration[method](); + } else { + await this.connection.transactional(async tx => { + migration.setTransactionContext(tx); + await migration[method](); + }, { ctx: this.masterTransaction }); + } + } + + setMasterMigration(trx: Transaction) { + this.masterTransaction = trx; + } + + unsetMasterMigration() { + delete this.masterTransaction; + } + +} diff --git a/packages/migrations-mongodb/src/MigrationStorage.ts b/packages/migrations-mongodb/src/MigrationStorage.ts new file mode 100644 index 000000000000..a2a6e2c541aa --- /dev/null +++ b/packages/migrations-mongodb/src/MigrationStorage.ts @@ -0,0 +1,58 @@ +import type { Dictionary, MigrationsOptions, Transaction } from '@mikro-orm/core'; +import type { MongoDriver } from '@mikro-orm/mongodb'; +import type { MigrationParams, UmzugStorage } from 'umzug'; +import * as path from 'path'; +import type { MigrationRow } from './typings'; + +export class MigrationStorage implements UmzugStorage { + + private masterTransaction?: Transaction; + + constructor(protected readonly driver: MongoDriver, + protected readonly options: MigrationsOptions) { } + + async executed(): Promise { + const migrations = await this.getExecutedMigrations(); + return migrations.map(({ name }) => `${this.getMigrationName(name)}`); + } + + async logMigration(params: MigrationParams): Promise { + const tableName = this.options.tableName!; + const name = this.getMigrationName(params.name); + await this.driver.nativeInsert(tableName, { name, created_at: new Date() }, { ctx: this.masterTransaction }); + } + + async unlogMigration(params: MigrationParams): Promise { + const tableName = this.options.tableName!; + const withoutExt = this.getMigrationName(params.name); + await this.driver.nativeDelete(tableName, { name: { $in: [params.name, withoutExt] } }, { ctx: this.masterTransaction }); + } + + async getExecutedMigrations(): Promise { + const tableName = this.options.tableName!; + return this.driver.find(tableName, {}, { ctx: this.masterTransaction, orderBy: { _id: 'asc' } as Dictionary }) as Promise; + } + + setMasterMigration(trx: Transaction) { + this.masterTransaction = trx; + } + + unsetMasterMigration() { + delete this.masterTransaction; + } + + /** + * @internal + */ + getMigrationName(name: string) { + const parsedName = path.parse(name); + + if (['.js', '.ts'].includes(parsedName.ext)) { + // strip extension + return parsedName.name; + } + + return name; + } + +} diff --git a/packages/migrations-mongodb/src/Migrator.ts b/packages/migrations-mongodb/src/Migrator.ts new file mode 100644 index 000000000000..1b9e2d1ae316 --- /dev/null +++ b/packages/migrations-mongodb/src/Migrator.ts @@ -0,0 +1,209 @@ +import type { InputMigrations, MigrateDownOptions, MigrateUpOptions, MigrationParams, RunnableMigration } from 'umzug'; +import { Umzug } from 'umzug'; +import { join } from 'path'; +import { ensureDir } from 'fs-extra'; +import type { Constructor, IMigrationGenerator, IMigrator, Transaction } from '@mikro-orm/core'; +import { Utils } from '@mikro-orm/core'; +import type { EntityManager } from '@mikro-orm/mongodb'; +import type { Migration } from './Migration'; +import { MigrationRunner } from './MigrationRunner'; +import { MigrationStorage } from './MigrationStorage'; +import type { MigrateOptions, MigrationResult, MigrationRow, UmzugMigration } from './typings'; +import { TSMigrationGenerator } from './TSMigrationGenerator'; +import { JSMigrationGenerator } from './JSMigrationGenerator'; + +export class Migrator implements IMigrator { + + private umzug!: Umzug; + private runner!: MigrationRunner; + private storage!: MigrationStorage; + private generator!: IMigrationGenerator; + private readonly driver = this.em.getDriver(); + private readonly config = this.em.config; + private readonly options = this.config.get('migrations'); + private readonly absolutePath: string; + + constructor(private readonly em: EntityManager) { + /* istanbul ignore next */ + const key = (this.config.get('tsNode', Utils.detectTsNode()) && this.options.pathTs) ? 'pathTs' : 'path'; + this.absolutePath = Utils.absolutePath(this.options[key]!, this.config.get('baseDir')); + this.createUmzug(); + } + + /** + * @inheritDoc + */ + async createMigration(path?: string): Promise { + await this.ensureMigrationsDirExists(); + const diff = { up: [], down: [] }; + const migration = await this.generator.generate(diff, path); + + return { + fileName: migration[1], + code: migration[0], + diff, + }; + } + + /** + * @inheritDoc + */ + async createInitialMigration(path?: string): Promise { + return this.createMigration(path); + } + + private createUmzug(): void { + this.runner = new MigrationRunner(this.driver, this.options); + this.storage = new MigrationStorage(this.driver, this.options); + + let migrations: InputMigrations = { + glob: join(this.absolutePath, this.options.glob!), + resolve: (params: MigrationParams) => this.resolve(params), + }; + + /* istanbul ignore next */ + if (this.options.migrationsList) { + migrations = this.options.migrationsList.map(migration => this.initialize(migration.class as Constructor, migration.name)); + } + + this.umzug = new Umzug({ + storage: this.storage, + logger: undefined, + migrations, + }); + + const logger = this.config.get('logger'); + this.umzug.on('migrating', event => logger(`Processing '${event.name}'`)); + this.umzug.on('migrated', event => logger(`Applied '${event.name}'`)); + this.umzug.on('reverting', event => logger(`Processing '${event.name}'`)); + this.umzug.on('reverted', event => logger(`Reverted '${event.name}'`)); + + /* istanbul ignore next */ + if (this.options.generator) { + this.generator = new this.options.generator(this.driver, this.config.getNamingStrategy(), this.options); + } else if (this.options.emit === 'js') { + this.generator = new JSMigrationGenerator(this.driver, this.config.getNamingStrategy(), this.options); + } else { + this.generator = new TSMigrationGenerator(this.driver, this.config.getNamingStrategy(), this.options); + } + } + + /** + * @inheritDoc + */ + async getExecutedMigrations(): Promise { + await this.ensureMigrationsDirExists(); + return this.storage.getExecutedMigrations(); + } + + /** + * @inheritDoc + */ + async getPendingMigrations(): Promise { + await this.ensureMigrationsDirExists(); + return this.umzug.pending(); + } + + /** + * @inheritDoc + */ + async up(options?: string | string[] | MigrateOptions): Promise { + return this.runMigrations('up', options); + } + + /** + * @inheritDoc + */ + async down(options?: string | string[] | MigrateOptions): Promise { + return this.runMigrations('down', options); + } + + getStorage(): MigrationStorage { + return this.storage; + } + + protected resolve(params: MigrationParams): RunnableMigration { + const createMigrationHandler = async (method: 'up' | 'down') => { + const migration = await Utils.dynamicImport(params.path!); + const MigrationClass = Object.values(migration)[0] as Constructor; + const instance = new MigrationClass(this.driver, this.config); + + await this.runner.run(instance, method); + }; + + return { + name: this.storage.getMigrationName(params.name), + up: () => createMigrationHandler('up'), + down: () => createMigrationHandler('down'), + }; + } + + /* istanbul ignore next */ + protected initialize(MigrationClass: Constructor, name: string): RunnableMigration { + const instance = new MigrationClass(this.driver, this.config); + + return { + name: this.storage.getMigrationName(name), + up: () => this.runner.run(instance, 'up'), + down: () => this.runner.run(instance, 'down'), + }; + } + + private getMigrationFilename(name: string): string { + name = name.replace(/\.[jt]s$/, ''); + return name.match(/^\d{14}$/) ? this.options.fileName!(name) : name; + } + + private prefix(options?: T): MigrateUpOptions & MigrateDownOptions { + if (Utils.isString(options) || Array.isArray(options)) { + return { migrations: Utils.asArray(options).map(name => this.getMigrationFilename(name)) }; + } + + if (!options) { + return {}; + } + + if (options.migrations) { + options.migrations = options.migrations.map(name => this.getMigrationFilename(name)); + } + + if (options.transaction) { + delete options.transaction; + } + + ['from', 'to'].filter(k => options[k]).forEach(k => options[k] = this.getMigrationFilename(options[k])); + + return options as MigrateUpOptions; + } + + private async runMigrations(method: 'up' | 'down', options?: string | string[] | MigrateOptions) { + await this.ensureMigrationsDirExists(); + + if (!this.options.transactional || !this.options.allOrNothing) { + return this.umzug[method](this.prefix(options as string[])); + } + + if (Utils.isObject(options) && options.transaction) { + return this.runInTransaction(options.transaction, method, options); + } + + return this.driver.getConnection().transactional(trx => this.runInTransaction(trx, method, options)); + } + + private async runInTransaction(trx: Transaction, method: 'up' | 'down', options: string | string[] | undefined | MigrateOptions) { + this.runner.setMasterMigration(trx); + this.storage.setMasterMigration(trx); + const ret = await this.umzug[method](this.prefix(options)); + this.runner.unsetMasterMigration(); + this.storage.unsetMasterMigration(); + + return ret; + } + + private async ensureMigrationsDirExists() { + if (!this.options.migrationsList) { + await ensureDir(this.absolutePath); + } + } + +} diff --git a/packages/migrations-mongodb/src/TSMigrationGenerator.ts b/packages/migrations-mongodb/src/TSMigrationGenerator.ts new file mode 100644 index 000000000000..bb1b61f772a1 --- /dev/null +++ b/packages/migrations-mongodb/src/TSMigrationGenerator.ts @@ -0,0 +1,28 @@ +import { MigrationGenerator } from './MigrationGenerator'; + +export class TSMigrationGenerator extends MigrationGenerator { + + /** + * @inheritDoc + */ + generateMigrationFile(className: string, diff: { up: string[]; down: string[] }): string { + let ret = `import { Migration } from '@mikro-orm/migrations-mongodb';\n\n`; + ret += `export class ${className} extends Migration {\n\n`; + ret += ` async up(): Promise {\n`; + /* istanbul ignore next */ + diff.up.forEach(sql => ret += this.createStatement(sql, 4)); + ret += ` }\n\n`; + + /* istanbul ignore next */ + if (diff.down.length > 0) { + ret += ` async down(): Promise {\n`; + diff.down.forEach(sql => ret += this.createStatement(sql, 4)); + ret += ` }\n\n`; + } + + ret += `}\n`; + + return ret; + } + +} diff --git a/packages/migrations-mongodb/src/index.ts b/packages/migrations-mongodb/src/index.ts new file mode 100644 index 000000000000..e90b824786fc --- /dev/null +++ b/packages/migrations-mongodb/src/index.ts @@ -0,0 +1,12 @@ +/** + * @packageDocumentation + * @module migrations-mongodb + */ +export * from './Migrator'; +export * from './Migration'; +export * from './MigrationRunner'; +export * from './MigrationGenerator'; +export * from './JSMigrationGenerator'; +export * from './TSMigrationGenerator'; +export * from './MigrationStorage'; +export * from './typings'; diff --git a/packages/migrations-mongodb/src/typings.ts b/packages/migrations-mongodb/src/typings.ts new file mode 100644 index 000000000000..afa43d0f3e55 --- /dev/null +++ b/packages/migrations-mongodb/src/typings.ts @@ -0,0 +1,6 @@ +import type { Transaction, MigrationDiff } from '@mikro-orm/core'; + +export type UmzugMigration = { name: string; path?: string }; +export type MigrateOptions = { from?: string | number; to?: string | number; migrations?: string[]; transaction?: Transaction }; +export type MigrationResult = { fileName: string; code: string; diff: MigrationDiff }; +export type MigrationRow = { name: string; executed_at: Date }; diff --git a/packages/migrations-mongodb/tsconfig.build.json b/packages/migrations-mongodb/tsconfig.build.json new file mode 100644 index 000000000000..856db0f2100a --- /dev/null +++ b/packages/migrations-mongodb/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "outDir": "./dist" + }, + "include": ["src/**/*"] +} diff --git a/packages/migrations-mongodb/tsconfig.json b/packages/migrations-mongodb/tsconfig.json new file mode 100644 index 000000000000..52d43eaaa9b9 --- /dev/null +++ b/packages/migrations-mongodb/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*"] +} diff --git a/packages/mongodb/package.json b/packages/mongodb/package.json index c393a7de7265..38b96bb71af5 100644 --- a/packages/mongodb/package.json +++ b/packages/mongodb/package.json @@ -69,6 +69,7 @@ "@mikro-orm/core": "^5.0.0", "@mikro-orm/entity-generator": "^5.0.0", "@mikro-orm/migrations": "^5.0.0", + "@mikro-orm/migrations-mongodb": "^5.0.0", "@mikro-orm/seeder": "^5.0.0" }, "peerDependenciesMeta": { @@ -78,6 +79,9 @@ "@mikro-orm/migrations": { "optional": true }, + "@mikro-orm/migrations-mongodb": { + "optional": true + }, "@mikro-orm/seeder": { "optional": true } diff --git a/packages/mongodb/src/MongoDriver.ts b/packages/mongodb/src/MongoDriver.ts index d7e6a181f5f4..18ec414fd79f 100644 --- a/packages/mongodb/src/MongoDriver.ts +++ b/packages/mongodb/src/MongoDriver.ts @@ -33,7 +33,7 @@ export class MongoDriver extends DatabaseDriver { where = this.renameFields(entityName, where, true); const res = await this.rethrow(this.getConnection('read').find(entityName, where, options.orderBy, options.limit, options.offset, fields, options.ctx)); - return res.map(r => this.mapResult(r, this.metadata.find(entityName)!)!); + return res.map(r => this.mapResult(r, this.metadata.find(entityName))!); } async findOne, P extends string = never>(entityName: string, where: FilterQuery, options: FindOneOptions = { populate: [], orderBy: {} }): Promise | null> { @@ -186,7 +186,12 @@ export class MongoDriver extends DatabaseDriver { } protected buildFields, P extends string = never>(entityName: string, populate: PopulateOptions[], fields?: readonly EntityField[]): string[] | undefined { - const meta = this.metadata.find(entityName)!; + const meta = this.metadata.find(entityName); + + if (!meta) { + return fields as string[]; + } + const lazyProps = meta.props.filter(prop => prop.lazy && !populate.some(p => p.field === prop.name || p.all)); const ret: string[] = []; diff --git a/packages/mongodb/src/MongoPlatform.ts b/packages/mongodb/src/MongoPlatform.ts index dc274d904d20..20d09fa8ba6e 100644 --- a/packages/mongodb/src/MongoPlatform.ts +++ b/packages/mongodb/src/MongoPlatform.ts @@ -24,6 +24,12 @@ export class MongoPlatform extends Platform { return new MongoSchemaGenerator(em ?? driver as any); } + getMigrator(em: EntityManager) { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { Migrator } = require('@mikro-orm/migrations-mongodb'); + return this.config.getCachedService(Migrator, em); + } + normalizePrimaryKey(data: Primary | IPrimaryKey | ObjectId): T { if (data instanceof ObjectId) { return data.toHexString() as T; diff --git a/packages/mongodb/src/MongoSchemaGenerator.ts b/packages/mongodb/src/MongoSchemaGenerator.ts index 86fdd1221996..0bf15e481e1f 100644 --- a/packages/mongodb/src/MongoSchemaGenerator.ts +++ b/packages/mongodb/src/MongoSchemaGenerator.ts @@ -37,7 +37,7 @@ export class MongoSchemaGenerator extends AbstractSchemaGenerator { } async ensureDatabase(): Promise { - return true; + return false; } async refreshDatabase(options: CreateSchemaOptions = {}): Promise { diff --git a/tests/DatabaseDriver.test.ts b/tests/DatabaseDriver.test.ts index 26f5af87ddff..917acc6e58bd 100644 --- a/tests/DatabaseDriver.test.ts +++ b/tests/DatabaseDriver.test.ts @@ -18,7 +18,7 @@ import { DatabaseDriver, EntityManager, EntityRepository, - LockMode, + LockMode, MikroORM, Platform, } from '@mikro-orm/core'; @@ -65,9 +65,10 @@ class Driver extends DatabaseDriver implements IDatabaseDriver { describe('DatabaseDriver', () => { + const config = new Configuration({ type: 'mongo', allowGlobalContext: true } as any, false); + const driver = new Driver(config, []); + test('default validations', async () => { - const config = new Configuration({ type: 'mongo', allowGlobalContext: true } as any, false); - const driver = new Driver(config, []); expect(driver.createEntityManager()).toBeInstanceOf(EntityManager); expect(driver.getPlatform().getRepositoryClass()).toBe(EntityRepository); expect(driver.getPlatform().quoteValue('a')).toBe('a'); @@ -80,4 +81,8 @@ describe('DatabaseDriver', () => { expect(() => driver.getPlatform().getSchemaGenerator(driver)).toThrowError('Driver does not support SchemaGenerator'); }); + test('not supported', async () => { + expect(() => driver.getPlatform().getMigrator({} as any)).toThrowError('Platform1 does not support Migrator'); + }); + }); diff --git a/tests/EntityManager.mongo.test.ts b/tests/EntityManager.mongo.test.ts index 5285fcd583d5..06d98293d0c9 100644 --- a/tests/EntityManager.mongo.test.ts +++ b/tests/EntityManager.mongo.test.ts @@ -394,7 +394,7 @@ describe('EntityManagerMongo', () => { test('transactions', async () => { const god1 = new Author('God1', 'hello@heaven1.god'); await orm.em.begin(); - await orm.em.persist(god1); + await orm.em.persist(god1).flush(); await orm.em.rollback(); const res1 = await orm.em.findOne(Author, { name: 'God1' }); expect(res1).toBeNull(); diff --git a/tests/bootstrap.ts b/tests/bootstrap.ts index 39631288a45e..701f74b1bd73 100644 --- a/tests/bootstrap.ts +++ b/tests/bootstrap.ts @@ -68,6 +68,7 @@ export async function initORMMongo(replicaSet = false) { validate: true, filters: { allowedFooBars: { cond: args => ({ id: { $in: args.allowed } }), entity: ['FooBar'], default: false } }, pool: { min: 1, max: 3 }, + migrations: { path: BASE_DIR + '/../temp/migrations-mongo' }, }); ensureIndexes = false; diff --git a/tests/features/migrations/Migrator.mongo.test.ts b/tests/features/migrations/Migrator.mongo.test.ts index 4d88bb8dad37..d1eab4fa5221 100644 --- a/tests/features/migrations/Migrator.mongo.test.ts +++ b/tests/features/migrations/Migrator.mongo.test.ts @@ -1,10 +1,267 @@ -import { MikroORM } from '@mikro-orm/core'; +(global as any).process.env.FORCE_COLOR = 0; +import { Umzug } from 'umzug'; +import type { MikroORM } from '@mikro-orm/core'; +import { Migration, Migrator } from '@mikro-orm/migrations-mongodb'; +import type { MongoDriver } from '@mikro-orm/mongodb'; +import { remove } from 'fs-extra'; +import { closeReplSets, initORMMongo, mockLogger } from '../../bootstrap'; -describe('Migrator', () => { +class MigrationTest1 extends Migration { - test('not supported [mongodb]', async () => { - const orm = await MikroORM.init({ type: 'mongo', dbName: 'mikro-orm-test', discovery: { warnWhenNoEntities: false } }, false); - expect(() => orm.getMigrator()).toThrowError('MongoPlatform does not support Migrator'); + async up(): Promise { + await this.getCollection('Book').updateMany({}, { $set: { updatedAt: new Date() } }); + await this.driver.nativeDelete('Book', { foo: true }, { ctx: this.ctx }); + } + +} + +class MigrationTest2 extends Migration { + + async up(): Promise { + await this.getCollection('Book').updateMany({}, { $unset: { title: 1 } }, { session: this.ctx }); + await this.driver.nativeDelete('Book', { foo: false }, { ctx: this.ctx }); + } + + isTransactional(): boolean { + return false; + } + +} + +describe('Migrator (mongo)', () => { + + let orm: MikroORM; + + beforeAll(async () => { + orm = await initORMMongo(true); + + const schemaGenerator = orm.getSchemaGenerator(); + await schemaGenerator.refreshDatabase(); + await remove(process.cwd() + '/temp/migrations-mongo'); + }); + + beforeEach(() => orm.config.resetServiceCache()); + + afterAll(async () => { + await orm.close(true); + await closeReplSets(); + }); + + test('generate js schema migration', async () => { + const dateMock = jest.spyOn(Date.prototype, 'toISOString'); + dateMock.mockReturnValue('2019-10-13T21:48:13.382Z'); + const migrationsSettings = orm.config.get('migrations'); + orm.config.set('migrations', { ...migrationsSettings, emit: 'js' }); // Set migration type to js + const migrator = orm.getMigrator(); + const migration = await migrator.createMigration(); + expect(migration).toMatchSnapshot('migration-js-dump'); + orm.config.set('migrations', migrationsSettings); // Revert migration config changes + await remove(process.cwd() + '/temp/migrations-mongo/' + migration.fileName); + }); + + test('generate migration with custom name', async () => { + const dateMock = jest.spyOn(Date.prototype, 'toISOString'); + dateMock.mockReturnValue('2019-10-13T21:48:13.382Z'); + const migrationsSettings = orm.config.get('migrations'); + orm.config.set('migrations', { ...migrationsSettings, fileName: time => `migration-${time}` }); + const migrator = orm.getMigrator(); + const migration = await migrator.createMigration(); + expect(migration).toMatchSnapshot('migration-dump'); + const upMock = jest.spyOn(Umzug.prototype, 'up'); + upMock.mockImplementation(() => void 0 as any); + const downMock = jest.spyOn(Umzug.prototype, 'down'); + downMock.mockImplementation(() => void 0 as any); + await migrator.up(); + await migrator.down(migration.fileName.replace('.ts', '')); + await migrator.up(); + await migrator.down(migration.fileName); + await migrator.up(); + await migrator.down(migration.fileName.replace('migration-', '').replace('.ts', '')); + orm.config.set('migrations', migrationsSettings); // Revert migration config changes + await remove(process.cwd() + '/temp/migrations-mongo/' + migration.fileName); + upMock.mockRestore(); + downMock.mockRestore(); + }); + + test('generate blank migration', async () => { + const dateMock = jest.spyOn(Date.prototype, 'toISOString'); + dateMock.mockReturnValue('2019-10-13T21:48:13.382Z'); + const migrator = orm.getMigrator(); + const migration = await migrator.createMigration(); + expect(migration).toMatchSnapshot('migration-dump'); + await remove(process.cwd() + '/temp/migrations-mongo/' + migration.fileName); + }); + + test('generate initial migration', async () => { + const migrator = orm.getMigrator(); + const spy = jest.spyOn(Migrator.prototype, 'createMigration'); + spy.mockImplementation(); + await migrator.createInitialMigration('abc'); + expect(spy).toBeCalledWith('abc'); + spy.mockRestore(); + }); + + test('run migration', async () => { + const upMock = jest.spyOn(Umzug.prototype, 'up'); + const downMock = jest.spyOn(Umzug.prototype, 'down'); + upMock.mockImplementationOnce(() => void 0 as any); + downMock.mockImplementationOnce(() => void 0 as any); + const migrator = orm.getMigrator(); + await migrator.up(); + expect(upMock).toBeCalledTimes(1); + expect(downMock).toBeCalledTimes(0); + await orm.em.begin(); + await migrator.down({ transaction: orm.em.getTransactionContext() }); + await orm.em.commit(); + expect(upMock).toBeCalledTimes(1); + expect(downMock).toBeCalledTimes(1); + upMock.mockRestore(); + }); + + test('run schema migration without existing migrations folder (GH #907)', async () => { + await remove(process.cwd() + '/temp/migrations-mongo'); + const migrator = orm.getMigrator(); + await migrator.up(); + }); + + test('list executed migrations', async () => { + const migrator = orm.getMigrator(); + const storage = migrator.getStorage(); + + await storage.logMigration({ name: 'test', context: null }); + await expect(storage.getExecutedMigrations()).resolves.toMatchObject([{ name: 'test' }]); + await expect(storage.executed()).resolves.toEqual(['test']); + + await storage.unlogMigration({ name: 'test', context: null }); + await expect(storage.executed()).resolves.toEqual([]); + + await expect(migrator.getPendingMigrations()).resolves.toEqual([]); + }); + + test('runner', async () => { + const migrator = orm.getMigrator(); + // @ts-ignore + const runner = migrator.runner; + + const mock = mockLogger(orm, ['query']); + + const migration1 = new MigrationTest1(orm.em.getDriver(), orm.config); + const spy1 = jest.spyOn(Migration.prototype, 'getCollection'); + mock.mock.calls.length = 0; + await runner.run(migration1, 'up'); + expect(spy1).toBeCalledWith('Book'); + // no logging for collection methods, only for driver ones + expect(mock.mock.calls).toHaveLength(3); + expect(mock.mock.calls[0][0]).toMatch('db.begin()'); + expect(mock.mock.calls[1][0]).toMatch(`db.getCollection('books-table').deleteMany({ foo: true }, { session: '[ClientSession]' })`); + expect(mock.mock.calls[2][0]).toMatch('db.commit()'); + mock.mock.calls.length = 0; + + await expect(runner.run(migration1, 'down')).rejects.toThrowError('This migration cannot be reverted'); + const executed = await migrator.getExecutedMigrations(); + expect(executed).toEqual([]); + + mock.mock.calls.length = 0; + const migration2 = new MigrationTest2(orm.em.getDriver(), orm.config); + await runner.run(migration2, 'up'); + expect(mock.mock.calls).toHaveLength(1); + expect(mock.mock.calls[0][0]).toMatch(`db.getCollection('books-table').deleteMany({ foo: false }, { session: undefined })`); + }); + + test('up/down params [all or nothing enabled]', async () => { + const migrator = orm.getMigrator(); + const path = process.cwd() + '/temp/migrations-mongo'; + + const migration = await migrator.createMigration(path, true); + const migratorMock = jest.spyOn(Migration.prototype, 'down'); + migratorMock.mockImplementation(); + + const mock = mockLogger(orm, ['query']); + + await migrator.up(migration.fileName); + await migrator.down(migration.fileName.replace('Migration', '').replace('.ts', '')); + await migrator.up({ migrations: [migration.fileName] }); + await migrator.down({ from: 0, to: 0 } as any); + await migrator.up({ to: migration.fileName }); + await migrator.up({ from: migration.fileName } as any); + await migrator.down(); + + await remove(path + '/' + migration.fileName); + const calls = mock.mock.calls.map(call => { + return call[0] + .replace(/ \[took \d+ ms]/, '') + .replace(/\[query] /, '') + .replace(/ trx\d+/, 'trx\\d+'); + }); + expect(calls).toMatchSnapshot('all-or-nothing'); + }); + + test('up/down with explicit transaction', async () => { + const migrator = orm.getMigrator(); + const path = process.cwd() + '/temp/migrations-mongo'; + + const dateMock = jest.spyOn(Date.prototype, 'toISOString'); + dateMock.mockReturnValueOnce('2020-09-22T10:00:01.000Z'); + const migration1 = await migrator.createMigration(path, true); + dateMock.mockReturnValueOnce('2020-09-22T10:00:02.000Z'); + const migration2 = await migrator.createMigration(path, true); + const migrationMock = jest.spyOn(Migration.prototype, 'down'); + migrationMock.mockImplementation(); + + const mock = mockLogger(orm, ['query']); + + await orm.em.transactional(async em => { + const ret1 = await migrator.up({ transaction: em.getTransactionContext() }); + const ret2 = await migrator.down({ transaction: em.getTransactionContext() }); + const ret3 = await migrator.down({ transaction: em.getTransactionContext() }); + const ret4 = await migrator.down({ transaction: em.getTransactionContext() }); + expect(ret1).toHaveLength(2); + expect(ret2).toHaveLength(1); + expect(ret3).toHaveLength(1); + expect(ret4).toHaveLength(0); + }); + + await remove(path + '/' + migration1.fileName); + await remove(path + '/' + migration2.fileName); + const calls = mock.mock.calls.map(call => { + return call[0] + .replace(/ \[took \d+ ms]/, '') + .replace(/\[query] /, '') + .replace(/ISODate\('.*'\)/, 'ISODate(...)') + .replace(/ trx\d+/, 'trx_xx'); + }); + expect(calls).toMatchSnapshot('explicit-tx'); + }); + + test('up/down params [all or nothing disabled]', async () => { + const migrator = orm.getMigrator(); + // @ts-ignore + migrator.options.allOrNothing = false; + const path = process.cwd() + '/temp/migrations-mongo'; + + const migration = await migrator.createMigration(path, true); + const migratorMock = jest.spyOn(Migration.prototype, 'down'); + migratorMock.mockImplementation(async () => void 0); + + const mock = mockLogger(orm, ['query']); + + await migrator.up(migration.fileName); + await migrator.down(migration.fileName.replace('Migration', '')); + await migrator.up({ migrations: [migration.fileName] }); + await migrator.down({ from: 0, to: 0 } as any); + await migrator.up({ to: migration.fileName }); + await migrator.up({ from: migration.fileName } as any); + await migrator.down(); + + await remove(path + '/' + migration.fileName); + const calls = mock.mock.calls.map(call => { + return call[0] + .replace(/ \[took \d+ ms]/, '') + .replace(/\[query] /, '') + .replace(/ISODate\('.*'\)/, 'ISODate(...)') + .replace(/ trx\d+/, 'trx_xx'); + }); + expect(calls).toMatchSnapshot('all-or-nothing-disabled'); }); }); diff --git a/tests/features/migrations/Migrator.postgres.test.ts b/tests/features/migrations/Migrator.postgres.test.ts index bab459404ec7..e61ded6b1f35 100644 --- a/tests/features/migrations/Migrator.postgres.test.ts +++ b/tests/features/migrations/Migrator.postgres.test.ts @@ -195,14 +195,14 @@ describe('Migrator (postgres)', () => { const migrator = orm.getMigrator(); expect(migrator.getStorage()).toBeInstanceOf(MigrationStorage); - expect(migrator.getStorage().getTableName()).toEqual({ + expect(migrator.getStorage().getTableName!()).toEqual({ schemaName: 'custom', tableName: 'mikro_orm_migrations', }); // @ts-expect-error private property migrator.options.tableName = 'custom.mikro_orm_migrations'; - expect(migrator.getStorage().getTableName()).toEqual({ + expect(migrator.getStorage().getTableName!()).toEqual({ schemaName: 'custom', tableName: 'mikro_orm_migrations', }); @@ -246,12 +246,12 @@ describe('Migrator (postgres)', () => { const migrator = orm.getMigrator(); const storage = migrator.getStorage(); - await storage.ensureTable(); // creates the table + await storage.ensureTable!(); // creates the table await storage.logMigration({ name: 'test', context: null }); await expect(storage.getExecutedMigrations()).resolves.toMatchObject([{ name: 'test' }]); await expect(storage.executed()).resolves.toEqual(['test']); - await storage.ensureTable(); // table exists, no-op + await storage.ensureTable!(); // table exists, no-op await storage.unlogMigration({ name: 'test', context: null }); await expect(storage.executed()).resolves.toEqual([]); @@ -261,7 +261,7 @@ describe('Migrator (postgres)', () => { test('runner', async () => { await orm.em.getKnex().schema.dropTableIfExists(orm.config.get('migrations').tableName!).withSchema('custom'); const migrator = orm.getMigrator(); - await migrator.getStorage().ensureTable(); + await migrator.getStorage().ensureTable!(); // @ts-ignore const runner = migrator.runner; @@ -416,7 +416,7 @@ test('ensureTable when the schema does not exist', async () => { const storage = orm.getMigrator().getStorage(); const mock = mockLogger(orm); - await storage.ensureTable(); // ensures the schema first + await storage.ensureTable!(); // ensures the schema first expect(mock.mock.calls[0][0]).toMatch(`select table_name, table_schema as schema_name, (select pg_catalog.obj_description(c.oid) from pg_catalog.pg_class c where c.oid = (select ('"' || table_schema || '"."' || table_name || '"')::regclass::oid) and c.relname = table_name) as table_comment from information_schema.tables where "table_schema" not like 'pg_%' and "table_schema" not like 'crdb_%' and "table_schema" not in ('information_schema', 'tiger', 'topology') and table_name != 'geometry_columns' and table_name != 'spatial_ref_sys' and table_type != 'VIEW' order by table_name`); expect(mock.mock.calls[1][0]).toMatch(`select schema_name from information_schema.schemata where "schema_name" not like 'pg_%' and "schema_name" not like 'crdb_%' and "schema_name" not in ('information_schema', 'tiger', 'topology') order by schema_name`); expect(mock.mock.calls[2][0]).toMatch(`create schema "custom2"`); diff --git a/tests/features/migrations/__snapshots__/Migrator.mongo.test.ts.snap b/tests/features/migrations/__snapshots__/Migrator.mongo.test.ts.snap new file mode 100644 index 000000000000..edbb714b774b --- /dev/null +++ b/tests/features/migrations/__snapshots__/Migrator.mongo.test.ts.snap @@ -0,0 +1,138 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Migrator (mongo) generate blank migration: migration-dump 1`] = ` +Object { + "code": "import { Migration } from '@mikro-orm/migrations-mongodb'; + +export class Migration20191013214813 extends Migration { + + async up(): Promise { + } + +} +", + "diff": Object { + "down": Array [], + "up": Array [], + }, + "fileName": "Migration20191013214813.ts", +} +`; + +exports[`Migrator (mongo) generate js schema migration: migration-js-dump 1`] = ` +Object { + "code": "'use strict'; +Object.defineProperty(exports, '__esModule', { value: true }); +const { Migration } = require('@mikro-orm/migrations-mongodb'); + +class Migration20191013214813 extends Migration { + + async up() { + } + +} +exports.Migration20191013214813 = Migration20191013214813; +", + "diff": Object { + "down": Array [], + "up": Array [], + }, + "fileName": "Migration20191013214813.js", +} +`; + +exports[`Migrator (mongo) generate migration with custom name: migration-dump 1`] = ` +Object { + "code": "import { Migration } from '@mikro-orm/migrations-mongodb'; + +export class Migration20191013214813 extends Migration { + + async up(): Promise { + } + +} +", + "diff": Object { + "down": Array [], + "up": Array [], + }, + "fileName": "migration-20191013214813.ts", +} +`; + +exports[`Migrator (mongo) up/down params [all or nothing disabled]: all-or-nothing-disabled 1`] = ` +Array [ + "db.getCollection('mikro_orm_migrations').find({}, { session: undefined }).sort([ [ '_id', 1 ] ]).toArray();", + "db.begin();", + "db.commit();", + "db.getCollection('mikro_orm_migrations').insertOne({ name: 'Migration20191013214813', created_at: ISODate(...) }, { session: undefined });", + "db.getCollection('mikro_orm_migrations').find({}, { session: undefined }).sort([ [ '_id', 1 ] ]).toArray();", + "db.begin();", + "db.commit();", + "db.getCollection('mikro_orm_migrations').deleteMany({ name: { '$in': [ 'Migration20191013214813', 'Migration20191013214813' ] } }, { session: undefined });", + "db.getCollection('mikro_orm_migrations').find({}, { session: undefined }).sort([ [ '_id', 1 ] ]).toArray();", + "db.begin();", + "db.commit();", + "db.getCollection('mikro_orm_migrations').insertOne({ name: 'Migration20191013214813', created_at: ISODate(...) }, { session: undefined });", + "db.getCollection('mikro_orm_migrations').find({}, { session: undefined }).sort([ [ '_id', 1 ] ]).toArray();", + "db.begin();", + "db.commit();", + "db.getCollection('mikro_orm_migrations').deleteMany({ name: { '$in': [ 'Migration20191013214813', 'Migration20191013214813' ] } }, { session: undefined });", + "db.getCollection('mikro_orm_migrations').find({}, { session: undefined }).sort([ [ '_id', 1 ] ]).toArray();", + "db.begin();", + "db.commit();", + "db.getCollection('mikro_orm_migrations').insertOne({ name: 'Migration20191013214813', created_at: ISODate(...) }, { session: undefined });", + "db.getCollection('mikro_orm_migrations').find({}, { session: undefined }).sort([ [ '_id', 1 ] ]).toArray();", + "db.getCollection('mikro_orm_migrations').find({}, { session: undefined }).sort([ [ '_id', 1 ] ]).toArray();", + "db.begin();", + "db.commit();", + "db.getCollection('mikro_orm_migrations').deleteMany({ name: { '$in': [ 'Migration20191013214813', 'Migration20191013214813' ] } }, { session: undefined });", +] +`; + +exports[`Migrator (mongo) up/down params [all or nothing enabled]: all-or-nothing 1`] = ` +Array [ + "db.begin();", + "db.getCollection('mikro_orm_migrations').find({}, { session: '[ClientSession]' }).sort([ [ '_id', 1 ] ]).toArray();", + "db.getCollection('mikro_orm_migrations').insertOne({ name: 'Migration20191013214813', created_at: ISODate('2019-10-13T21:48:13.382Z') }, { session: '[ClientSession]' });", + "db.commit();", + "db.begin();", + "db.getCollection('mikro_orm_migrations').find({}, { session: '[ClientSession]' }).sort([ [ '_id', 1 ] ]).toArray();", + "db.getCollection('mikro_orm_migrations').deleteMany({ name: { '$in': [ 'Migration20191013214813', 'Migration20191013214813' ] } }, { session: '[ClientSession]' });", + "db.commit();", + "db.begin();", + "db.getCollection('mikro_orm_migrations').find({}, { session: '[ClientSession]' }).sort([ [ '_id', 1 ] ]).toArray();", + "db.getCollection('mikro_orm_migrations').insertOne({ name: 'Migration20191013214813', created_at: ISODate('2019-10-13T21:48:13.382Z') }, { session: '[ClientSession]' });", + "db.commit();", + "db.begin();", + "db.getCollection('mikro_orm_migrations').find({}, { session: '[ClientSession]' }).sort([ [ '_id', 1 ] ]).toArray();", + "db.getCollection('mikro_orm_migrations').deleteMany({ name: { '$in': [ 'Migration20191013214813', 'Migration20191013214813' ] } }, { session: '[ClientSession]' });", + "db.commit();", + "db.begin();", + "db.getCollection('mikro_orm_migrations').find({}, { session: '[ClientSession]' }).sort([ [ '_id', 1 ] ]).toArray();", + "db.getCollection('mikro_orm_migrations').insertOne({ name: 'Migration20191013214813', created_at: ISODate('2019-10-13T21:48:13.382Z') }, { session: '[ClientSession]' });", + "db.commit();", + "db.begin();", + "db.getCollection('mikro_orm_migrations').find({}, { session: '[ClientSession]' }).sort([ [ '_id', 1 ] ]).toArray();", + "db.commit();", + "db.begin();", + "db.getCollection('mikro_orm_migrations').find({}, { session: '[ClientSession]' }).sort([ [ '_id', 1 ] ]).toArray();", + "db.getCollection('mikro_orm_migrations').deleteMany({ name: { '$in': [ 'Migration20191013214813', 'Migration20191013214813' ] } }, { session: '[ClientSession]' });", + "db.commit();", +] +`; + +exports[`Migrator (mongo) up/down with explicit transaction: explicit-tx 1`] = ` +Array [ + "db.begin();", + "db.getCollection('mikro_orm_migrations').find({}, { session: '[ClientSession]' }).sort([ [ '_id', 1 ] ]).toArray();", + "db.getCollection('mikro_orm_migrations').insertOne({ name: 'Migration20200922100001', created_at: ISODate(...) }, { session: '[ClientSession]' });", + "db.getCollection('mikro_orm_migrations').insertOne({ name: 'Migration20200922100002', created_at: ISODate(...) }, { session: '[ClientSession]' });", + "db.getCollection('mikro_orm_migrations').find({}, { session: '[ClientSession]' }).sort([ [ '_id', 1 ] ]).toArray();", + "db.getCollection('mikro_orm_migrations').deleteMany({ name: { '$in': [ 'Migration20200922100002', 'Migration20200922100002' ] } }, { session: '[ClientSession]' });", + "db.getCollection('mikro_orm_migrations').find({}, { session: '[ClientSession]' }).sort([ [ '_id', 1 ] ]).toArray();", + "db.getCollection('mikro_orm_migrations').deleteMany({ name: { '$in': [ 'Migration20200922100001', 'Migration20200922100001' ] } }, { session: '[ClientSession]' });", + "db.getCollection('mikro_orm_migrations').find({}, { session: '[ClientSession]' }).sort([ [ '_id', 1 ] ]).toArray();", + "db.commit();", +] +`;