objection
Version:
An SQL-friendly ORM for Node.js
536 lines (428 loc) • 12.6 kB
JavaScript
'use strict';
const { isString, isFunction, isRegExp, mergeMaps, last } = require('../utils/objectUtils');
const { QueryBuilderContextBase } = require('./QueryBuilderContextBase');
const { QueryBuilderUserContext } = require('./QueryBuilderUserContext');
const { deprecate } = require('../utils/deprecate');
const AllSelector = () => true;
const SelectSelector =
/^(select|columns|column|distinct|count|countDistinct|min|max|sum|sumDistinct|avg|avgDistinct)$/;
const WhereSelector = /^(where|orWhere|andWhere|find\w+)/;
const OnSelector = /^(on|orOn|andOn)/;
const OrderBySelector = /orderBy/;
const JoinSelector = /(join|joinRaw|joinRelated)$/i;
const FromSelector = /^(from|into|table)$/;
class QueryBuilderOperationSupport {
constructor(...args) {
this.constructor.init(this, ...args);
}
static init(self, modelClass) {
self._modelClass = modelClass;
self._operations = [];
self._context = new this.QueryBuilderContext(self);
self._parentQuery = null;
self._isPartialQuery = false;
self._activeOperations = [];
}
static forClass(modelClass) {
return new this(modelClass);
}
static get AllSelector() {
return AllSelector;
}
static get QueryBuilderContext() {
return QueryBuilderContextBase;
}
static get QueryBuilderUserContext() {
return QueryBuilderUserContext;
}
static get SelectSelector() {
return SelectSelector;
}
static get WhereSelector() {
return WhereSelector;
}
static get OnSelector() {
return OnSelector;
}
static get JoinSelector() {
return JoinSelector;
}
static get FromSelector() {
return FromSelector;
}
static get OrderBySelector() {
return OrderBySelector;
}
modelClass() {
return this._modelClass;
}
context(obj) {
const ctx = this._context;
if (arguments.length === 0) {
return ctx.userContext;
} else {
ctx.userContext = ctx.userContext.newMerge(this, obj);
return this;
}
}
clearContext() {
const ctx = this._context;
ctx.userContext = new this.constructor.QueryBuilderUserContext(this);
return this;
}
internalContext(ctx) {
if (arguments.length === 0) {
return this._context;
} else {
this._context = ctx;
return this;
}
}
internalOptions(opt) {
if (arguments.length === 0) {
return this._context.options;
} else {
const oldOpt = this._context.options;
this._context.options = Object.assign(oldOpt, opt);
return this;
}
}
isPartial(isPartial) {
if (arguments.length === 0) {
return this._isPartialQuery;
} else {
this._isPartialQuery = isPartial;
return this;
}
}
isInternal() {
return this.internalOptions().isInternalQuery;
}
tableNameFor(tableName, newTableName) {
const ctx = this.internalContext();
const tableMap = ctx.tableMap;
if (isString(newTableName)) {
ctx.tableMap = tableMap || new Map();
ctx.tableMap.set(tableName, newTableName);
return this;
} else {
return (tableMap && tableMap.get(tableName)) || tableName;
}
}
aliasFor(tableName, alias) {
const ctx = this.internalContext();
const aliasMap = ctx.aliasMap;
if (isString(alias)) {
ctx.aliasMap = aliasMap || new Map();
ctx.aliasMap.set(tableName, alias);
return this;
} else {
return (aliasMap && aliasMap.get(tableName)) || null;
}
}
tableRefFor(tableName) {
return this.aliasFor(tableName) || this.tableNameFor(tableName);
}
childQueryOf(query, { fork, isInternalQuery } = {}) {
if (query) {
let currentCtx = this.context();
let ctx = query.internalContext();
if (fork) {
ctx = ctx.clone();
}
if (isInternalQuery) {
ctx.options.isInternalQuery = true;
}
this._parentQuery = query;
this.internalContext(ctx);
this.context(currentCtx);
// Use the parent's knex if there was no knex in `ctx`.
if (this.unsafeKnex() === null) {
this.knex(query.unsafeKnex());
}
}
return this;
}
subqueryOf(query) {
if (query) {
if (this._isPartialQuery) {
// Merge alias and table name maps for "partial" subqueries.
const ctx = this.internalContext();
ctx.aliasMap = mergeMaps(query.internalContext().aliasMap, ctx.aliasMap);
ctx.tableMap = mergeMaps(query.internalContext().tableMap, ctx.tableMap);
}
this._parentQuery = query;
if (this.unsafeKnex() === null) {
this.knex(query.unsafeKnex());
}
}
return this;
}
parentQuery() {
return this._parentQuery;
}
knex(...args) {
if (args.length === 0) {
const knex = this.unsafeKnex();
if (!knex) {
throw new Error(
`no database connection available for a query. You need to bind the model class or the query to a knex instance.`,
);
}
return knex;
} else {
this._context.knex = args[0];
return this;
}
}
unsafeKnex() {
return this._context.knex || this._modelClass.knex() || null;
}
clear(operationSelector) {
const operationsToRemove = new Set();
this.forEachOperation(operationSelector, (op) => {
// If an ancestor operation has already been removed,
// there's no need to remove the children anymore.
if (!op.isAncestorInSet(operationsToRemove)) {
operationsToRemove.add(op);
}
});
for (const op of operationsToRemove) {
this.removeOperation(op);
}
return this;
}
toFindQuery() {
const findQuery = this.clone();
const operationsToReplace = [];
const operationsToRemove = [];
findQuery.forEachOperation(
(op) => op.hasToFindOperation(),
(op) => {
const findOp = op.toFindOperation(findQuery);
if (!findOp) {
operationsToRemove.push(op);
} else {
operationsToReplace.push({ op, findOp });
}
},
);
for (const op of operationsToRemove) {
findQuery.removeOperation(op);
}
for (const { op, findOp } of operationsToReplace) {
findQuery.replaceOperation(op, findOp);
}
return findQuery;
}
clearSelect() {
return this.clear(SelectSelector);
}
clearWhere() {
return this.clear(WhereSelector);
}
clearOrder() {
return this.clear(OrderBySelector);
}
copyFrom(queryBuilder, operationSelector) {
const operationsToAdd = new Set();
queryBuilder.forEachOperation(operationSelector, (op) => {
// If an ancestor operation has already been added,
// there is no need to add
if (!op.isAncestorInSet(operationsToAdd)) {
operationsToAdd.add(op);
}
});
for (const op of operationsToAdd) {
const opClone = op.clone();
// We may be moving nested operations to the root. Clear
// any links to the parent operations.
opClone.parentOperation = null;
opClone.adderHookName = null;
// We don't use `addOperation` here because we don't what to
// call `onAdd` or add these operations as child operations.
this._operations.push(opClone);
}
return this;
}
has(operationSelector) {
return !!this.findOperation(operationSelector);
}
forEachOperation(operationSelector, callback, match = true) {
const selector = buildFunctionForOperationSelector(operationSelector);
for (const op of this._operations) {
if (selector(op) === match && callback(op) === false) {
break;
}
const childRes = op.forEachDescendantOperation((op) => {
if (selector(op) === match && callback(op) === false) {
return false;
}
});
if (childRes === false) {
break;
}
}
return this;
}
findOperation(operationSelector) {
let op = null;
this.forEachOperation(operationSelector, (it) => {
op = it;
return false;
});
return op;
}
findLastOperation(operationSelector) {
let op = null;
this.forEachOperation(operationSelector, (it) => {
op = it;
});
return op;
}
everyOperation(operationSelector) {
let every = true;
this.forEachOperation(
operationSelector,
() => {
every = false;
return false;
},
false,
);
return every;
}
callOperationMethod(operation, hookName, args) {
try {
operation.removeChildOperationsByHookName(hookName);
this._activeOperations.push({
operation,
hookName,
});
return operation[hookName](...args);
} finally {
this._activeOperations.pop();
}
}
async callAsyncOperationMethod(operation, hookName, args) {
operation.removeChildOperationsByHookName(hookName);
this._activeOperations.push({
operation,
hookName,
});
try {
return await operation[hookName](...args);
} finally {
this._activeOperations.pop();
}
}
addOperation(operation, args) {
const ret = this.addOperationUsingMethod('push', operation, args);
return ret;
}
addOperationToFront(operation, args) {
return this.addOperationUsingMethod('unshift', operation, args);
}
addOperationUsingMethod(arrayMethod, operation, args) {
const shouldAdd = this.callOperationMethod(operation, 'onAdd', [this, args]);
if (shouldAdd) {
if (this._activeOperations.length) {
const { operation: parentOperation, hookName } = last(this._activeOperations);
parentOperation.addChildOperation(hookName, operation);
} else {
this._operations[arrayMethod](operation);
}
}
return this;
}
removeOperation(operation) {
if (operation.parentOperation) {
operation.parentOperation.removeChildOperation(operation);
} else {
const index = this._operations.indexOf(operation);
if (index !== -1) {
this._operations.splice(index, 1);
}
}
return this;
}
replaceOperation(operation, newOperation) {
if (operation.parentOperation) {
operation.parentOperation.replaceChildOperation(operation, newOperation);
} else {
const index = this._operations.indexOf(operation);
if (index !== -1) {
this._operations[index] = newOperation;
}
}
return this;
}
clone() {
return this.baseCloneInto(new this.constructor(this.unsafeKnex()));
}
baseCloneInto(builder) {
builder._modelClass = this._modelClass;
builder._operations = this._operations.map((it) => it.clone());
builder._context = this._context.clone();
builder._parentQuery = this._parentQuery;
builder._isPartialQuery = this._isPartialQuery;
// Don't copy the active operation stack. We never continue (nor can we)
// a query from the exact mid-hook-call state.
builder._activeOperations = [];
return builder;
}
toKnexQuery(knexBuilder = this.knex().queryBuilder()) {
this.executeOnBuild();
return this.executeOnBuildKnex(knexBuilder);
}
executeOnBuild() {
this.forEachOperation(true, (op) => {
if (op.hasOnBuild()) {
this.callOperationMethod(op, 'onBuild', [this]);
}
});
}
executeOnBuildKnex(knexBuilder) {
this.forEachOperation(true, (op) => {
if (op.hasOnBuildKnex()) {
const newKnexBuilder = this.callOperationMethod(op, 'onBuildKnex', [knexBuilder, this]);
// Default to the input knex builder for backwards compatibility
// with QueryBuilder.onBuildKnex hooks.
knexBuilder = newKnexBuilder || knexBuilder;
}
});
return knexBuilder;
}
toString() {
return this.toKnexQuery().toString();
}
toSql() {
return this.toString();
}
skipUndefined() {
deprecate('skipUndefined() is deprecated and will be removed in objection 4.0');
this.internalOptions().skipUndefined = true;
return this;
}
}
function buildFunctionForOperationSelector(operationSelector) {
if (operationSelector === true) {
return AllSelector;
} else if (isRegExp(operationSelector)) {
return (op) => operationSelector.test(op.name);
} else if (isString(operationSelector)) {
return (op) => op.name === operationSelector;
} else if (
isFunction(operationSelector) &&
operationSelector.isObjectionQueryBuilderOperationClass
) {
return (op) => op.is(operationSelector);
} else if (isFunction(operationSelector)) {
return operationSelector;
} else {
return () => false;
}
}
module.exports = {
QueryBuilderOperationSupport,
};