diff --git a/src/Schema/chain.js b/src/Schema/chain.js new file mode 100644 index 00000000..582149f1 --- /dev/null +++ b/src/Schema/chain.js @@ -0,0 +1,329 @@ +'use strict' + +/** + * adonis-schema + * + * (c) Harminder Virk + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. +*/ + +const GE = require('@adonisjs/generic-exceptions') + +class SchemaChain { + constructor () { + this._deferredActions = [] + this._scheduleFn = null + } + + /** + * Select schema to be used with postgreSQL. + * + * @method withSchema + * + * @param {String} schema + * + * @chainable + */ + withSchema (schema) { + this._deferredActions.push({ name: 'withSchema', args: [schema] }) + return this + } + + /** + * Create a extension. + * + * NOTE: This action is deferred + * + * @method createExtension + * + * @param {String} extensionName + * + * @return {void} + */ + createExtension (extensionName) { + this._deferredActions.push({ name: 'createExtension', args: [extensionName] }) + } + + /** + * Create a extension if not already exists. + * + * NOTE: This action is deferred + * + * @method createExtensionIfNotExists + * + * @param {String} extensionName + * + * @return {void} + */ + createExtensionIfNotExists (extensionName) { + this._deferredActions.push({ name: 'createExtensionIfNotExists', args: [extensionName] }) + } + + /** + * Create a new table. + * + * NOTE: This action is deferred + * + * @method createTable + * + * @param {String} tableName + * @param {Function} callback + * + * @return {void} + */ + createTable (tableName, callback) { + this._deferredActions.push({ name: 'createTable', args: [tableName, callback] }) + } + + /** + * Create a new table if not already exists. + * + * NOTE: This action is deferred + * + * @method createTableIfNotExists + * + * @param {String} tableName + * @param {Function} callback + * + * @return {void} + */ + createTableIfNotExists (tableName, callback) { + this._deferredActions.push({ name: 'createTableIfNotExists', args: [tableName, callback] }) + } + + /** + * Rename existing table. + * + * NOTE: This action is deferred + * + * @method renameTable + * + * @param {String} fromTable + * @param {String} toTable + * + * @return {void} + */ + renameTable (fromTable, toTable) { + this._deferredActions.push({ name: 'renameTable', args: [fromTable, toTable] }) + } + + /** + * Drop existing extension. + * + * NOTE: This action is deferred + * + * @method dropExtension + * + * @param {String} extensionName + * + * @return {void} + */ + dropExtension (extensionName) { + this._deferredActions.push({ name: 'dropExtension', args: [extensionName] }) + } + + /** + * Drop extension only if it exists. + * + * NOTE: This action is deferred + * + * @method dropExtensionIfExists + * + * @param {String} extensionName + * + * @return {void} + */ + dropExtensionIfExists (extensionName) { + this._deferredActions.push({ name: 'dropExtensionIfExists', args: [extensionName] }) + } + + /** + * Drop existing table. + * + * NOTE: This action is deferred + * + * @method dropTable + * + * @param {String} tableName + * + * @return {void} + */ + dropTable (tableName) { + this._deferredActions.push({ name: 'dropTable', args: [tableName] }) + } + + /** + * Drop table only if it exists. + * + * NOTE: This action is deferred + * + * @method dropTableIfExists + * + * @param {String} tableName + * + * @return {void} + */ + dropTableIfExists (tableName) { + this._deferredActions.push({ name: 'dropTableIfExists', args: [tableName] }) + } + + /** + * Select table for altering it. + * + * NOTE: This action is deferred + * + * @method table + * + * @param {String} tableName + * @param {Function} callback + * + * @return {void} + */ + table (tableName, callback) { + this._deferredActions.push({ name: 'table', args: [tableName, callback] }) + } + + /* istanbul ignore next */ + /** + * Run a raw SQL statement + * + * @method raw + * + * @param {String} statement + * + * @return {Object} + * + * @return {void} + */ + raw (statement) { + this._deferredActions.push({ name: 'raw', args: [statement] }) + return this + } + + /** + * Schedule a method to be executed in sequence with migrations + * + * @method schedule + * + * @param {Function} fn + * + * @return {void} + */ + schedule (fn) { + if (typeof (fn) !== 'function') { + throw GE.InvalidArgumentException.invalidParameter(`this.schedule expects 1st argument to be a function`) + } + this._scheduleFn = fn + } + + /** + * Alias for @ref('Schema.table') + * + * @method alter + */ + alter (...args) { + return this.table(...args) + } + + /** + * Alias for @ref('Schema.createTable') + * + * @method create + */ + create (...args) { + return this.createTable(...args) + } + + /** + * Alias for @ref('Schema.createTableIfNotExists') + * + * @method createIfNotExists + */ + createIfNotExists (...args) { + return this.createTableIfNotExists(...args) + } + + /** + * Alias for @ref('Schema.dropTable') + * + * @method drop + */ + drop (...args) { + return this.dropTable(...args) + } + + /** + * Alias for @ref('Schema.dropTableIfExists') + * + * @method dropIfExists + */ + dropIfExists (...args) { + return this.dropTableIfExists(...args) + } + + /** + * Alias for @ref('Schema.renameTable') + * + * @method rename + */ + rename (...args) { + return this.renameTable(...args) + } + + /** + * Returns the SQL query for all the actions. + * + * @method toString + * + * @return {String} + */ + toString (schema) { + this._deferredActions.forEach((action) => (schema[action.name](...action.args))) + return schema.toString() + } + + /** + * Executes the deferred actions on a single chain. This method will + * rollback the trx on error. + * + * @method execute + * + * @param {Object} trx + * + * @return {void} + */ + async execute (trx) { + /** + * If schedule fn is defined, then execute it. Within a chain a user + * can never have `schedule` and `deferredActions` together. + */ + if (typeof (this._scheduleFn) === 'function') { + try { + await this._scheduleFn(trx) + } catch (error) { + trx.rollback() + throw error + } + return + } + + const schema = trx.schema + + /** + * Looping over all the deferred actions + */ + this._deferredActions.forEach((action) => (schema[action.name](...action.args))) + + try { + await schema + this._deferredActions = [] + } catch (error) { + trx.rollback() + throw error + } + } +} + +module.exports = SchemaChain diff --git a/src/Schema/index.js b/src/Schema/index.js index 6136255d..c5649ebd 100644 --- a/src/Schema/index.js +++ b/src/Schema/index.js @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -const GE = require('@adonisjs/generic-exceptions') +const SchemaChain = require('./chain') /** * The schema is used to define SQL table schemas. This makes @@ -26,7 +26,7 @@ const GE = require('@adonisjs/generic-exceptions') class Schema { constructor (Database) { this.db = Database.connection(this.constructor.connection) - this._deferredActions = [] + this._chains = [] } /** @@ -62,216 +62,6 @@ class Schema { return this.db.fn } - /** - * Select schema to be used with postgreSQL. - * - * @method withSchema - * - * @param {String} schema - * - * @chainable - */ - withSchema (schema) { - this.schema.withSchema(schema) - return this - } - - /** - * Create a extension. - * - * NOTE: This action is deferred - * - * @method createExtension - * - * @param {String} extensionName - * - * @chainable - */ - createExtension (extensionName) { - this._deferredActions.push({ name: 'createExtension', args: [extensionName] }) - return this - } - - /** - * Create a extension if not already exists. - * - * NOTE: This action is deferred - * - * @method createExtensionIfNotExists - * - * @param {String} extensionName - * - * @chainable - */ - createExtensionIfNotExists (extensionName) { - this._deferredActions.push({ name: 'createExtensionIfNotExists', args: [extensionName] }) - return this - } - - /** - * Create a new table. - * - * NOTE: This action is deferred - * - * @method createTable - * - * @param {String} tableName - * @param {Function} callback - * - * @chainable - */ - createTable (tableName, callback) { - this._deferredActions.push({ name: 'createTable', args: [tableName, callback] }) - return this - } - - /** - * Create a new table if not already exists. - * - * NOTE: This action is deferred - * - * @method createTableIfNotExists - * - * @param {String} tableName - * @param {Function} callback - * - * @chainable - */ - createTableIfNotExists (tableName, callback) { - this._deferredActions.push({ name: 'createTableIfNotExists', args: [tableName, callback] }) - return this - } - - /** - * Rename existing table. - * - * NOTE: This action is deferred - * - * @method renameTable - * - * @param {String} fromTable - * @param {String} toTable - * - * @chainable - */ - renameTable (fromTable, toTable) { - this._deferredActions.push({ name: 'renameTable', args: [fromTable, toTable] }) - return this - } - - /** - * Drop existing extension. - * - * NOTE: This action is deferred - * - * @method dropExtension - * - * @param {String} extensionName - * - * @chainable - */ - dropExtension (extensionName) { - this._deferredActions.push({ name: 'dropExtension', args: [extensionName] }) - return this - } - - /** - * Drop extension only if it exists. - * - * NOTE: This action is deferred - * - * @method dropExtensionIfExists - * - * @param {String} extensionName - * - * @chainable - */ - dropExtensionIfExists (extensionName) { - this._deferredActions.push({ name: 'dropExtensionIfExists', args: [extensionName] }) - return this - } - - /** - * Drop existing table. - * - * NOTE: This action is deferred - * - * @method dropTable - * - * @param {String} tableName - * - * @chainable - */ - dropTable (tableName) { - this._deferredActions.push({ name: 'dropTable', args: [tableName] }) - return this - } - - /** - * Drop table only if it exists. - * - * NOTE: This action is deferred - * - * @method dropTableIfExists - * - * @param {String} tableName - * - * @chainable - */ - dropTableIfExists (tableName) { - this._deferredActions.push({ name: 'dropTableIfExists', args: [tableName] }) - return this - } - - /** - * Select table for altering it. - * - * NOTE: This action is deferred - * - * @method table - * - * @param {String} tableName - * @param {Function} callback - * - * @chainable - */ - table (tableName, callback) { - this._deferredActions.push({ name: 'table', args: [tableName, callback] }) - return this - } - - /* istanbul ignore next */ - /** - * Run a raw SQL statement - * - * @method raw - * - * @param {String} statement - * - * @return {Object} - */ - raw (statement) { - this._deferredActions.push({ name: 'raw', args: [statement] }) - return this - } - - /** - * Schedule a method to be executed in sequence with migrations - * - * @method schedule - * - * @param {Function} fn - * - * @chainable - */ - schedule (fn) { - if (typeof (fn) !== 'function') { - throw GE.InvalidArgumentException.invalidParameter(`this.schedule expects 1st argument to be a function`) - } - this._deferredActions.push({ name: 'schedule', args: [fn] }) - return this - } - /** * Returns a boolean indicating if a table * already exists or not @@ -299,61 +89,7 @@ class Schema { * @return {Boolean} */ hasColumn (tableName, columnName) { - return this.schema.hasTable(tableName, columnName) - } - - /** - * Alias for @ref('Schema.table') - * - * @method alter - */ - alter (...args) { - return this.table(...args) - } - - /** - * Alias for @ref('Schema.createTable') - * - * @method create - */ - create (...args) { - return this.createTable(...args) - } - - /** - * Alias for @ref('Schema.createTableIfNotExists') - * - * @method createIfNotExists - */ - createIfNotExists (...args) { - return this.createTableIfNotExists(...args) - } - - /** - * Alias for @ref('Schema.dropTable') - * - * @method drop - */ - drop (...args) { - return this.dropTable(...args) - } - - /** - * Alias for @ref('Schema.dropTableIfExists') - * - * @method dropIfExists - */ - dropIfExists (...args) { - return this.dropTableIfExists(...args) - } - - /** - * Alias for @ref('Schema.renameTable') - * - * @method rename - */ - rename (...args) { - return this.renameTable(...args) + return this.schema.hasColumn(tableName, columnName) } /** @@ -372,28 +108,44 @@ class Schema { * Returns SQL array over executing the actions */ if (getSql) { - return this._deferredActions.map((action) => { - return this.schema[action.name](...action.args).toString() - }) + return this._chains.map((chain) => chain.toString(this.schema)) } + /** + * We need one transaction per class, so we start it here, instead of + * doing it inside the `for of` loop. + */ const trx = await this.db.beginTransaction() - for (let action of this._deferredActions) { - try { - if (action.name === 'schedule') { - await action.args[0](trx) - } else { - await trx.schema[action.name](...action.args) - } - } catch (error) { - trx.rollback() - throw error - } + + /** + * Execute all the chains + */ + for (let chain of this._chains) { + await chain.execute(trx) } + + /** + * Finally commit the transaction + */ trx.commit() - this._deferredActions = [] - return [] // just to be consistent with the return output + + return [] } } +/** + * Copying all the chain method to the Schema prototype. + */ +Object + .getOwnPropertyNames(SchemaChain.prototype) + .filter((method) => method !== 'constructor') + .forEach((method) => { + Schema.prototype[method] = function (...args) { + const chain = new SchemaChain() + chain[method](...args) + this._chains.push(chain) + return chain + } + }) + module.exports = Schema diff --git a/test/unit/schema.spec.js b/test/unit/schema.spec.js index ed503412..d3871615 100644 --- a/test/unit/schema.spec.js +++ b/test/unit/schema.spec.js @@ -58,7 +58,9 @@ test.group('Schema', (group) => { const userSchema = new UserSchema(ioc.use('Database')) const fn = function () {} userSchema.createTable('users', fn) - assert.deepEqual(userSchema._deferredActions, [{ name: 'createTable', args: ['users', fn] }]) + + assert.lengthOf(userSchema._chains, 1) + assert.deepEqual(userSchema._chains[0]._deferredActions, [{ name: 'createTable', args: ['users', fn] }]) }) test('add deferred action for createTableIfNotExists', (assert) => { @@ -67,16 +69,22 @@ test.group('Schema', (group) => { const userSchema = new UserSchema(ioc.use('Database')) const fn = function () {} userSchema.createIfNotExists('users', fn) - assert.deepEqual(userSchema._deferredActions, [{ name: 'createTableIfNotExists', args: ['users', fn] }]) + + assert.lengthOf(userSchema._chains, 1) + assert.deepEqual(userSchema._chains[0]._deferredActions, [{ name: 'createTableIfNotExists', args: ['users', fn] }]) }) test('add deferred action for renameTable', (assert) => { class UserSchema extends Schema { } + const userSchema = new UserSchema(ioc.use('Database')) const fn = function () {} + userSchema.renameTable('users', fn) - assert.deepEqual(userSchema._deferredActions, [{ name: 'renameTable', args: ['users', fn] }]) + + assert.lengthOf(userSchema._chains, 1) + assert.deepEqual(userSchema._chains[0]._deferredActions, [{ name: 'renameTable', args: ['users', fn] }]) }) test('add deferred action for table', (assert) => { @@ -85,7 +93,9 @@ test.group('Schema', (group) => { const userSchema = new UserSchema(ioc.use('Database')) const fn = function () {} userSchema.alter('users', fn) - assert.deepEqual(userSchema._deferredActions, [{ name: 'table', args: ['users', fn] }]) + + assert.lengthOf(userSchema._chains, 1) + assert.deepEqual(userSchema._chains[0]._deferredActions, [{ name: 'table', args: ['users', fn] }]) }) test('add deferred action for dropTableIfExists', (assert) => { @@ -93,7 +103,9 @@ test.group('Schema', (group) => { } const userSchema = new UserSchema(ioc.use('Database')) userSchema.dropIfExists('users') - assert.deepEqual(userSchema._deferredActions, [{ name: 'dropTableIfExists', args: ['users'] }]) + + assert.lengthOf(userSchema._chains, 1) + assert.deepEqual(userSchema._chains[0]._deferredActions, [{ name: 'dropTableIfExists', args: ['users'] }]) }) test('add deferred action for rename table', (assert) => { @@ -101,7 +113,9 @@ test.group('Schema', (group) => { } const userSchema = new UserSchema(ioc.use('Database')) userSchema.rename('users', 'my_users') - assert.deepEqual(userSchema._deferredActions, [{ name: 'renameTable', args: ['users', 'my_users'] }]) + + assert.lengthOf(userSchema._chains, 1) + assert.deepEqual(userSchema._chains[0]._deferredActions, [{ name: 'renameTable', args: ['users', 'my_users'] }]) }) if (process.env.DB === 'pg') { @@ -110,7 +124,9 @@ test.group('Schema', (group) => { } const userSchema = new UserSchema(ioc.use('Database')) userSchema.createExtension('postgis') - assert.deepEqual(userSchema._deferredActions, [{ name: 'createExtension', args: ['postgis'] }]) + + assert.lengthOf(userSchema._chains, 1) + assert.deepEqual(userSchema._chains[0]._deferredActions, [{ name: 'createExtension', args: ['postgis'] }]) }) test('add deferred action for createExtensionIfNotExists', (assert) => { @@ -118,7 +134,9 @@ test.group('Schema', (group) => { } const userSchema = new UserSchema(ioc.use('Database')) userSchema.createExtensionIfNotExists('postgis') - assert.deepEqual(userSchema._deferredActions, [{ name: 'createExtensionIfNotExists', args: ['postgis'] }]) + + assert.lengthOf(userSchema._chains, 1) + assert.deepEqual(userSchema._chains[0]._deferredActions, [{ name: 'createExtensionIfNotExists', args: ['postgis'] }]) }) test('add deferred action for dropExtension', (assert) => { @@ -126,7 +144,9 @@ test.group('Schema', (group) => { } const userSchema = new UserSchema(ioc.use('Database')) userSchema.dropExtension('postgis') - assert.deepEqual(userSchema._deferredActions, [{ name: 'dropExtension', args: ['postgis'] }]) + + assert.lengthOf(userSchema._chains, 1) + assert.deepEqual(userSchema._chains[0]._deferredActions, [{ name: 'dropExtension', args: ['postgis'] }]) }) test('add deferred action for dropExtensionIfExists', (assert) => { @@ -134,7 +154,34 @@ test.group('Schema', (group) => { } const userSchema = new UserSchema(ioc.use('Database')) userSchema.dropExtensionIfExists('postgis') - assert.deepEqual(userSchema._deferredActions, [{ name: 'dropExtensionIfExists', args: ['postgis'] }]) + + assert.lengthOf(userSchema._chains, 1) + assert.deepEqual(userSchema._chains[0]._deferredActions, [{ name: 'dropExtensionIfExists', args: ['postgis'] }]) + }) + + test('should be able to chain withSchema', (assert) => { + const fn = function () {} + + class UserSchema extends Schema { + up () { + this + .withSchema('public') + .table('users', fn) + } + } + + const userSchema = new UserSchema(ioc.use('Database')) + userSchema.up() + + assert.lengthOf(userSchema._chains, 1) + assert.deepEqual(userSchema._chains[0]._deferredActions, [ + { + name: 'withSchema', args: ['public'] + }, + { + name: 'table', args: ['users', fn] + } + ]) }) } @@ -157,13 +204,17 @@ test.group('Schema', (group) => { }) } } + const userSchema = new UserSchema(ioc.use('Database')) userSchema.up() + await userSchema.executeActions() const hasUsers = await userSchema.hasTable('schema_users') const hasProfile = await userSchema.hasTable('schema_profile') + assert.isTrue(hasUsers) assert.isTrue(hasProfile) + await ioc.use('Database').schema.dropTable('schema_users') await ioc.use('Database').schema.dropTable('schema_profile') }) @@ -183,6 +234,7 @@ test.group('Schema', (group) => { const userSchema = new UserSchema(ioc.use('Database')) userSchema.up() + try { await userSchema.executeActions() assert.isFalse(true) @@ -210,8 +262,10 @@ test.group('Schema', (group) => { const userSchema = new UserSchema(ioc.use('Database')) userSchema.up() + const queries = await userSchema.executeActions(true) assert.lengthOf(queries, 2) + const hasSchemaUsers = await ioc.use('Database').schema.hasTable('schema_users') assert.isFalse(hasSchemaUsers) }) @@ -219,14 +273,20 @@ test.group('Schema', (group) => { test('calling this.raw should not cause infinite loop lucid#212', async (assert) => { class UserSchema extends Schema { up () { - this.raw('CREATE table schema_users (id int);') + this + .raw('CREATE table schema_users (id int);') + .table('schema_users', (table) => { + table.string('username') + }) } } const userSchema = new UserSchema(ioc.use('Database')) userSchema.up() + await userSchema.executeActions() - const hasSchemaUsers = await ioc.use('Database').schema.hasTable('schema_users') + const hasSchemaUsers = await ioc.use('Database').schema.hasColumn('schema_users', 'id') + assert.isTrue(hasSchemaUsers) }) @@ -236,7 +296,9 @@ test.group('Schema', (group) => { const userSchema = new UserSchema(ioc.use('Database')) userSchema.raw('CREATE table schema_users (id int);') - assert.deepEqual(userSchema._deferredActions, [{ name: 'raw', args: ['CREATE table schema_users (id int);'] }]) + + assert.lengthOf(userSchema._chains, 1) + assert.deepEqual(userSchema._chains[0]._deferredActions, [{ name: 'raw', args: ['CREATE table schema_users (id int);'] }]) }) test('schedule a function to be called in sequence with schema statements', async (assert) => { @@ -255,14 +317,14 @@ test.group('Schema', (group) => { } const userSchema = new UserSchema(ioc.use('Database')) + userSchema.up() await userSchema.executeActions() + assert.deepEqual(users, []) }) test('throw exception when function is not passed to schedule method', async (assert) => { - let users = null - class UserSchema extends Schema { up () { this.createTable('schema_users', (table) => {