UNPKG

breeze-sequelize

Version:
409 lines 17.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const _ = require("lodash"); const sequelize_1 = require("sequelize"); /** Visit the nodes in a Breeze query, converting it to a Sequelize query */ const toSQVisitor = (function () { const visitor = { passthruPredicate: function () { return this.value; }, unaryPredicate: function (context) { const predSq = this.pred.visit(context); if (this.op.key !== "not") { throw new Error("Not yet implemented: Unary operation: " + this.op.key + " pred: " + JSON.stringify(this.pred)); } if (!isEmpty(predSq.include)) { throw new Error("Unable to negate an expression that requires a Sequelize 'include'"); } predSq.where = applyNot(predSq.where); return predSq; }, binaryPredicate: function (context) { const result = {}; const op = this.op.key; // TODO: right now only handling case where e1 : PropExpr and e2 : LitExpr | PropExpr // not yet handled: e1: FnExpr | e2: FnExpr let where, p1Value, p2Value; if (this.expr1.visitorMethodName === "propExpr") { p1Value = processPropExpr(this.expr1, context, result); } else if (this.expr1.visitorMethodName === "fnExpr") { p1Value = processFnExpr(this.expr1, context, result); } else { // also note that literal exprs are not allowed for expr1 ( i.e. only allowed on expr2) throw new Error("Not yet implemented: binary predicate with a expr1 type of: " + this.expr1.visitorMethodName + " - " + this.expr1.toString()); } let crit; const like = _boolOpMap.like.sequelizeOp; if (this.expr2.visitorMethodName === "litExpr") { p2Value = this.expr2.value; if (op === "eq") { crit = p2Value; // where[p1Value] = p2Value; } else if (op === "startswith") { crit = { [like]: p2Value + "%" }; } else if (op === "endswith") { crit = { [like]: "%" + p2Value }; } else if (op === "contains") { crit = { [like]: "%" + p2Value + "%" }; } else { crit = {}; const mop = _boolOpMap[op].sequelizeOp; crit[mop] = p2Value; } } else if (this.expr2.visitorMethodName === "propExpr") { let p2Value = this.expr2.propertyPath; const props = context.entityType.getPropertiesOnPath(p2Value, context.toNameOnServer, true); p2Value = props.map(function (p) { return p.nameOnServer; }).join("."); const colVal = sequelize_1.Sequelize.col(p2Value); if (op === "eq") { crit = colVal; } else if (op === "startswith") { crit = { [like]: sequelize_1.Sequelize.literal("concat(" + p2Value + ",'%')") }; } else if (op === "endswith") { crit = { [like]: sequelize_1.Sequelize.literal("concat('%'," + p2Value + ")") }; } else if (op === "contains") { crit = { [like]: sequelize_1.Sequelize.literal("concat('%'," + p2Value + ",'%')") }; } else { crit = {}; const mop = _boolOpMap[op].sequelizeOp; crit[mop] = colVal; } } else { throw new Error("Not yet implemented: binary predicate with a expr2 type of: " + this.expr2.visitorMethodName + " - " + this.expr2.toString()); } where = makeWhere(p1Value, crit); // the 'where' clause may be on a nested include if (result.lastInclude) { result.lastInclude.where = where; } else if (result.include && result.include.length > 0) { result.include[0].where = where; } else { result.where = where; } return result; }, andOrPredicate: function (context) { const result = {}; const predSqs = this.preds.map(function (pred) { return pred.visit(context); }); const wheres = []; const includes = []; if (predSqs.length === 0) { return null; } else if (predSqs.length === 1) { return predSqs[0]; } else { const that = this; predSqs.forEach(function (predSq) { if (!isEmpty(predSq.where)) { wheres.push(predSq.where); } if (!isEmpty(predSq.include)) { const processIncludes = function (sourceIncludes, targetIncludes) { sourceIncludes.forEach(function (sourceInclude) { if (!targetIncludes) { targetIncludes = []; } const include = _.find(targetIncludes, { model: sourceInclude.model }); if (!include) { targetIncludes.push(sourceInclude); } else { if (include.where === null) { include.where = sourceInclude.where; } else if (sourceInclude.where != null) { const where = {}; where[that.op.key] = [include.where, sourceInclude.where]; include.where = where; } if (include.attributes === null || include.attributes.length === 0) { include.attributes = sourceInclude.attributes; } else if (sourceInclude.attributes != null) { include.attributes = _.uniq(include.attributes.concat(sourceInclude.attributes)); } if (!isEmpty(sourceInclude.include)) { processIncludes(sourceInclude.include, include.include); } } }); }; processIncludes(predSq.include, includes); } }); } if (this.op.key === "and") { if (wheres.length > 0) { result.where = wheres.length === 1 ? wheres[0] : { [sequelize_1.Op.and]: wheres }; } // q = Sequelize.and(q1, q2); } else { if (includes.length > 1 || (includes.length === 1 && wheres.length !== 0)) { throw new Error("Cannot translate a query with nested property paths and 'OR' conditions to Sequelize. (Sorry)."); } if (wheres.length > 0) { result.where = wheres.length === 1 ? wheres[0] : { [sequelize_1.Op.or]: wheres }; } // q = Sequelize.or(q1, q2); } result.include = includes; return result; }, anyAllPredicate: function (context) { if (this.op.key === "all") { throw new Error("The 'all' predicate is not currently supported for Sequelize"); } const props = context.entityType.getPropertiesOnPath(this.expr.propertyPath, context.toNameOnServer, true); const parent = {}; const include = context.sequelizeQuery._addInclude(parent, props); const newContext = _.clone(context); newContext.entityType = this.expr.dataType; // after this line the logic below will apply to the include instead of the top level where. // predicate is applied to inner context const r = this.pred.visit(newContext); include.where = r.where || {}; include.required = true; if (r.include) { include.include = r.include; } return { include: parent.include }; }, litExpr: function () { }, propExpr: function (context) { }, fnExpr: function (context) { } }; function makeWhere(p1Value, crit) { let where; if (typeof (p1Value) === 'string') { where = {}; where[p1Value] = crit; } else { where = sequelize_1.Sequelize.where(p1Value, crit); } return where; } function processPropExpr(expr, context, result) { let exprVal; const pp = expr.propertyPath; const props = context.entityType.getPropertiesOnPath(pp, context.toNameOnServer, true); if (props.length > 1) { // handle a nested property path on the LHS - query gets moved into the include // context.include starts out null at top level const parent = {}; const include = context.sequelizeQuery._addInclude(parent, props); include.where = {}; result.include = parent.include; result.lastInclude = include; exprVal = props[props.length - 1].nameOnServer; } else { result.where = {}; exprVal = props[0].nameOnServer; } return exprVal; } function processFnExpr(expr, context, result) { const fnName = expr.fnName; const methodInfo = translateMap[fnName]; if (methodInfo == null) { throw new Error('Unable to locate fn: ' + fnName); } methodInfo.validate && methodInfo.validate(expr.exprs); const exprs = expr.exprs.map(function (ex) { return processNestedExpr(ex, context, result); }); const exprVal = methodInfo.fn(exprs); return exprVal; } function processNestedExpr(expr, context, result) { let exprVal; if (expr.visitorMethodName === 'propExpr') { exprVal = processPropExpr(expr, context, result); return sequelize_1.Sequelize.col(exprVal); } else if (expr.visitorMethodName === 'fnExpr') { const exprVal = processFnExpr(expr, context, result); return exprVal; } else if (expr.visitorMethodName = 'litExpr') { return expr.value; } else { throw new Error("Unable to understand expr for: " + this.expr.visitorMethodName + " - " + this.expr.toString()); } } /** lodash isEmpty function returns true for object with Symbol keys, so we have to extend */ function isEmpty(value) { if (value === null || value === undefined) { return true; } return _.isEmpty(value) && Object.getOwnPropertySymbols(value).length === 0; } const translateMap = { toupper: { fn: function (sqArgs) { return sequelize_1.Sequelize.fn("UPPER", sqArgs[0]); }, validate: function (exprs) { validateMonadicFn("toUpper", exprs); } }, tolower: { fn: function (sqArgs) { return sequelize_1.Sequelize.fn("LOWER", sqArgs[0]); }, validate: function (exprs) { validateMonadicFn("toLower", exprs); } }, substring: { fn: function (sqArgs) { // MySQL's substring is 1 origin - javascript ( and breeze's ) is O origin. return sequelize_1.Sequelize.fn("SUBSTRING", sqArgs[0], 1 + parseInt(sqArgs[1], 10), parseInt(sqArgs[2], 10)); } } }; const simpleFnNames = ['length', 'trim', 'ceiling', 'floor', 'round', 'second', 'minute', 'hour', 'day', 'month', 'year']; simpleFnNames.forEach(function (fnName) { translateMap[fnName] = { fn: function (sqArgs) { return sequelize_1.Sequelize.fn(fnName.toUpperCase(), sqArgs[0]); }, validate: function (exprs) { validateMonadicFn(fnName, exprs); } }; }); function validateMonadicFn(fnName, exprs) { const errTmpl = "Error with call to the '%1' function."; let errMsg; if (exprs.length !== 1) { errMsg = formatString(errTmpl + " This function only takes a single parameter", fnName); } else if (exprs[0].visitorMethodName === 'litExpr') { errMsg = formatString(errTmpl + " The single parameter may not be a literal expression. Param: %2", fnName, exprs[0].toString()); } if (errMsg) { throw new Error(errMsg); } } // Based on fragment from Dean Edwards' Base 2 library // format("a %1 and a %2", "cat", "dog") -> "a cat and a dog" function formatString(str, ...rest) { const args = arguments; const pattern = RegExp("%([1-" + (arguments.length - 1) + "])", "g"); return str.replace(pattern, function (match, index) { return args[index]; }); } return visitor; }()); exports.toSQVisitor = toSQVisitor; function applyNot(q1) { // rules are: // not { a: 1} -> { a: { ne: 1 }} // not { a: { gt: 1 }} -> { a: { le: 1}}} // not { and: { a: 1, b: 2 } -> { or: { a: { $ne: 1 }, b: { $ne 2 }}} // not { or { a: 1, b: 2 } -> { and: [ a: { $ne: 1 }, b: { $ne 2 }]} let results = [], result; const keys = Reflect.ownKeys(q1); for (const k of keys) { const v = q1[k]; if (k === sequelize_1.Op.or) { result = { [sequelize_1.Op.and]: [applyNot(v[0]), applyNot(v[1])] }; } else if (k === sequelize_1.Op.and) { result = { [sequelize_1.Op.or]: [applyNot(v[0]), applyNot(v[1])] }; } else if (_notOps[k]) { result = {}; result[_notOps[k]] = v; } else { result = {}; if (v != null && typeof (v) === "object") { result[k] = applyNot(v); } else { result[k] = { [sequelize_1.Op.ne]: v }; } } results.push(result); } if (results.length === 1) { return results[0]; } else { // Don't think we should ever get here with the current logic because all // queries should only have a single node return { [sequelize_1.Op.or]: results }; } } const _boolOpMap = { eq: { not: sequelize_1.Op.ne }, gt: { sequelizeOp: sequelize_1.Op.gt, not: sequelize_1.Op.lte }, ge: { sequelizeOp: sequelize_1.Op.gte, not: sequelize_1.Op.lt }, lt: { sequelizeOp: sequelize_1.Op.lt, not: sequelize_1.Op.gte }, le: { sequelizeOp: sequelize_1.Op.lte, not: sequelize_1.Op.gt }, ne: { sequelizeOp: sequelize_1.Op.ne, not: sequelize_1.Op.eq }, in: { sequelizeOp: sequelize_1.Op.in }, like: { sequelizeOp: sequelize_1.Op.like } }; const _notOps = { gt: "lte", lte: "gt", gte: "lt", lt: "gte", ne: "eq", eq: "ne", like: "nlike", nlike: "like", in: "notIn", notIn: "in", [sequelize_1.Op.gt]: sequelize_1.Op.lte, [sequelize_1.Op.lte]: sequelize_1.Op.gt, [sequelize_1.Op.gte]: sequelize_1.Op.lt, [sequelize_1.Op.lt]: sequelize_1.Op.gte, [sequelize_1.Op.ne]: sequelize_1.Op.eq, [sequelize_1.Op.like]: sequelize_1.Op.notLike, [sequelize_1.Op.notLike]: sequelize_1.Op.like, [sequelize_1.Op.in]: sequelize_1.Op.notIn, [sequelize_1.Op.notIn]: sequelize_1.Op.in }; // Used to determine if a clause is the result of a Sequelize.and/or method call. // Not currently need because of processAndOr method below // let isSequelizeAnd = function(o) { // return Object.getPrototypeOf(o).constructor == Sequelize.Utils.and; // } // // let isSequelizeOr = function(o) { // return Object.getPrototypeOf(o).constructor == Sequelize.Utils.or; // } // -------------------------------- //# sourceMappingURL=SQVisitor.js.map