From d5c9aa15bb0a0d5944268ca0406f2947034c9cb8 Mon Sep 17 00:00:00 2001 From: Tobias Gurtzick Date: Fri, 10 May 2019 19:48:40 +0200 Subject: [PATCH] feat(learning): add db learning This will provide with the base functionality of deterministic migrations in v2 migrations Signed-off-by: Tobias Gurtzick --- lib/driver/shadow.js | 63 +++++++--- lib/executors/versioned/v2.js | 76 ++++++++++++ lib/learn.js | 211 ++++++++++++++++++++++++++++++++++ lib/walker.js | 5 + package-lock.json | 11 +- 5 files changed, 347 insertions(+), 19 deletions(-) create mode 100644 lib/executors/versioned/v2.js create mode 100644 lib/learn.js diff --git a/lib/driver/shadow.js b/lib/driver/shadow.js index 6779aa98..b8dbf56a 100644 --- a/lib/driver/shadow.js +++ b/lib/driver/shadow.js @@ -1,30 +1,34 @@ /** - * The shadow driver is basically a MITM object. Or in other words: - * - * This shadow is very infectius. It infects other objects and overwrite their - * behavior. It gets a shadow part of this object and executes various - * actions. - * - * In our case, it records the execution of methods. - */ + * The shadow driver is basically a MITM object. Or in other words: + * + * This shadow is very infectius. It infects other objects and overwrite their + * behavior. It gets a shadow part of this object and executes various + * actions. + * + * In our case, it records the execution of methods. + */ /** - * 'Infect' the original class - */ + * 'Infect' the original class + */ exports.infect = function (db, intern, ShadowProto) { - db._shadowsHost = {}; - db._shadowProto = {}; + const newDb = {}; + if (db._shadowsHost || db._shadowProto) { + throw new Error("Can't shadow a shadow!"); + } + newDb._shadowsHost = {}; + newDb._shadowProto = {}; for (var prop in db) { if ( typeof ShadowProto[prop] === 'function' && typeof db[prop] === 'function' ) { - db._shadowsHost[prop] = db[prop]; - db._shadowProto[prop] = ShadowProto[prop]; + newDb._shadowsHost[prop] = db[prop]; + newDb._shadowProto[prop] = ShadowProto[prop]; (function (property) { - db[property] = function () { + newDb[property] = function () { var params = arguments; var self = this; @@ -35,8 +39,35 @@ exports.infect = function (db, intern, ShadowProto) { }); }; })(prop); + } else { + newDb[prop] = db[prop]; + } + } + + return newDb; +}; + +/** + * 'Overshadow' the original class + * + * This basically overwrites methods existent from the original + * with a provided replacement. If no replacement was found, it will overwrite + * with an error instead, which needs to be supplied as a fallback. + */ + +exports.overshadow = function (db, ShadowProto, ShadowError) { + const newDb = Object.assign({}, ShadowProto); + + for (var prop in db) { + if ( + typeof ShadowProto[prop] === 'function' && + typeof db[prop] === 'function' + ) { + newDb[prop] = ShadowProto[prop]; + } else if (typeof db[prop] === 'function') { + newDb[prop] = ShadowError(prop); } } - return db; + return newDb; }; diff --git a/lib/executors/versioned/v2.js b/lib/executors/versioned/v2.js new file mode 100644 index 00000000..589e71fa --- /dev/null +++ b/lib/executors/versioned/v2.js @@ -0,0 +1,76 @@ +'use strict'; + +const Promise = require('bluebird'); +const Learn = require('../../learn'); + +const execUnit = { + _extend: (context, type) => { + return { + atomic: function (actions) { + let action = actions[type]; + let reverse = actions[type === 'up' ? 'up' : 'down']; + + if (!action || !reverse) { + return Promise.reject(new Error('invalid operation')); + } + + return action().catch(() => reverse()); + } + }; + }, + + learn: (context, driver, file) => { + const _file = file.get(); + const i = Learn.getInterface(context._driver, driver, context.internals); + + return _file.migrate(i, { + options: context.internals.safeOptions, + seedLink: context.seedLink, + dbm: context.internals.safeOptions.dbmigrate + }); + }, + + up: function (context, driver, file) { + return execUnit.learn(context, driver, file); + + return context.driver + .startMigration() + .then(() => { + const _file = file.get(); + + return _file.up(context.driver, { + options: context.internals.safeOptions, + seedLink: context.seedLink, + dbm: context.internals.safeOptions.dbmigrate + }); + }) + .then(() => { + return Promise.promisify(context.writeMigrationRecord.bind(context))( + file + ); + }) + .then(context.driver.endMigration.bind(context.driver)); + }, + + down: function (context, driver, file) { + return driver + .startMigration() + .then(() => { + const _file = file.get(); + + return _file.down(context.driver, { + options: context.internals.safeOptions, + seedLink: context.seedLink, + dbm: context.internals.safeOptions.dbmigrate + }); + }) + .then(() => { + return Promise.promisify(context.deleteMigrationRecord.bind(context))( + file + ); + }) + .then(context.driver.endMigration.bind(context.driver)); + } +}; + +module.exports = execUnit; diff --git a/lib/learn.js b/lib/learn.js new file mode 100644 index 00000000..2515d7c8 --- /dev/null +++ b/lib/learn.js @@ -0,0 +1,211 @@ +const Promise = require('bluebird'); +const Shadow = require('./driver/shadow'); + +function dummy () { + arguments[arguments.length - 1]('not implemented'); +} + +class STD { + constructor ({ schema, modSchema: mod }) { + this.indizies = schema.i; + this.schema = schema.c; + this.foreign = schema.f; + this.modS = mod.c; + this.modI = mod.i; + this.modF = mod.f; + } + + dropTable (tableName) { + if (this.schema[tableName]) { + this.modS[tableName] = this.schema[tableName]; + delete this.schema[tableName]; + } + + return Promise.resolve(); + } + + createTable (tableName, columnSpec) { + this.schema[tableName] = Object.assign({}, columnSpec); + + return Promise.resolve(); + } + + renameCollection (...args) { + return this.renameTable.apply(this, args); + } + + dropCollection (...args) { + return this.dropTable.apply(this, args); + } + + createCollection (...args) { + return this.createTable.apply(this, args); + } + + removeColumn (tableName, columnName, columnSpec) { + if (this.schema[tableName]) { + this.modS[tableName] = {}; + this.modS[tableName][columnName] = this.schema[tableName][columnName]; + delete this.schema[tableName][columnName]; + } + + return Promise.resolve(); + } + + renameColumn (t, o, n) { + if (this.schema[t]) { + this.schema[t][n] = this.schema[t][o]; + delete this.schema[t][o]; + } + + return Promise.resolve(); + } + + addColumn (t, c, s) { + if (!this.schema[t]) { + throw new Error(`There is no ${t} table in schema!`); + } + this.schema[t] = this.schema[t] || {}; + this.schema[t][c] = s; + + return Promise.resolve(); + } + + checkColumn (t, c) { + if (!this.schema[t]) { + throw new Error(`There is no ${t} table in schema!`); + } + + if (!this.schema[t][c]) { + throw new Error(`There is no ${c} column in schema!`); + } + } + + changeColumn (t, c, s) { + this.checkColumn(t, c); + + this.schema[t][c] = Object.assign(this.schema[t][c], s); + + return Promise.resolve(); + } + + addIndex (t, i, c, u) { + this.checkColumn(t, c); + + if (!this.schema[t][c].indizies) { + this.schema[t][c].indizies = {}; + } + + const index = { t, c }; + + if (u === true) { + index.u = true; + } + + if (!this.indizies[t]) this.indizies[t] = {}; + this.indizies[t][i] = index; + + return Promise.resolve(); + } + + removeIndex (t, _i) { + let i; + if (!_i) { + i = t; + } else { + i = _i; + } + + if (!this.schema[t]) { + throw new Error(`There is no ${t} table in schema!`); + } + + if (!this.indizies[t] || !this.indizies[t][i]) { + throw new Error(`There is no index ${i} in ${t} table!`); + } + + this.modI[i] = this.indizies[t][i]; + delete this.indizies[t][i]; + + return Promise.resolve(); + } + + addForeignKey (t, rt, k, m, r) { + if (!this.schema[t]) { + throw new Error(`There is no ${t} table in schema!`); + } + + if (!this.schema[rt]) { + throw new Error(`There is no ${rt} table in schema!`); + } + + if (!this.foreign[t]) this.foreign[t] = {}; + + this.foreign[t][k] = { t, rt, m }; + + if (r) { + this.foreign[t][k].r = r; + } + + return Promise.resolve(); + } + + removeForeignKey (t, k, o) { + if (!this.schema[t]) { + throw new Error(`There is no ${t} table in schema!`); + } + + if (!this.foreign[t] || !this.foreign[t][k]) { + throw new Error(`There is no foreign key ${k} in ${t} table!`); + } + + delete this.foreign[t][k]; + + return Promise.resolve(); + } + + // + // checkDBMS: dummy, + // + // createDatabase: dummy, + // + // switchDatabase: dummy, + // + // dropDatabase: dummy, + // + // runSql: dummy, +} + +Object.keys(STD.prototype).forEach(method => { + const m = STD.prototype[method]; + STD.prototype[method] = function (...args) { + let cb = args.pop(); + if (typeof cb !== 'function') { + args.push(cb); + cb = undefined; + } + + return m.apply(this, args).asCallback(cb); + }; +}); + +const noLearnError = prop => { + return function () { + throw new Error(`Can't learn function ${prop}`); + }; +}; + +module.exports = { + getInterface: (context, driver, internals) => { + if (context.learnable) { + const _std = new STD(internals); + return Shadow.overshadow( + driver, + Object.assign(_std, context.learnable), + noLearnError + ); + } + + return Shadow.overshadow(driver, new STD(internals), noLearnError); + } +}; diff --git a/lib/walker.js b/lib/walker.js index 603ec278..31eb0868 100644 --- a/lib/walker.js +++ b/lib/walker.js @@ -29,6 +29,8 @@ const Walker = function (driver, directory, mode, intern, prefix) { Promise.promisifyAll(this._driver); this.directory = directory; this.internals = intern; + this.internals.schema = { i: {}, c: {}, f: {} }; + this.internals.modSchema = { i: {}, c: {}, f: {} }; this.mode = mode; if (!this.mode) this.prefix = `static-${prefix}`; @@ -151,6 +153,9 @@ Walker.prototype = { file ); }) + .then(d => { + console.log('iam', this.internals.schema); + }) .nodeify(callback); }, diff --git a/package-lock.json b/package-lock.json index 603a49be..20e1d667 100644 --- a/package-lock.json +++ b/package-lock.json @@ -105,6 +105,7 @@ "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", "dev": true, + "optional": true, "requires": { "kind-of": "^3.0.2", "longest": "^1.0.1", @@ -1485,7 +1486,8 @@ "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true + "dev": true, + "optional": true }, "is-builtin-module": { "version": "1.0.0", @@ -1652,6 +1654,7 @@ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, + "optional": true, "requires": { "is-buffer": "^1.1.5" } @@ -1929,7 +1932,8 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", - "dev": true + "dev": true, + "optional": true }, "lru-cache": { "version": "4.1.5", @@ -2599,7 +2603,8 @@ "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true + "dev": true, + "optional": true }, "request": { "version": "2.88.0",