breeze-sequelize
Version:
Breeze Sequelize server implementation
409 lines • 17.4 kB
JavaScript
;
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