UNPKG

nestjs-paginate

Version:

Pagination and filtering helper method for TypeORM repositories or query builders using Nest.js framework.

292 lines 13 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.OperatorSymbolToFunction = exports.FilterComparator = exports.FilterSuffix = exports.FilterOperator = void 0; exports.isOperator = isOperator; exports.isSuffix = isSuffix; exports.isComparator = isComparator; exports.fixQueryParam = fixQueryParam; exports.generatePredicateCondition = generatePredicateCondition; exports.addWhereCondition = addWhereCondition; exports.parseFilterToken = parseFilterToken; exports.parseFilter = parseFilter; exports.addFilter = addFilter; const lodash_1 = require("lodash"); const typeorm_1 = require("typeorm"); const helper_1 = require("./helper"); var FilterOperator; (function (FilterOperator) { FilterOperator["EQ"] = "$eq"; FilterOperator["GT"] = "$gt"; FilterOperator["GTE"] = "$gte"; FilterOperator["IN"] = "$in"; FilterOperator["NULL"] = "$null"; FilterOperator["LT"] = "$lt"; FilterOperator["LTE"] = "$lte"; FilterOperator["BTW"] = "$btw"; FilterOperator["ILIKE"] = "$ilike"; FilterOperator["SW"] = "$sw"; FilterOperator["CONTAINS"] = "$contains"; })(FilterOperator || (exports.FilterOperator = FilterOperator = {})); function isOperator(value) { return (0, lodash_1.values)(FilterOperator).includes(value); } var FilterSuffix; (function (FilterSuffix) { FilterSuffix["NOT"] = "$not"; })(FilterSuffix || (exports.FilterSuffix = FilterSuffix = {})); function isSuffix(value) { return (0, lodash_1.values)(FilterSuffix).includes(value); } var FilterComparator; (function (FilterComparator) { FilterComparator["AND"] = "$and"; FilterComparator["OR"] = "$or"; })(FilterComparator || (exports.FilterComparator = FilterComparator = {})); function isComparator(value) { return (0, lodash_1.values)(FilterComparator).includes(value); } exports.OperatorSymbolToFunction = new Map([ [FilterOperator.EQ, typeorm_1.Equal], [FilterOperator.GT, typeorm_1.MoreThan], [FilterOperator.GTE, typeorm_1.MoreThanOrEqual], [FilterOperator.IN, typeorm_1.In], [FilterOperator.NULL, typeorm_1.IsNull], [FilterOperator.LT, typeorm_1.LessThan], [FilterOperator.LTE, typeorm_1.LessThanOrEqual], [FilterOperator.BTW, typeorm_1.Between], [FilterOperator.ILIKE, typeorm_1.ILike], [FilterSuffix.NOT, typeorm_1.Not], [FilterOperator.SW, typeorm_1.ILike], [FilterOperator.CONTAINS, typeorm_1.ArrayContains], ]); // This function is used to fix the query parameters when using relation, embeded or virtual properties // It will replace the column name with the alias name and return the new parameters function fixQueryParam(alias, column, filter, condition, parameters) { const isNotOperator = condition.operator === 'not'; const conditionFixer = (alias, column, filter, operator, parameters) => { let condition_params = undefined; let params = parameters; switch (operator) { case 'between': condition_params = [alias, `:${column}_from`, `:${column}_to`]; params = { [column + '_from']: filter.findOperator.value[0], [column + '_to']: filter.findOperator.value[1], }; break; case 'in': condition_params = [alias, `:...${column}`]; break; default: condition_params = [alias, `:${column}`]; break; } return { condition_params, params }; }; const { condition_params, params } = conditionFixer(alias, column, filter, isNotOperator ? condition['condition']['operator'] : condition.operator, parameters); if (isNotOperator) { condition['condition']['parameters'] = condition_params; } else { condition.parameters = condition_params; } return params; } function generatePredicateCondition(qb, column, filter, alias, isVirtualProperty = false) { return qb['getWherePredicateCondition'](isVirtualProperty ? column : alias, filter.findOperator); } function addWhereCondition(qb, column, filter) { const columnProperties = (0, helper_1.getPropertiesByColumnName)(column); const { isVirtualProperty, query: virtualQuery } = (0, helper_1.extractVirtualProperty)(qb, columnProperties); const isRelation = (0, helper_1.checkIsRelation)(qb, columnProperties.propertyPath); const isEmbedded = (0, helper_1.checkIsEmbedded)(qb, columnProperties.propertyPath); const isArray = (0, helper_1.checkIsArray)(qb, columnProperties.propertyName); const alias = (0, helper_1.fixColumnAlias)(columnProperties, qb.alias, isRelation, isVirtualProperty, isEmbedded, virtualQuery); filter[column].forEach((columnFilter, index) => { var _a; const columnNamePerIteration = `${columnProperties.column}${index}`; const condition = generatePredicateCondition(qb, columnProperties.column, columnFilter, alias, isVirtualProperty); const parameters = fixQueryParam(alias, columnNamePerIteration, columnFilter, condition, { [columnNamePerIteration]: columnFilter.findOperator.value, }); if (isArray && ((_a = condition.parameters) === null || _a === void 0 ? void 0 : _a.length) && !['not', 'isNull', 'arrayContains'].includes(condition.operator)) { condition.parameters[0] = `cardinality(${condition.parameters[0]})`; } if (columnFilter.comparator === FilterComparator.OR) { qb.orWhere(qb['createWhereConditionExpression'](condition), parameters); } else { qb.andWhere(qb['createWhereConditionExpression'](condition), parameters); } }); } function parseFilterToken(raw) { if (raw === undefined || raw === null) { return null; } const token = { comparator: FilterComparator.AND, suffix: undefined, operator: FilterOperator.EQ, value: raw, }; const MAX_OPERTATOR = 4; // max 4 operator es: $and:$not:$eq:$null const OPERAND_SEPARATOR = ':'; const matches = raw.split(OPERAND_SEPARATOR); const maxOperandCount = matches.length > MAX_OPERTATOR ? MAX_OPERTATOR : matches.length; const notValue = []; for (let i = 0; i < maxOperandCount; i++) { const match = matches[i]; if (isComparator(match)) { token.comparator = match; } else if (isSuffix(match)) { token.suffix = match; } else if (isOperator(match)) { token.operator = match; } else { break; } notValue.push(match); } if (notValue.length) { token.value = token.operator === FilterOperator.NULL ? undefined : raw.replace(`${notValue.join(OPERAND_SEPARATOR)}${OPERAND_SEPARATOR}`, ''); } return token; } function fixColumnFilterValue(column, qb, isJsonb = false) { const columnProperties = (0, helper_1.getPropertiesByColumnName)(column); const virtualProperty = (0, helper_1.extractVirtualProperty)(qb, columnProperties); const columnType = virtualProperty.type; return (value) => { if (((0, helper_1.isDateColumnType)(columnType) || isJsonb) && (0, helper_1.isISODate)(value)) { return new Date(value); } if ((columnType === Number || isJsonb) && !Number.isNaN(value)) { return Number(value); } return value; }; } function parseFilter(query, filterableColumns, qb) { const filter = {}; if (!filterableColumns || !query.filter) { return {}; } for (const column of Object.keys(query.filter)) { if (!(column in filterableColumns)) { continue; } const allowedOperators = filterableColumns[column]; const input = query.filter[column]; const statements = !Array.isArray(input) ? [input] : input; for (const raw of statements) { const token = parseFilterToken(raw); if (!token) { continue; } if (allowedOperators === true) { if (token.operator && !isOperator(token.operator)) { continue; } if (token.suffix && !isSuffix(token.suffix)) { continue; } } else { if (token.operator && token.operator !== FilterOperator.EQ && !allowedOperators.includes(token.operator)) { continue; } if (token.suffix && !allowedOperators.includes(token.suffix)) { continue; } } const params = { comparator: token.comparator, findOperator: undefined, }; const fixValue = fixColumnFilterValue(column, qb); const columnProperties = (0, helper_1.getPropertiesByColumnName)(column); const isJsonb = (0, helper_1.checkIsJsonb)(qb, columnProperties.column); switch (token.operator) { case FilterOperator.BTW: params.findOperator = exports.OperatorSymbolToFunction.get(token.operator)(...token.value.split(',').map(fixValue)); break; case FilterOperator.IN: case FilterOperator.CONTAINS: params.findOperator = exports.OperatorSymbolToFunction.get(token.operator)(token.value.split(',')); break; case FilterOperator.ILIKE: params.findOperator = exports.OperatorSymbolToFunction.get(token.operator)(`%${token.value}%`); break; case FilterOperator.SW: params.findOperator = exports.OperatorSymbolToFunction.get(token.operator)(`${token.value}%`); break; default: params.findOperator = exports.OperatorSymbolToFunction.get(token.operator)(fixValue(token.value)); } if (isJsonb) { const parts = column.split('.'); const dbColumnName = parts[parts.length - 2]; const jsonColumnName = parts[parts.length - 1]; const jsonFixValue = fixColumnFilterValue(column, qb, true); const jsonParams = { comparator: params.comparator, findOperator: (0, typeorm_1.JsonContains)({ [jsonColumnName]: jsonFixValue(token.value), //! Below seems to not be possible from my understanding, https://github.com/typeorm/typeorm/pull/9665 //! This limits the functionaltiy to $eq only for json columns, which is a bit of a shame. //! If this is fixed or changed, we can use the commented line below instead. //[jsonColumnName]: params.findOperator, }), }; filter[dbColumnName] = [...(filter[column] || []), jsonParams]; } else { filter[column] = [...(filter[column] || []), params]; } if (token.suffix) { const lastFilterElement = filter[column].length - 1; filter[column][lastFilterElement].findOperator = exports.OperatorSymbolToFunction.get(token.suffix)(filter[column][lastFilterElement].findOperator); } } } return filter; } function addFilter(qb, query, filterableColumns) { const filter = parseFilter(query, filterableColumns, qb); const filterEntries = Object.entries(filter); const orFilters = filterEntries.filter(([_, value]) => value[0].comparator === '$or'); const andFilters = filterEntries.filter(([_, value]) => value[0].comparator === '$and'); qb.andWhere(new typeorm_1.Brackets((qb) => { for (const [column] of orFilters) { addWhereCondition(qb, column, filter); } })); for (const [column] of andFilters) { qb.andWhere(new typeorm_1.Brackets((qb) => { addWhereCondition(qb, column, filter); })); } // Set the join type of every relationship used in a filter to `innerJoinAndSelect` // so that records without that relationships don't show up in filters on their columns. return Object.fromEntries(filterEntries .map(([key]) => [key, (0, helper_1.getPropertiesByColumnName)(key)]) .filter(([, properties]) => properties.propertyPath) .flatMap(([, properties]) => { const nesting = properties.column.split('.'); return Array.from({ length: nesting.length - 1 }, (_, i) => nesting.slice(0, i + 1).join('.')) .filter((relation) => (0, helper_1.checkIsNestedRelation)(qb, relation)) .map((relation) => [relation, 'innerJoinAndSelect']); })); } //# sourceMappingURL=filter.js.map