diff --git a/lib/migration/migration.classFactory.js b/lib/migration/migration.classFactory.js new file mode 100644 index 0000000..cb7e2d3 --- /dev/null +++ b/lib/migration/migration.classFactory.js @@ -0,0 +1,48 @@ + +/** + * Inject dependencies in migration + * @param {Ilorm} ilorm The ilorm linked with the Migration class + * @returns {Migration} The Migration class bound with this ilorm instance + */ +function injectDependencies({ ilorm, }) { + const migrationsIndex = []; + + /** + * Migration class + * class to manage migration of data + */ + return class Migration { + /** + * Create a migration + * @param {Number} timestamp The timestamp associate with this migration + * @param {Schema} schema The schema bound with this migration + * @param {Function} up Handler called to apply this migration + * @param {Function} down Handler called to rollback this migration + */ + constructor({ timestamp, schema, up, down, }) { + this.time = timestamp; + this.schema = schema; + this.up = up; + this.down = down; + + migrationsIndex.push(this); + migrationsIndex.sort((migrationA, migrationB) => migrationA.time - migrationB.time); + } + + /** + * Apply this migration + */ + up() { + + } + + /** + * Rollback this migration + */ + down() { + + } + }; +} + +module.exports = injectDependencies; diff --git a/lib/migration/migrationConnector.classFactory.js b/lib/migration/migrationConnector.classFactory.js new file mode 100644 index 0000000..5cffa41 --- /dev/null +++ b/lib/migration/migrationConnector.classFactory.js @@ -0,0 +1,44 @@ +/** + * Inject dependencies to create MigrationConnector + * @param {ilorm} ilorm Ilorm instance bound with the context + * @param {Connector} Connector The connector linked with the Migration Connector + * @returns {MigrationConnector} Return the MigrationConnector + */ +function injectDependencies({ ilorm, Connector, }) { + const { Schema, newModel, } = ilorm; + const migrationSchema = new Schema({ + appliedAt: Schema.date().default(() => Date.now()), + version: Schema.string(), + }); + const modelParams = { + connector: new Connector({ + sourceName: 'ilormMigration', + }), + schema: migrationSchema, + }; + + /** + * MigrationModel class + * Model managed by ilorm to manage migration, only used if the user use migration system + */ + class MigrationModel extends newModel(modelParams) { + /** + * Get last migration + * @returns {MigrationModel} The last migration applied + */ + getLastMigration() { + return this.query() + .appliedAt.useAsSortDesc() + .findOne(); + } + } + + /** + * Manage all migration as a Connector level + */ + return class MigrationConnector { + + }; +} + +module.exports = injectDependencies; diff --git a/lib/schema/baseSchema.class.js b/lib/schema/baseSchema.class.js index 489b710..05d69e7 100644 --- a/lib/schema/baseSchema.class.js +++ b/lib/schema/baseSchema.class.js @@ -23,6 +23,7 @@ class BaseSchema { * @param {Object} options Options to apply to the schema */ constructor(schemaDefinition, options = {}) { + this.pastSchema = []; this.definition = schemaDefinition; this.properties = Object.keys(this.definition); this.options = { @@ -60,6 +61,55 @@ class BaseSchema { return new this(schema.definition, schema.options); } + /** + * Declare the function to run to apply this schema (in schema migration) + * @param {Function} handler The handler to run + * @returns {Schema} Return schema for chained call + */ + up(handler) { + this.onUp = handler; + + return this; + } + + /** + * Declare the function to rollback from this schema (in schema migration) + * @param {Function} handler The handler to run + * @returns {Schema} Return schema for chained call + */ + down(handler) { + this.onDown = handler; + + return this; + } + + /** + * Declare a past version of the current schema + * @param {Number} timestamp The timestamp associate with this schema version + * @param {Object} schema The previous schema definition + * @returns {Schema} Return version of the schema for chained call + */ + version(timestamp, schema = null) { + if (this.currentSchema) { + return this.currentSchema.version(timestamp, schema); + } + + if (!schema) { + this.timestamp = timestamp; + + return this; + } + + const instantiatedSchema = new this.constructor(schema); + + instantiatedSchema.timestamp = timestamp; + instantiatedSchema.currentSchema = this; + + this.pastSchema.push(instantiatedSchema); + this.pastSchema.sort((schemaA, schemaB) => schemaB.timestamp - schemaA.timestamp); + + return instantiatedSchema; + } /** * Return primary key of the current schema diff --git a/spec/common/config.js b/spec/common/config.js index 2509da5..d8a5db0 100644 --- a/spec/common/config.js +++ b/spec/common/config.js @@ -22,5 +22,6 @@ module.exports = { extra: { collectionManager: true, transaction: true, + migration: true, }, }; diff --git a/spec/common/extra/migration.js b/spec/common/extra/migration.js new file mode 100644 index 0000000..defaff4 --- /dev/null +++ b/spec/common/extra/migration.js @@ -0,0 +1,99 @@ +const { expect, } = require('chai'); + +const TIME_V1 = 1581125250; +const TIME_V2 = 1601825250; +const TIME_V3 = 1611825250; + +module.exports = (TestContext) => { + const testContext = new TestContext(); + + describe('Migration', () => { + after(async () => { + await testContext.deleteSource('users'); + + return testContext.finalCleanUp(); + }); + + const { Schema, newModel, } = testContext.ilorm; + + // Always most recent version; + const userSchema = new Schema({ + id: Schema.string(), + firstName: Schema.string(), + lastName: Schema.string(), + age: Schema.number(), + }); + + it('API up / down should work', () => { + userSchema + .version(TIME_V3) + .up((UserModel) => { + UserModel.query() + .stream() + .on('data', (user) => { + const [ firstName, lastName, ] = user.name.split(' '); + + user.firstName = firstName; + user.lastName = lastName; + user.age = parseInt(user.previous.age); + user.save(); + }); + }) + .down((UserModel) => { + UserModel.query() + .stream() + .on('data', (user) => { + user.name = `${user.firstName} ${user.lastName}`; + user.age = `${user.previous.age}`; + user.save(); + }); + }); + }); + + it('API version should work', () => { + userSchema + .version(TIME_V2, { + id: Schema.string(), + name: Schema.string(), + age: Schema.string(), + }) + .version(TIME_V1, { + id: Schema.string(), + name: Schema.string(), + }) + .up(async (UserModel) => { + const user = new UserModel(); + + user.id = '12345'; + user.name = 'Guillaume Daix'; + + await user.save(); + }); + }); + + let UserModel; + + it('Should build db with the most recent schema', async () => { + UserModel = newModel({ + connector: new testContext.Connector({ + sourceName: 'users', + }), + schema: userSchema, + }); + + // Will apply migration in order; + await UserModel.applyMigration(); + + // The migration TIME_V1 insert a user on an old schema, in theory, still in the db; + const user = await UserModel.query().findOne(); + + expect(user.id).to.be.equals('12345'); + expect(user.firstName).to.be.equals('Guillaume'); + expect(user.lastName).to.be.equals('Daix'); + }); + + it('Should rollback on past schema', () => {}); + + it('Should re-up on last schema', () => {}); + }); +}; diff --git a/spec/common/index.js b/spec/common/index.js index 66c942d..c54ea34 100644 --- a/spec/common/index.js +++ b/spec/common/index.js @@ -25,6 +25,7 @@ const TESTS = { extra: { collectionManager: require('./extra/collectionManager'), transaction: require('./extra/transaction'), + migration: require('./extra/migration'), }, };