Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(mongo): add support for migrations in mongo driver #3347

Merged
merged 1 commit into from
Jul 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
49 changes: 46 additions & 3 deletions docs/docs/migrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<void> {
// 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
Expand All @@ -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
4 changes: 4 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -92,6 +93,9 @@
"@mikro-orm/migrations": {
"optional": true
},
"@mikro-orm/migrations-mongodb": {
"optional": true
},
"@mikro-orm/seeder": {
"optional": true
},
Expand Down
11 changes: 4 additions & 7 deletions packages/cli/src/commands/MigrationCommandFactory.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -85,9 +83,8 @@ export class MigrationCommandFactory {

static async handleMigrationCommand(args: ArgumentsCamelCase<Options>, method: MigratorMethod): Promise<void> {
const options = { pool: { min: 1, max: 1 } } as Partial<MikroORMOptions>;
const orm = await CLIHelper.getORM(undefined, options) as MikroORM<AbstractSqlDriver>;
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':
Expand Down Expand Up @@ -167,8 +164,8 @@ export class MigrationCommandFactory {
CLIHelper.dump(colors.green(`${ret.fileName} successfully created`));
}

private static async handleFreshCommand(args: ArgumentsCamelCase<Options>, migrator: IMigrator, orm: MikroORM<AbstractSqlDriver>) {
const generator = new SchemaGenerator(orm.em);
private static async handleFreshCommand(args: ArgumentsCamelCase<Options>, 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);
Expand Down
4 changes: 4 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -87,6 +88,9 @@
"@mikro-orm/migrations": {
"optional": true
},
"@mikro-orm/migrations-mongodb": {
"optional": true
},
"@mikro-orm/mongodb": {
"optional": true
},
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/drivers/DatabaseDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,9 @@ export abstract class DatabaseDriver<C extends Connection> implements IDatabaseD
await this.nativeUpdate<T>(coll.owner.constructor.name, coll.owner.__helper!.getPrimaryKey() as FilterQuery<T>, data, options);
}

mapResult<T>(result: EntityDictionary<T>, meta: EntityMetadata<T>, populate: PopulateOptions<T>[] = []): EntityData<T> | null {
mapResult<T>(result: EntityDictionary<T>, meta?: EntityMetadata<T>, populate: PopulateOptions<T>[] = []): EntityData<T> | null {
if (!result || !meta) {
return null;
return result ?? null;
}

return this.comparator.mapResult(meta.className, result);
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/typings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -497,11 +497,11 @@ export interface IMigratorStorage {
logMigration(params: Dictionary): Promise<void>;
unlogMigration(params: Dictionary): Promise<void>;
getExecutedMigrations(): Promise<MigrationRow[]>;
ensureTable(): Promise<void>;
ensureTable?(): Promise<void>;
setMasterMigration(trx: Transaction): void;
unsetMasterMigration(): void;
getMigrationName(name: string): string;
getTableName(): { schemaName: string; tableName: string };
getTableName?(): { schemaName?: string; tableName: string };
}

export interface IMigrator {
Expand Down
8 changes: 8 additions & 0 deletions packages/migrations-mongodb/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
node_modules
src
tests
coverage
temp
yarn-error.log
data
tsconfig.*
73 changes: 73 additions & 0 deletions packages/migrations-mongodb/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
31 changes: 31 additions & 0 deletions packages/migrations-mongodb/src/JSMigrationGenerator.ts
Original file line number Diff line number Diff line change
@@ -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;
}

}
34 changes: 34 additions & 0 deletions packages/migrations-mongodb/src/Migration.ts
Original file line number Diff line number Diff line change
@@ -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<ClientSession>;

constructor(protected readonly driver: MongoDriver,
protected readonly config: Configuration) { }

abstract up(): Promise<void>;

async down(): Promise<void> {
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<any>): Collection {
return this.driver.getConnection().getCollection(entityName);
}

}
47 changes: 47 additions & 0 deletions packages/migrations-mongodb/src/MigrationGenerator.ts
Original file line number Diff line number Diff line change
@@ -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;

}
37 changes: 37 additions & 0 deletions packages/migrations-mongodb/src/MigrationRunner.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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;
}

}