From d8c52a65b3e3d5642ede2bdeff18a6c086ec8c37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Thu, 22 Nov 2018 13:49:03 +0100 Subject: [PATCH] squash! use dataSource-level migration --- docs/site/Database-migrations.md | 4 +- examples/todo/src/migrate.ts | 14 ++- packages/repository/src/datasource.ts | 10 +- .../repository/src/mixins/repository.mixin.ts | 39 +++++--- packages/repository/src/repositories/index.ts | 1 - .../src/repositories/legacy-juggler-bridge.ts | 11 +-- .../repositories/migrateable.repository.ts | 49 --------- .../test/unit/mixins/repository.mixin.unit.ts | 99 ++++++++++++++++--- .../legacy-juggler-bridge.unit.ts | 47 --------- 9 files changed, 131 insertions(+), 143 deletions(-) delete mode 100644 packages/repository/src/repositories/migrateable.repository.ts diff --git a/docs/site/Database-migrations.md b/docs/site/Database-migrations.md index 97fbf6617480..bfedbdea38f7 100644 --- a/docs/site/Database-migrations.md +++ b/docs/site/Database-migrations.md @@ -74,12 +74,12 @@ shown below. import {TodoListApplication} from './application'; export async function migrate(args: string[]) { - const rebuild = args.includes('--rebuild'); + const dropExistingTables = args.includes('--rebuild'); console.log('Migrating schemas (%s)', rebuild ? 'rebuild' : 'update'); const app = new TodoListApplication(); await app.boot(); - await app.migrateSchema({rebuild}); + await app.migrateSchema({dropExistingTables}); } migrate(process.argv).catch(err => { diff --git a/examples/todo/src/migrate.ts b/examples/todo/src/migrate.ts index f1cb22ce6a7a..8271f0713f8f 100644 --- a/examples/todo/src/migrate.ts +++ b/examples/todo/src/migrate.ts @@ -6,12 +6,20 @@ import {TodoListApplication} from './application'; export async function migrate(args: string[]) { - const rebuild = args.includes('--rebuild'); - console.log('Migrating schemas (%s)', rebuild ? 'rebuild' : 'update'); + const dropExistingSchema = args.includes('--rebuild'); + console.log( + 'Migrating schemas (%s)', + dropExistingSchema ? 'rebuild' : 'update', + ); const app = new TodoListApplication(); await app.boot(); - await app.migrateSchema({rebuild}); + await app.migrateSchema({dropExistingSchema}); + + // Connectors usually keep a pool of opened connections, + // this keeps the process running even after all works is done. + // We need to exit explicitly. + process.exit(0); } migrate(process.argv).catch(err => { diff --git a/packages/repository/src/datasource.ts b/packages/repository/src/datasource.ts index 05e6ca3d4293..5332b41fb4cd 100644 --- a/packages/repository/src/datasource.ts +++ b/packages/repository/src/datasource.ts @@ -3,7 +3,7 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {AnyObject} from './common-types'; +import {AnyObject, Options} from './common-types'; import {Connector} from './connectors'; /** @@ -17,3 +17,11 @@ export interface DataSource { // tslint:disable-next-line:no-any [property: string]: any; // Other properties that vary by connectors } + +export interface SchemaMigrationOptions extends Options { + /** + * When set to true, schema migration will drop existing tables and recreate + * them from scratch, removing any existing data along the way. + */ + dropExistingSchema?: boolean; +} diff --git a/packages/repository/src/mixins/repository.mixin.ts b/packages/repository/src/mixins/repository.mixin.ts index cb91ce61c08c..a41dec17357b 100644 --- a/packages/repository/src/mixins/repository.mixin.ts +++ b/packages/repository/src/mixins/repository.mixin.ts @@ -3,16 +3,12 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {BindingScope} from '@loopback/context'; +import {BindingScope, Binding} from '@loopback/context'; import {Application} from '@loopback/core'; import * as debugFactory from 'debug'; import {Class} from '../common-types'; -import { - isMigrateableRepository, - juggler, - Repository, - SchemaMigrationOptions, -} from '../repositories'; +import {juggler, Repository} from '../repositories'; +import {SchemaMigrationOptions} from '../datasource'; const debug = debugFactory('loopback:repository:mixin'); @@ -184,17 +180,30 @@ export function RepositoryMixin>(superClass: T) { * @param options Migration options, e.g. whether to update tables * preserving data or rebuild everything from scratch. */ - async migrateSchema(options?: SchemaMigrationOptions): Promise { - const repoBindings = this.findByTag('repository'); + async migrateSchema(options: SchemaMigrationOptions = {}): Promise { + const operation = options.dropExistingSchema + ? 'automigrate' + : 'autoupdate'; - for (const b of repoBindings) { - const repo = await this.get(b.key); + // Instantiate all repositories to ensure models are registered & attached + // to their datasources + const repoBindings: Readonly>[] = this.findByTag( + 'repository', + ); + await Promise.all(repoBindings.map(b => this.get(b.key))); - if (isMigrateableRepository(repo)) { - debug('Migrating repository %s', b.key); - await repo.migrateSchema(options); + // Look up all datasources and update/migrate schemas one by one + const dsBindings: Readonly>[] = this.findByTag( + 'datasource', + ); + for (const b of dsBindings) { + const ds = await this.get(b.key); + + if (operation in ds && typeof ds[operation] === 'function') { + debug('Migrating dataSource %s', b.key); + await ds[operation](); } else { - debug('Skipping migration of repository %s', b.key); + debug('Skipping migration of dataSource %s', b.key); } } } diff --git a/packages/repository/src/repositories/index.ts b/packages/repository/src/repositories/index.ts index f783fe1107d0..60175cd87ba5 100644 --- a/packages/repository/src/repositories/index.ts +++ b/packages/repository/src/repositories/index.ts @@ -8,4 +8,3 @@ export * from './legacy-juggler-bridge'; export * from './kv.repository.bridge'; export * from './repository'; export * from './constraint-utils'; -export * from './migrateable.repository'; diff --git a/packages/repository/src/repositories/legacy-juggler-bridge.ts b/packages/repository/src/repositories/legacy-juggler-bridge.ts index 1709826bd9e9..73a095bac6dc 100644 --- a/packages/repository/src/repositories/legacy-juggler-bridge.ts +++ b/packages/repository/src/repositories/legacy-juggler-bridge.ts @@ -28,10 +28,6 @@ import { } from '../relations'; import {resolveType} from '../type-resolver'; import {EntityCrudRepository} from './repository'; -import { - MigrateableRepository, - SchemaMigrationOptions, -} from './migrateable.repository'; export namespace juggler { export import DataSource = legacy.DataSource; @@ -79,7 +75,7 @@ export function ensurePromise(p: legacy.PromiseOrVoid): Promise { * and data source */ export class DefaultCrudRepository - implements EntityCrudRepository, MigrateableRepository { + implements EntityCrudRepository { modelClass: juggler.PersistedModelClass; /** @@ -336,9 +332,4 @@ export class DefaultCrudRepository protected toEntities(models: juggler.PersistedModel[]): T[] { return models.map(m => this.toEntity(m)); } - - async migrateSchema(options?: SchemaMigrationOptions): Promise { - const operation = options && options.rebuild ? 'automigrate' : 'autoupdate'; - await this.dataSource[operation](this.modelClass.modelName); - } } diff --git a/packages/repository/src/repositories/migrateable.repository.ts b/packages/repository/src/repositories/migrateable.repository.ts deleted file mode 100644 index 30a719a83a8b..000000000000 --- a/packages/repository/src/repositories/migrateable.repository.ts +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright IBM Corp. 2017,2018. All Rights Reserved. -// Node module: @loopback/repository -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -import {Options} from '../common-types'; -import {Model} from '../model'; -import {Repository} from './repository'; - -export interface SchemaMigrationOptions extends Options { - /** - * When set to true, schema migration will drop existing tables and recreate - * them from scratch, removing any existing data along the way. - */ - rebuild?: boolean; -} - -/** - * A repository capable of database schema migration (auto-update/auto-migrate). - */ -export interface MigrateableRepository extends Repository { - /** - * Update or recreate the database schema. - * - * **WARNING**: By default, `migrateSchema()` will attempt to preserve data - * while updating the schema in your target database, but this is not - * guaranteed to be safe. - * - * Please check the documentation for your specific connector(s) for - * a detailed breakdown of behaviors for automigrate! - * - * @param options Migration options, e.g. whether to update tables - * preserving data or rebuild everything from scratch. - */ - migrateSchema(options?: SchemaMigrationOptions): Promise; -} - -/** - * A type guard for detecting repositories implementing MigratableRepository - * interface. - * - * @param repo The repository instance to check. - */ -export function isMigrateableRepository( - // tslint:disable-next-line:no-any - repo: any, -): repo is MigrateableRepository { - return typeof repo.migrateSchema === 'function'; -} diff --git a/packages/repository/test/unit/mixins/repository.mixin.unit.ts b/packages/repository/test/unit/mixins/repository.mixin.unit.ts index 8d3a8ea719ba..bbe0cf5d071e 100644 --- a/packages/repository/test/unit/mixins/repository.mixin.unit.ts +++ b/packages/repository/test/unit/mixins/repository.mixin.unit.ts @@ -3,16 +3,17 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {Application, Component} from '@loopback/core'; +import {Application, Component, BindingScope} from '@loopback/core'; import {expect, sinon} from '@loopback/testlab'; import { Class, juggler, - MigrateableRepository, - Model, Repository, RepositoryMixin, + DataSource, + Entity, } from '../../../'; +import {DefaultCrudRepository, ModelDefinition} from '../../../src'; // tslint:disable:no-any @@ -70,27 +71,95 @@ describe('RepositoryMixin', () => { }); context('migrateSchema', () => { + let app: AppWithRepoMixin; + let migrateStub: sinon.SinonStub; + let updateStub: sinon.SinonStub; + let DataSourceStub: typeof juggler.DataSource; + + beforeEach(setupTestHelpers); + it('is a method provided by the mixin', () => { - const myApp = new AppWithRepoMixin(); - expect(typeof myApp.migrateSchema).to.be.eql('function'); + expect(typeof app.migrateSchema).to.be.eql('function'); }); - it('it migrates all migrateable repositories', async () => { - const app = new AppWithRepoMixin(); + it('calls autoupdate on registered datasources', async () => { + app.dataSource(DataSourceStub); + + await app.migrateSchema({dropExistingSchema: false}); + + sinon.assert.called(updateStub); + sinon.assert.notCalled(migrateStub); + }); + + it('calls automigrate on registered datasources', async () => { + app.dataSource(DataSourceStub); + + await app.migrateSchema({dropExistingSchema: true}); + + sinon.assert.called(migrateStub); + sinon.assert.notCalled(updateStub); + }); - const migrateStub = sinon.stub().resolves(); - class MigrateableRepo implements MigrateableRepository { - migrateSchema = migrateStub; + it('skips datasources not implementing schema migrations', async () => { + class OtherDataSource implements DataSource { + name: string = 'other'; + connector = undefined; + settings = {}; } - app.repository(MigrateableRepo); - class OtherRepo implements Repository {} - app.repository(OtherRepo); + // Bypass app.dataSource type checks and bind a custom datasource class + app + .bind('datasources.other') + .toClass(OtherDataSource) + .tag('datasource') + .inScope(BindingScope.SINGLETON); - await app.migrateSchema({rebuild: true}); + await app.migrateSchema({dropExistingSchema: true}); + }); + + it('ensures models are attached to datasources', async () => { + let modelsMigrated = ['no models were migrated']; + + const ds = new juggler.DataSource({name: 'db', connector: 'memory'}); + // FIXME(bajtos) typings for connectors are missing autoupdate/autoupgrade + (ds.connector as any).automigrate = function( + models: string[], + cb: Function, + ) { + modelsMigrated = models; + cb(); + }; + app.dataSource(ds); + + class Product extends Entity { + static definition = new ModelDefinition('Product').addProperty('id', { + type: 'number', + id: true, + }); + } + class ProductRepository extends DefaultCrudRepository { + constructor() { + super(Product, ds); + } + } + app.repository(ProductRepository); + + await app.migrateSchema({dropExistingSchema: true}); - sinon.assert.calledWith(migrateStub, {rebuild: true}); + expect(modelsMigrated).to.eql(['Product']); }); + + function setupTestHelpers() { + app = new AppWithRepoMixin(); + + migrateStub = sinon.stub().resolves(); + updateStub = sinon.stub().resolves(); + + DataSourceStub = class extends juggler.DataSource { + automigrate = migrateStub; + autoupdate = updateStub; + }; + } }); class AppWithRepoMixin extends RepositoryMixin(Application) {} diff --git a/packages/repository/test/unit/repositories/legacy-juggler-bridge.unit.ts b/packages/repository/test/unit/repositories/legacy-juggler-bridge.unit.ts index 83716b11f0ca..2afe3af07664 100644 --- a/packages/repository/test/unit/repositories/legacy-juggler-bridge.unit.ts +++ b/packages/repository/test/unit/repositories/legacy-juggler-bridge.unit.ts @@ -9,7 +9,6 @@ import { DefaultCrudRepository, Entity, EntityNotFoundError, - isMigrateableRepository, juggler, ModelDefinition, } from '../../..'; @@ -325,50 +324,4 @@ describe('DefaultCrudRepository', () => { const ok = await repo.exists(note1.id); expect(ok).to.be.true(); }); - - context('schema migration', () => { - it('provides migrateSchema() API', () => { - const repo = new DefaultCrudRepository(Note, ds); - expect(repo) - .to.have.property('migrateSchema') - .type('function'); - expect(isMigrateableRepository(repo)).to.be.true(); - }); - - it('performs non-destructive update by default ', async () => { - const repo = new DefaultCrudRepository(Note, ds); - const autoupdateStub = sinon.stub().resolves(); - ds.autoupdate = autoupdateStub; - - await repo.migrateSchema(); - - sinon.assert.calledWith(autoupdateStub, 'Note'); - }); - - it('provides an option to perform destructive rebuild', async () => { - const repo = new DefaultCrudRepository(Note, ds); - const automigrateStub = sinon.stub().resolves(); - ds.automigrate = automigrateStub; - - await repo.migrateSchema({rebuild: true}); - - sinon.assert.calledWith(automigrateStub, 'Note'); - }); - - it('succeeds when the connector does not implement autoupdate', async () => { - const repo = new DefaultCrudRepository(Note, ds); - // tslint:disable-next-line:no-any - (ds.connector as any).autoupdate = undefined; - - await repo.migrateSchema(); - }); - - it('succeeds when the connector does not implement automigrate', async () => { - const repo = new DefaultCrudRepository(Note, ds); - // tslint:disable-next-line:no-any - (ds.connector as any).automigrate = undefined; - - await repo.migrateSchema({rebuild: true}); - }); - }); });