Skip to content

Commit

Permalink
feat(mongo): add support for migrations in mongo driver
Browse files Browse the repository at this point in the history
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
  • Loading branch information
B4nan committed Jul 30, 2022
1 parent dd04f46 commit ffe84a7
Show file tree
Hide file tree
Showing 29 changed files with 1,036 additions and 32 deletions.
35 changes: 32 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,36 @@ await MikroORM.init({
});
```

## MongoDB support

Support for migrations in MongoDB has been added in v5.3. There are some limitations to it:

- uses `@mikro-orm/migrations-mongodb` package
- no nested transaction support
- no schema diffing
- only blank migrations are generated
- use `this.driver` or `this.getCollection()` to manipulate with the database

> Current CLI commands will work with it transparently.
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() } });

// or use `this.driver` to work with the `MongoDriver` API instead
await this.driver.nativeDelete('Book', { foo: true }, { ctx: this.ctx });
}

}
```

## Limitations

### MySQL
Expand Down
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 } from 'mongodb';

export abstract class Migration {

protected ctx?: Transaction;

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;
}

}

0 comments on commit ffe84a7

Please sign in to comment.