Skip to content

Commit

Permalink
squash! use dataSource-level migration
Browse files Browse the repository at this point in the history
  • Loading branch information
bajtos committed Nov 22, 2018
1 parent 6ede598 commit d8c52a6
Show file tree
Hide file tree
Showing 9 changed files with 131 additions and 143 deletions.
4 changes: 2 additions & 2 deletions docs/site/Database-migrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down
14 changes: 11 additions & 3 deletions examples/todo/src/migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down
10 changes: 9 additions & 1 deletion packages/repository/src/datasource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand All @@ -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;
}
39 changes: 24 additions & 15 deletions packages/repository/src/mixins/repository.mixin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -184,17 +180,30 @@ export function RepositoryMixin<T extends Class<any>>(superClass: T) {
* @param options Migration options, e.g. whether to update tables
* preserving data or rebuild everything from scratch.
*/
async migrateSchema(options?: SchemaMigrationOptions): Promise<void> {
const repoBindings = this.findByTag('repository');
async migrateSchema(options: SchemaMigrationOptions = {}): Promise<void> {
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<Binding<unknown>>[] = 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<Binding<object>>[] = 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);
}
}
}
Expand Down
1 change: 0 additions & 1 deletion packages/repository/src/repositories/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
11 changes: 1 addition & 10 deletions packages/repository/src/repositories/legacy-juggler-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -79,7 +75,7 @@ export function ensurePromise<T>(p: legacy.PromiseOrVoid<T>): Promise<T> {
* and data source
*/
export class DefaultCrudRepository<T extends Entity, ID>
implements EntityCrudRepository<T, ID>, MigrateableRepository<T> {
implements EntityCrudRepository<T, ID> {
modelClass: juggler.PersistedModelClass;

/**
Expand Down Expand Up @@ -336,9 +332,4 @@ export class DefaultCrudRepository<T extends Entity, ID>
protected toEntities(models: juggler.PersistedModel[]): T[] {
return models.map(m => this.toEntity(m));
}

async migrateSchema(options?: SchemaMigrationOptions): Promise<void> {
const operation = options && options.rebuild ? 'automigrate' : 'autoupdate';
await this.dataSource[operation](this.modelClass.modelName);
}
}
49 changes: 0 additions & 49 deletions packages/repository/src/repositories/migrateable.repository.ts

This file was deleted.

99 changes: 84 additions & 15 deletions packages/repository/test/unit/mixins/repository.mixin.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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<Model> {
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<Model> {}
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<Product, number> {
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) {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
DefaultCrudRepository,
Entity,
EntityNotFoundError,
isMigrateableRepository,
juggler,
ModelDefinition,
} from '../../..';
Expand Down Expand Up @@ -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});
});
});
});

0 comments on commit d8c52a6

Please sign in to comment.