UNPKG

slim-ef

Version:

An implementation of basic entity framework functionnalities in typescript

468 lines 23.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SQLQuerySpecificationEvaluator = void 0; const specification_interface_1 = require("./specification.interface"); const typeorm_1 = require("typeorm"); const utils_1 = require("./utils"); const slim_exp_1 = require("slim-exp"); const exception_1 = require("./exception"); const INITIAL_ALIAS = 'entity'; class SQLQuerySpecificationEvaluator { constructor(initialQuery, spec) { this.initialQuery = initialQuery; this.spec = spec; this.registerdAliases = [INITIAL_ALIAS]; this._discriminator = 0; this._queryReady = false; } _applyLeftJoin(query, intialAlias, exp) { return this._applyJoin(query.leftJoinAndSelect, query, intialAlias, exp); } _applyJoin(toApply, query, intialAlias, exp) { const name = typeof exp === 'string' ? exp : slim_exp_1.SlimExpression.nameOf(exp); // expressionParser will parse property name as empty if trying to parse obj => obj if (!name.trim()) throw new exception_1.SQLQuerySpecificationException('You are trying to include self entity'); const { isAlreadyRegistered, propertyName, entityAlias } = this._getFieldNameAndAlias(intialAlias, name); if (!isAlreadyRegistered) toApply.call(query, propertyName, entityAlias); return { propertyName, entityAlias }; } _getFieldNameAndAlias(alias, name, handlingFunction = false) { const splitted = name.split('.'); if (splitted.length >= 2 && !handlingFunction) throw new exception_1.SQLQuerySpecificationException('Include or Where syntax error. Use thenInclude to include composite entity'); name = splitted[0]; const entityAlias = `${alias}_${name}`; const isAlreadyRegistered = this.registerdAliases.includes(entityAlias); if (!isAlreadyRegistered && !handlingFunction) { if (!handlingFunction) this.registerdAliases.push(entityAlias); else throw new exception_1.SQLQuerySpecificationException('Include or Where syntax error. Use thenInclude to include composite entity'); } return { propertyName: `${alias}.${name}`, entityAlias: handlingFunction ? `${alias}_${splitted.join('_')}` : entityAlias, isAlreadyRegistered }; } _getPropertyAlias(f) { const name = typeof f === 'string' ? f : slim_exp_1.SlimExpression.nameOf(f); if (!name.trim()) throw new exception_1.SQLQuerySpecificationException('You are trying to apply boolean condition on self entity'); const propertyAlias = this._getFieldFromRegisteredAlias(INITIAL_ALIAS, name); return propertyAlias; } _getFieldFromRegisteredAlias(initialAlias, name) { const path = name.split('.'); let finalAlias = initialAlias; const finalName = path.pop(); if (path.length !== 0) { finalAlias += `_${path.join('_')}`; } if (!this.registerdAliases.includes(finalAlias)) throw new exception_1.SQLQuerySpecificationException("Condition added to where clause with a property that was not included in the query expression.Please use 'include' with/or 'thenInclude' to include desire entity: " + finalAlias); return `${finalAlias}.${finalName}`; } _isOrBinding(val) { return val === '||' || val === '|'; } _generateQuery(alias, sqlQuery, selector, isFirst = false) { try { const exp = new slim_exp_1.SlimExpression(); exp.fromAction(selector.func, selector.context, false); exp.compile(); const toApply = isFirst ? sqlQuery.where : sqlQuery.andWhere; // applying brackets around each where clause to improve consistency // and fiability of the request const brackets = new typeorm_1.Brackets(wh => { this._chunkDownToQuery(exp, wh, alias, toApply); }); toApply.call(sqlQuery, brackets); return sqlQuery; } catch (error) { throw new exception_1.SQLQuerySpecificationException((error === null || error === void 0 ? void 0 : error.message) + '; func:' + selector.func.toString() + '; context: ' + selector.context ? JSON.stringify(selector.context) : ''); } } _chunkDownToQuery(exp, sqlQuery, alias, initialToApply, closingExp, setupBrackets = 'inactive') { try { const querySequence = this._getQuerySequence(exp, alias, initialToApply, closingExp, setupBrackets); this._applyRecursively(sqlQuery, querySequence); return sqlQuery; } catch (error) { throw new exception_1.SQLQuerySpecificationException(error.message); } } _applyRecursively(sqlQuery, querySequence) { const first = querySequence; const callBrackets = () => { for (const b of first.bracketSequence) { const brackets = new typeorm_1.Brackets(wh => { this._applyRecursively(wh, b); }); b.initialToApply.call(sqlQuery, brackets); } }; const callTopLevel = () => { for (const s of first.topLevelSequence) { s.toApply.call(sqlQuery, s.queryStr, s.queryParams); } }; if (first.firstSequence === 'brackets') { callBrackets(); callTopLevel(); } else { callTopLevel(); callBrackets(); } } _getQuerySequence(exp, alias, initialToApply, closingExp, setupBrackets = 'inactive') { var _a, _b; let e = exp; let toApply = initialToApply; const querySequence = { topLevelSequence: [], bracketSequence: [], initialToApply }; do { // When trying to understand this stuff, remember that logical operators are // associative, no matter the other be it exp1 && exp2 or exp2 && exp1 the // result is the same. So the real focus is to be able to handle the paranthesis // in a clean and oderable manner if ((_a = e.brackets) === null || _a === void 0 ? void 0 : _a.openingExp) { querySequence.bracketSequence.push(this._getQuerySequence(e.brackets.openingExp, alias, toApply, e.brackets.closingExp, 'active')); if (!querySequence.firstSequence) { querySequence.firstSequence = 'brackets'; } } if (e.computeHash() === (closingExp === null || closingExp === void 0 ? void 0 : closingExp.computeHash())) { setupBrackets = 'inactive'; } const sequence = this._buildQueryFromExpression(e.leftHandSide, e.rightHandSide, e.operator, e.expObjectName, alias, toApply, setupBrackets !== 'inactive'); if (!querySequence.firstSequence) { querySequence.firstSequence = 'topLevel'; } querySequence.topLevelSequence.push(...(sequence.topLevelSequence.filter(s => !!s.queryStr) || [])); querySequence.bracketSequence.push(...(sequence.bracketSequence || [])); if (e.next) { toApply = this._isOrBinding(e.next.bindedBy) ? typeorm_1.SelectQueryBuilder.prototype.orWhere : typeorm_1.SelectQueryBuilder.prototype.andWhere; } e = (_b = e === null || e === void 0 ? void 0 : e.next) === null || _b === void 0 ? void 0 : _b.followedBy; } while (e); return querySequence; } _buildQueryFromExpression(lhs, rhs, operator, expObjectName, alias, toApply, isInBracketGroup = false) { let queryStr = ''; let queryParams; if (!lhs && !rhs) { return { topLevelSequence: [{ toApply, queryStr, queryParams }], initialToApply: toApply }; } const lhsProp = lhs.isMethod ? lhs.propertyTree.slice(0, lhs.propertyTree.length - 1).join('.') : lhs.propertyName; const rhsProp = rhs === null || rhs === void 0 ? void 0 : rhs.propertyName; let lhsAlias; if (typeof alias !== 'string') { lhsAlias = alias(expObjectName); } else { lhsAlias = alias; } let rhsAlias; if (rhs) { if (typeof alias !== 'string') { rhsAlias = alias(rhs.implicitContextName); } else { rhsAlias = alias; } } const lhsName = this._getFieldFromRegisteredAlias(lhsAlias, lhsProp); const rhsName = rhsProp && rhsAlias ? this._getFieldFromRegisteredAlias(rhsAlias, rhsProp) : ''; if (lhs.suffixOperator && !lhs.isMethod && !operator && !rhs) { const suffixOp = lhs.suffixOperator; queryStr += ` ${lhsName} ${utils_1.convertToSqlComparisonOperator(utils_1.ComparisonOperators.EQUAL_TO)} ${suffixOp === '!' ? utils_1.SQLConstants.FALSE : utils_1.SQLConstants.TRUE}`; } if (lhs.isMethod) { return this._handleFunctionInvokation(lhsName, lhsAlias, expObjectName, lhs, toApply, isInBracketGroup); } if (!lhs.suffixOperator && operator && rhs) { if (rhs.propertyType !== slim_exp_1.PrimitiveValueTypes.undefined) { const paramName = this._getUniqueParamName(rhs.propertyName); queryStr += ` ${lhs.isMethod ? '' : lhsName} ${utils_1.convertToSqlComparisonOperator(operator, rhs.propertyValue)} :${paramName}`; queryParams = {}; // we need to format the date because typeorm has issue handling it with // sqlite queryParams[paramName] = rhs.propertyType === slim_exp_1.PrimitiveValueTypes.date ? this._polyfillDate(rhs.propertyValue) : rhs.propertyType === slim_exp_1.PrimitiveValueTypes.number ? rhs.propertyValue.toString() : rhs.propertyValue; } else { queryStr += ` ${lhs.isMethod ? '' : lhsName} ${utils_1.convertToSqlComparisonOperator(operator, rhs.propertyValue)} ${rhsName}`; } } return { topLevelSequence: [{ toApply, queryStr, queryParams }], initialToApply: toApply }; } _polyfillDate(val) { const pad = (num, p = 2, bf = true) => bf ? num.toString().padStart(p, '0') : num.toString().padEnd(p, '0'); return `${val.getFullYear()}-${pad(val.getMonth() + 1)}-${pad(val.getDate())} ${pad(val.getHours())}-${pad(val.getMinutes())}-${pad(val.getSeconds())}.${pad(val.getMilliseconds(), 3, false)}`; } _handleFunctionInvokation(name, initialAlias, initialExpObjectName, leftHandSide, toApply, isInBracketGroup = false) { var _a; if (!leftHandSide.content) throw new Error('LeftHandSide Content not defined'); let func; let sqlPart; const propName = name; const content = leftHandSide.content; if (content.type in slim_exp_1.PrimitiveValueTypes && content.methodName in utils_1.SQLStringFunctions) { func = utils_1.SQLStringFunctions[content.methodName]; sqlPart = utils_1.format(func, propName, content.primitiveValue.toString()); return { topLevelSequence: [{ toApply, queryStr: sqlPart }], initialToApply: toApply }; } else if (!(content.type in slim_exp_1.PrimitiveValueTypes) && content.methodName in utils_1.SQLArrayFunctions && Array.isArray(content.primitiveValue)) { func = utils_1.SQLArrayFunctions[content.methodName]; const tb = content.primitiveValue.map(v => `'${v}'`); sqlPart = utils_1.format(func, propName, tb.toString()); return { topLevelSequence: [{ toApply, queryStr: sqlPart }], initialToApply: toApply }; } else if (!(content.type in slim_exp_1.PrimitiveValueTypes) && content.methodName in utils_1.SQLJoinFunctions && content.isExpression) { let LHS = leftHandSide; let alias = initialAlias; const contextNamesAndalias = new Map(); contextNamesAndalias.set(initialExpObjectName, initialAlias); do { // trying to get the property name on which the function is called // i.e t => t.tickets.some(...), we have to get 'tickets' // but since .propertyName attribute gives 'some' we // have to go up the propertytree // LHS.propertyTree.slice(0, LHS.propertyTree.length - 1).join('.') const { entityAlias } = this._getFieldNameAndAlias(alias, LHS.propertyTree.slice(0, LHS.propertyTree.length - 1).join('.'), true); alias = entityAlias; const exp = (_a = LHS === null || LHS === void 0 ? void 0 : LHS.content) === null || _a === void 0 ? void 0 : _a.expression; if (exp) { contextNamesAndalias.set(exp.expObjectName, alias); if (exp.rightHandSide) { return this._getQuerySequence(exp, o => contextNamesAndalias.get(o), toApply, null, isInBracketGroup ? 'active' : 'inactive'); } else if (exp.leftHandSide && exp.leftHandSide.isMethod && !exp.leftHandSide.content.isExpression) { const propTree = exp.leftHandSide.propertyName.split('.'); propTree.pop(); const field = this._getFieldFromRegisteredAlias(alias, propTree.join('.')); return this._handleFunctionInvokation(field, entityAlias, exp.expObjectName, exp.leftHandSide, toApply, false); } } LHS = exp === null || exp === void 0 ? void 0 : exp.leftHandSide; } while (LHS && LHS.isMethod && LHS.content && LHS.content.isExpression); return { topLevelSequence: [{ toApply, queryStr: sqlPart }], initialToApply: toApply }; } throw new Error('Unsupported Function Invokation: ' + content.methodName); } _getUniqueParamName(paramName) { paramName = paramName.replace(/\[|\]|\(|\)|\*|\+|\-|\'|\"/g, ''); return paramName + '_' + ++this._discriminator; } getParams() { return this._query.getParameters(); } getQuery() { return new Promise((res, rej) => { try { this._query = this.initialQuery(INITIAL_ALIAS).select(); // tslint:disable-next-line: one-variable-per-declaration const includes = this.spec.getIncludes(), chainedIncludes = this.spec.getChainedIncludes(), criterias = this.spec.getCriterias(), orderBy = this.spec.getOrderBy(), groupBy = this.spec.getGroupBy(), thenGroupBy = this.spec.getThenGroupBy(), orderByDescending = this.spec.getOrderByDescending(), selector = this.spec.getSelector(), take = this.spec.getTake(), skip = this.spec.getSkip(), thenOrderBy = this.spec.getThenOrderBy(), isPagingEnabled = this.spec.getIsPagingEnabled(), func = this.spec.getFunction(), isDistinct = this.spec.getDistinct(); if (chainedIncludes && chainedIncludes.length > 0) { for (const i of chainedIncludes) { let { entityAlias: currentAlias } = this._applyLeftJoin(this._query, INITIAL_ALIAS, i.initial); i.chain.forEach(c => { const { entityAlias } = this._applyLeftJoin(this._query, currentAlias, c); currentAlias = entityAlias; }); } } if (includes && includes.length > 0) { for (const i of includes) { this._applyLeftJoin(this._query, INITIAL_ALIAS, i); } } if (criterias && criterias.length > 0) { const [first, ...rest] = criterias; this._query = this._generateQuery(INITIAL_ALIAS, this._query, first, true); if (rest && rest.length > 0) { for (const q of rest) { this._query = this._generateQuery(INITIAL_ALIAS, this._query, q); } } } if (func) { this._query = this._applyFunction(this._query, func); } else { let propertyAlias; let isAsc = false; if (orderBy) { propertyAlias = this._getPropertyAlias(orderBy); isAsc = true; this._query = this._query.orderBy(propertyAlias, 'ASC'); } else if (orderByDescending) { propertyAlias = this._getPropertyAlias(orderByDescending); this._query = this._query.orderBy(propertyAlias, 'DESC'); } if ((orderBy || orderByDescending) && (thenOrderBy === null || thenOrderBy === void 0 ? void 0 : thenOrderBy.length)) { thenOrderBy.forEach(tb => { propertyAlias = this._getPropertyAlias(tb); this._query = this._query.addOrderBy(propertyAlias, isAsc ? 'ASC' : 'DESC'); }); } if (groupBy) { propertyAlias = this._getPropertyAlias(groupBy); this._query = this._query.groupBy(propertyAlias); } if (groupBy && (thenGroupBy === null || thenGroupBy === void 0 ? void 0 : thenGroupBy.length)) { thenGroupBy.forEach(tb => { propertyAlias = this._getPropertyAlias(tb); this._query = this._query.addGroupBy(propertyAlias); }); } if (isPagingEnabled) { if (take) { this._query = this._query.take(take); } if (skip) { this._query = this._query.skip(skip); } } if (isDistinct) { this._query = this._query.distinct(true); } if (selector && selector.fieldsToSelect && selector.fieldsToSelect.length > 0) { const toSelect = this._buildSelect(this._query, selector); this._query.select(toSelect); this._selectBuilder = selector.builder; } } this._queryReady = true; return res(this._query.getQuery()); } catch (error) { throw new exception_1.SQLQuerySpecificationException(error); } }); } _buildSelect(_query, selector) { const toSelect = [ ...selector.fieldsToSelect.map(f => this._getPropertyAlias(f.field)) ]; // If id is not present typeorm throws an exception. // So we go get the id field for (const c of this._query.expressionMap.mainAlias.metadata.columns) { if (c.isPrimary) { toSelect.push(this._getFieldFromRegisteredAlias(INITIAL_ALIAS, c.propertyName)); break; } } // I didn't find any other way to add the realtionsId to // the select query. // If the relations' id are not present in the select query // the entities will not be loaded by typeorm for (const j of this._query.expressionMap.joinAttributes) { const propName = j.alias.name.split('_'); propName.pop(); const finalName = propName.join('_'); for (const f of j.relation.foreignKeys) { for (const c of f.columnNames) { toSelect.push(this._getFieldFromRegisteredAlias(finalName, c)); } } } return toSelect; } _applyFunction(query, value) { const field = value.func ? this._getPropertyAlias(value.func) : '*'; return query.select(`${value.type}(${field}) as ${value.type}`); } async executeQuery(type) { if (!this._queryReady) await this.getQuery(); let toApply; let defaultVal; switch (type) { case specification_interface_1.QueryType.ALL: toApply = this._query.getMany; defaultVal = []; break; case specification_interface_1.QueryType.ONE: toApply = this._query.getOne; defaultVal = void 0; break; case specification_interface_1.QueryType.RAW_ONE: toApply = this._query.getRawOne; defaultVal = void 0; break; case specification_interface_1.QueryType.RAW_ALL: toApply = this._query.getRawMany; defaultVal = []; break; default: break; } const result = (await toApply.call(this._query)) || defaultVal; let finalRes; if (this._selectBuilder) { finalRes = Array.isArray(result) ? result.map(r => this._selectBuilder(r)) : this._selectBuilder(result); } else { finalRes = result; } return finalRes; } } exports.SQLQuerySpecificationEvaluator = SQLQuerySpecificationEvaluator; //# sourceMappingURL=specification-evaluator.js.map