nestjs-paginate
Version:
Pagination and filtering helper method for TypeORM repositories or query builders using Nest.js framework.
292 lines • 13 kB
JavaScript
;
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