245 lines
9.3 KiB
JavaScript
245 lines
9.3 KiB
JavaScript
// Sync
|
|
// ---------------
|
|
'use strict';
|
|
|
|
const _ = require('lodash');
|
|
const Promise = require('bluebird');
|
|
const validLocks = ['forShare', 'forUpdate'];
|
|
|
|
function supportsReturning(client = {}) {
|
|
if (!client.config || !client.config.client) return false;
|
|
return ['postgresql', 'postgres', 'pg', 'oracle', 'mssql'].includes(client.config.client);
|
|
}
|
|
|
|
// Sync is the dispatcher for any database queries,
|
|
// taking the "syncing" `model` or `collection` being queried, along with
|
|
// a hash of options that are used in the various query methods.
|
|
// If the `transacting` option is set, the query is assumed to be
|
|
// part of a transaction, and this information is passed along to `Knex`.
|
|
const Sync = function(syncing, options) {
|
|
options = options || {};
|
|
this.query = syncing.query();
|
|
this.syncing = syncing.resetQuery();
|
|
this.options = options;
|
|
if (options.debug) this.query.debug();
|
|
if (options.transacting) {
|
|
this.query.transacting(options.transacting);
|
|
if (validLocks.indexOf(options.lock) > -1) this.query[options.lock]();
|
|
}
|
|
if (options.withSchema) this.query.withSchema(options.withSchema);
|
|
};
|
|
|
|
_.extend(Sync.prototype, {
|
|
// Prefix all keys of the passed in object with the
|
|
// current table name
|
|
prefixFields: function(fields) {
|
|
const tableName = this.syncing.tableName;
|
|
const prefixed = {};
|
|
for (const key in fields) {
|
|
prefixed[tableName + '.' + key] = fields[key];
|
|
}
|
|
return prefixed;
|
|
},
|
|
|
|
// Select the first item from the database - only used by models.
|
|
first: Promise.method(function(attributes) {
|
|
const model = this.syncing;
|
|
const query = this.query;
|
|
|
|
// We'll never use an JSON object for a search, because even
|
|
// PostgreSQL, which has JSON type columns, does not support the `=`
|
|
// operator.
|
|
//
|
|
// NOTE: `_.omit` returns an empty object, even if attributes are null.
|
|
const whereAttributes = _.omitBy(attributes, (attribute, name) => {
|
|
return _.isPlainObject(attribute) || name === model.idAttribute;
|
|
});
|
|
const formattedAttributes = model.format(whereAttributes);
|
|
|
|
if (model.idAttribute in attributes) {
|
|
formattedAttributes[model.idAttribute] = attributes[model.idAttribute];
|
|
}
|
|
|
|
if (!_.isEmpty(formattedAttributes)) query.where(this.prefixFields(formattedAttributes));
|
|
query.limit(1);
|
|
|
|
return this.select();
|
|
}),
|
|
|
|
// Runs a `count` query on the database, adding any necessary relational
|
|
// constraints. Returns a promise that resolves to an integer count.
|
|
count: Promise.method(function(column) {
|
|
const knex = this.query,
|
|
options = this.options,
|
|
relatedData = this.syncing.relatedData,
|
|
fks = {};
|
|
|
|
return Promise.bind(this)
|
|
.then(function() {
|
|
// Inject all appropriate select costraints dealing with the relation
|
|
// into the `knex` query builder for the current instance.
|
|
if (relatedData)
|
|
return Promise.try(function() {
|
|
if (relatedData.isThrough()) {
|
|
fks[relatedData.key('foreignKey')] = relatedData.parentFk;
|
|
const through = new relatedData.throughTarget(fks);
|
|
relatedData.pivotColumns = through.parse(relatedData.pivotColumns);
|
|
} else if (relatedData.type === 'hasMany') {
|
|
const fk = relatedData.key('foreignKey');
|
|
knex.where(fk, relatedData.parentFk);
|
|
}
|
|
});
|
|
})
|
|
.then(function() {
|
|
options.query = knex;
|
|
|
|
/**
|
|
* Counting event.
|
|
*
|
|
* Fired before a `count` query. A promise may be
|
|
* returned from the event handler for async behaviour.
|
|
*
|
|
* @event Model#counting
|
|
* @tutorial events
|
|
* @param {Model} model The model firing the event.
|
|
* @param {Object} options Options object passed to {@link Model#count count}.
|
|
* @returns {Promise}
|
|
*/
|
|
return this.syncing.triggerThen('counting', this.syncing, options);
|
|
})
|
|
.then(function() {
|
|
return knex.count((column || '*') + ' as count');
|
|
})
|
|
.then(function(rows) {
|
|
return rows[0].count;
|
|
});
|
|
}),
|
|
|
|
// Runs a `select` query on the database, adding any necessary relational
|
|
// constraints, resetting the query when complete. If there are results and
|
|
// eager loaded relations, those are fetched and returned on the model before
|
|
// the promise is resolved. Any `success` handler passed in the
|
|
// options will be called - used by both models & collections.
|
|
select: Promise.method(function() {
|
|
const knex = this.query;
|
|
const options = this.options;
|
|
const relatedData = this.syncing.relatedData;
|
|
const fks = {};
|
|
let columns = null;
|
|
|
|
// Check if any `select` style statements have been called with column
|
|
// specifications. This could include `distinct()` with no arguments, which
|
|
// does not affect inform the columns returned.
|
|
const queryContainsColumns = _(knex._statements)
|
|
.filter({grouping: 'columns'})
|
|
.some('value.length');
|
|
|
|
return Promise.bind(this)
|
|
.then(function() {
|
|
// Set the query builder on the options, in-case we need to
|
|
// access in the `fetching` event handlers.
|
|
options.query = knex;
|
|
|
|
// Inject all appropriate select costraints dealing with the relation
|
|
// into the `knex` query builder for the current instance.
|
|
if (relatedData)
|
|
return Promise.try(function() {
|
|
if (relatedData.isThrough()) {
|
|
fks[relatedData.key('foreignKey')] = relatedData.parentFk;
|
|
const through = new relatedData.throughTarget(fks);
|
|
|
|
return through.triggerThen('fetching', through, relatedData.pivotColumns, options).then(function() {
|
|
relatedData.pivotColumns = through.parse(relatedData.pivotColumns);
|
|
});
|
|
}
|
|
});
|
|
})
|
|
.tap(() => {
|
|
// If this is a relation, apply the appropriate constraints.
|
|
if (relatedData) {
|
|
relatedData.selectConstraints(knex, options);
|
|
} else {
|
|
// Call the function, if one exists, to constrain the eager loaded query.
|
|
if (options._beforeFn) options._beforeFn.call(knex, knex);
|
|
|
|
if (options.columns) {
|
|
// Normalize single column name into array.
|
|
columns = Array.isArray(options.columns) ? options.columns : [options.columns];
|
|
} else if (!queryContainsColumns) {
|
|
// If columns have already been selected via the `query` method
|
|
// we will use them. Otherwise, select all columns in this table.
|
|
columns = [_.result(this.syncing, 'tableName') + '.*'];
|
|
}
|
|
}
|
|
|
|
// Set the query builder on the options, for access in the `fetching`
|
|
// event handlers.
|
|
options.query = knex;
|
|
|
|
/**
|
|
* Fired before a `fetch` operation. A promise may be returned from the event handler for
|
|
* async behaviour.
|
|
*
|
|
* @example
|
|
* const MyModel = bookshelf.model('MyModel', {
|
|
* initialize() {
|
|
* this.on('fetching', function(model, columns, options) {
|
|
* options.query.where('status', 'active')
|
|
* })
|
|
* }
|
|
* })
|
|
*
|
|
* @event Model#fetching
|
|
* @tutorial events
|
|
* @param {Model} model The model which is about to be fetched.
|
|
* @param {string[]} columns The columns to be retrieved by the query.
|
|
* @param {Object} options Options object passed to {@link Model#fetch fetch}.
|
|
* @param {QueryBuilder} options.query
|
|
* Query builder to be used for fetching. This can be used to modify or add to the query
|
|
* before it is executed. See example above.
|
|
* @return {Promise}
|
|
*/
|
|
return this.syncing.triggerThen('fetching', this.syncing, columns, options);
|
|
})
|
|
.then(() => knex.select(columns));
|
|
}),
|
|
|
|
// Issues an `insert` command on the query - only used by models.
|
|
insert: Promise.method(function() {
|
|
const syncing = this.syncing;
|
|
return this.query.insert(
|
|
syncing.format(_.extend(Object.create(null), syncing.attributes)),
|
|
supportsReturning(this.query.client) && this.options.autoRefresh !== false ? '*' : null
|
|
);
|
|
}),
|
|
|
|
// Issues an `update` command on the query - only used by models.
|
|
update: Promise.method(function(attrs) {
|
|
const syncing = this.syncing,
|
|
query = this.query;
|
|
if (syncing.id != null) query.where(syncing.format({[syncing.idAttribute]: syncing.id}));
|
|
if (_.filter(query._statements, {grouping: 'where'}).length === 0) {
|
|
throw new Error('A model cannot be updated without a "where" clause or an idAttribute.');
|
|
}
|
|
var updating = syncing.format(_.extend(Object.create(null), attrs));
|
|
if (syncing.id === updating[syncing.idAttribute]) {
|
|
delete updating[syncing.idAttribute];
|
|
}
|
|
if (supportsReturning(query.client) && this.options.autoRefresh !== false) query.returning('*');
|
|
return query.update(updating);
|
|
}),
|
|
|
|
// Issues a `delete` command on the query.
|
|
del: Promise.method(function() {
|
|
const query = this.query,
|
|
syncing = this.syncing;
|
|
if (syncing.id != null) query.where(syncing.format({[syncing.idAttribute]: syncing.id}));
|
|
if (_.filter(query._statements, {grouping: 'where'}).length === 0) {
|
|
throw new Error('A model cannot be destroyed without a "where" clause or an idAttribute.');
|
|
}
|
|
return this.query.del();
|
|
})
|
|
});
|
|
|
|
module.exports = Sync;
|