lib/PreparedQuery.js
const constructQuery = require('./constructQuery');
const doJoins = require('./doJoins');
/**
* Class that presents a simple interface for constructing queries for a
* relational database using a model map object describing the connections
* between models.
*
* All methods except `exec` are chainable.
*
*/
class PreparedQuery {
/**
* Generally, this shouldn't be called by external code. This object is generated by the main
* interface when the `generateQueryFor` method is used.
*
* @param {String} modelId - The `primary` model to query
* @param {JoinJsMap} models - The complete schema of the database (after internal processing)
* @param {KnexInstance} knex - A knex instance
*/
constructor(modelId, models, knex) {
// This is safe as the models object is serialisable
this.models = JSON.parse(JSON.stringify((models)));
this.knex = knex;
this.modelId = modelId;
this.populated = [];
this.whereClauses = [];
this.originalModel = this.models.find(model => model.mapId === modelId);
if (!this.originalModel) {
throw new Error(`Invalid model ID ${modelId}`);
}
return this;
}
/**
* Populate the joins for a collection or association of the primary model of the query.
*
* @param {String} name - The name property of the association or collection
*/
populate(name) {
this.populated.push(name);
// Chainable
return this;
}
/**
* Define the number of elements to skip (useful for pagination).
*
* @param {Number} value - The number of elements to skip
*/
skip(value) {
this._skip = value;
// Chainable
return this;
}
/**
* Define the maximum number of elements to return (useful for pagination).
*
* @param {Number} value - The maximum number of elements to return
*/
limit(value) {
this._limit = value;
// Chainable
return this;
}
/**
* Define a restriction on the result set.
*
* @param {Object|Array} clauses - An object (or list of objects) specifying the
* predicate that all results must satisfy. Multiple calls to this function
* on a single prepared query instance will be appended to the list of clauses
* that are combined using boolean AND logic (ie. results must satisfy all
* predicates in the list)
*
* Each predicate consists of a `key` (the column name) and a `value` or `values`
* list. If a list of values is specifed it is interpreted as a WhereIn operation
* such that results can match ANY of the values supplied.
*/
where(clauses) {
if (!clauses || clauses.length === 0) {
return this;
}
// Allow a single where clause to be passed in
if (!Array.isArray(clauses)) {
clauses = [clauses];
}
// Validate the input structure
clauses.forEach(clause => {
if (!clause.key) {
throw new Error(`Where clause missing key: ${clause}`);
}
if ((clause.value === undefined) && !Array.isArray(clause.values)) {
throw new Error(`Where clause missing a value or values (array): ${clause}`);
}
if (clause.value !== undefined && Array.isArray(clause.values)) {
throw new Error(
'Each predicate in a where clause must specify a value OR a list of values');
}
});
this.whereClauses = this.whereClauses.concat(clauses);
// Chainable
return this;
}
/**
* Define the ordering of the results
*
* @param {String} attr - The column name to sort based on
* @param {String} dir - The direction of the sorting. Must be `asc` or `desc`.
*/
sort(attr, dir) {
this.sortAttr = attr;
this.sortDir = dir;
// Chainable
return this;
}
/**
* Run the query and construct a nested JS object representation of it.
*
* @returns {Promise} - A promise that resolves to the result of the query
*/
exec() {
// Note that we don't want to modify the original model
const modifiedModel = {
mapId: this.originalModel.mapId,
viewId: this.originalModel.viewId,
idProperty: this.originalModel.idProperty,
properties: this.originalModel.properties,
// Add a flag to determine which connections will need populating
associations: this.originalModel.associations.map(assoc => {
assoc.populate = !!this.populated.find(name => name === assoc.name);
return assoc;
}),
collections: this.originalModel.collections.map(coll => {
coll.populate = !!this.populated.find(name => name === coll.name);
return coll;
})
};
// Construct the query, run it and convert it to a nested object
return constructQuery(
this.models,
modifiedModel,
this.knex,
this.whereClauses,
this.sortAttr,
this.sortDir,
this._limit,
this._skip
).then(data => doJoins(
data,
this.models,
this.originalModel,
modifiedModel
));
}
}
module.exports = PreparedQuery;