objection
Version:
An SQL-friendly ORM for Node.js
294 lines (252 loc) • 9.33 kB
JavaScript
'use strict';
const hookNameToHasMethodName = {
onAdd: 'hasOnAdd',
onBefore1: 'hasOnBefore1',
onBefore2: 'hasOnBefore2',
onBefore3: 'hasOnBefore3',
onBuild: 'hasOnBuild',
onBuildKnex: 'hasOnBuildKnex',
onRawResult: 'hasOnRawResult',
queryExecutor: 'hasQueryExecutor',
onAfter1: 'hasOnAfter1',
onAfter2: 'hasOnAfter2',
onAfter3: 'hasOnAfter3',
onError: 'hasOnError',
};
// An abstract base class for all query builder operations. QueryBuilderOperations almost always
// correspond to a single query builder method call. For example SelectOperation could be added when
// a `select` method is called.
//
// QueryBuilderOperation is just a bunch of query execution lifecycle hooks that subclasses
// can (but don't have to) implement.
//
// Basically a query builder is nothing but an array of QueryBuilderOperations. When the query is
// executed the hooks are called in the order explained below. The hooks are called so that a
// certain hook is called for _all_ operations before the next hook is called. For example if
// a builder has 5 operations, onBefore1 hook is called for each of them (and their results are awaited)
// before onBefore2 hook is called for any of the operations.
class QueryBuilderOperation {
constructor(name = null, opt = {}) {
this.name = name;
this.opt = opt;
// From which hook was this operation added as a child
// operation.
this.adderHookName = null;
// The parent operation that added this operation.
this.parentOperation = null;
// Operations this operation added in any of its hooks.
this.childOperations = [];
}
is(OperationClass) {
return this instanceof OperationClass;
}
hasHook(hookName) {
return this[hookNameToHasMethodName[hookName]]();
}
// This is called immediately when a query builder method is called.
//
// This method must be synchronous.
// This method should never call any methods that add operations to the builder.
onAdd(builder, args) {
return true;
}
hasOnAdd() {
return true;
}
// This is called as the first thing when the query is executed but before
// the actual database operation (knex query) is executed.
//
// This method can be asynchronous.
// You may call methods that add operations to to the builder.
onBefore1(builder, result) {}
hasOnBefore1() {
return this.onBefore1 !== QueryBuilderOperation.prototype.onBefore1;
}
// This is called as the second thing when the query is executed but before
// the actual database operation (knex query) is executed.
//
// This method can be asynchronous.
// You may call methods that add operations to to the builder.
onBefore2(builder, result) {}
hasOnBefore2() {
return this.onBefore2 !== QueryBuilderOperation.prototype.onBefore2;
}
// This is called as the third thing when the query is executed but before
// the actual database operation (knex query) is executed.
//
// This method can be asynchronous.
// You may call methods that add operations to to the builder.
onBefore3(builder, result) {}
hasOnBefore3() {
return this.onBefore3 !== QueryBuilderOperation.prototype.onBefore3;
}
// This is called as the last thing when the query is executed but before
// the actual database operation (knex query) is executed. If your operation
// needs to call other query building operations (methods that add QueryBuilderOperations)
// this is the best and last place to do it.
//
// This method must be synchronous.
// You may call methods that add operations to to the builder.
onBuild(builder) {}
hasOnBuild() {
return this.onBuild !== QueryBuilderOperation.prototype.onBuild;
}
// This is called when the knex query is built. Here you should only call knex
// methods. You may call getters and other immutable methods of the `builder`
// but you should never call methods that add QueryBuilderOperations.
//
// This method must be synchronous.
// This method should never call any methods that add operations to the builder.
// This method should always return the knex query builder.
onBuildKnex(knexBuilder, builder) {
return knexBuilder;
}
hasOnBuildKnex() {
return this.onBuildKnex !== QueryBuilderOperation.prototype.onBuildKnex;
}
// The raw knex result is passed to this method right after the database query
// has finished. This method may modify it and return the modified rows. The
// rows are automatically converted to models (if possible) after this hook
// is called.
//
// This method can be asynchronous.
onRawResult(builder, rows) {
return rows;
}
hasOnRawResult() {
return this.onRawResult !== QueryBuilderOperation.prototype.onRawResult;
}
// This is called as the first thing after the query has been executed and
// rows have been converted to model instances.
//
// This method can be asynchronous.
onAfter1(builder, result) {
return result;
}
hasOnAfter1() {
return this.onAfter1 !== QueryBuilderOperation.prototype.onAfter1;
}
// This is called as the second thing after the query has been executed and
// rows have been converted to model instances.
//
// This method can be asynchronous.
onAfter2(builder, result) {
return result;
}
hasOnAfter2() {
return this.onAfter2 !== QueryBuilderOperation.prototype.onAfter2;
}
// This is called as the third thing after the query has been executed and
// rows have been converted to model instances.
//
// This method can be asynchronous.
onAfter3(builder, result) {
return result;
}
hasOnAfter3() {
return this.onAfter3 !== QueryBuilderOperation.prototype.onAfter3;
}
// This method can be implemented to return another operation that will replace
// this one. This method is called after all `onBeforeX` and `onBuildX` hooks
// but before the database query is executed.
//
// This method must return a QueryBuilder instance.
queryExecutor(builder) {}
hasQueryExecutor() {
return this.queryExecutor !== QueryBuilderOperation.prototype.queryExecutor;
}
// This is called if an error occurs in the query execution.
//
// This method must return a QueryBuilder instance.
onError(builder, error) {}
hasOnError() {
return this.onError !== QueryBuilderOperation.prototype.onError;
}
// Returns the "find" equivalent of this operation.
//
// For example an operation that finds an item and updates it
// should return an operation that simply finds the item but
// doesn't update anything. An insert operation should return
// null since there is no find equivalent for it etc.
toFindOperation(builder) {
return this;
}
hasToFindOperation() {
return this.toFindOperation !== QueryBuilderOperation.prototype.toFindOperation;
}
// Given a set of operations, returns true if any of this operation's
// ancestor operations are included in the set.
isAncestorInSet(operationSet) {
let ancestor = this.parentOperation;
while (ancestor) {
if (operationSet.has(ancestor)) {
return true;
}
ancestor = ancestor.parentOperation;
}
return false;
}
// Takes a deep clone of this operation.
clone() {
const clone = new this.constructor(this.name, this.opt);
clone.adderHookName = this.adderHookName;
clone.parentOperation = this.parentOperation;
clone.childOperations = this.childOperations.map((childOp) => {
const childOpClone = childOp.clone();
childOpClone.parentOperation = clone;
return childOpClone;
});
return clone;
}
// Add an operation as a child operation. `hookName` must be the
// name of the parent operation's hook that called this method.
addChildOperation(hookName, operation) {
operation.adderHookName = hookName;
operation.parentOperation = this;
this.childOperations.push(operation);
}
// Removes a single child operation.
removeChildOperation(operation) {
const index = this.childOperations.indexOf(operation);
if (index !== -1) {
operation.parentOperation = null;
this.childOperations.splice(index, 1);
}
}
// Replaces a single child operation.
replaceChildOperation(operation, newOperation) {
const index = this.childOperations.indexOf(operation);
if (index !== -1) {
newOperation.adderHookName = operation.adderHookName;
newOperation.parentOperation = this;
operation.parentOperation = null;
this.childOperations[index] = newOperation;
}
}
// Removes all child operations that were added from the `hookName` hook.
removeChildOperationsByHookName(hookName) {
this.childOperations = this.childOperations.filter((op) => op.adderHookName !== hookName);
}
// Iterates through all descendant operations recursively.
forEachDescendantOperation(callback) {
for (const operation of this.childOperations) {
if (callback(operation) === false) {
return false;
}
if (operation.forEachDescendantOperation(callback) === false) {
return false;
}
}
return true;
}
}
Object.defineProperties(QueryBuilderOperation, {
isObjectionQueryBuilderOperationClass: {
enumerable: false,
writable: false,
value: true,
},
});
module.exports = {
QueryBuilderOperation,
};