src/services/filters-parser.js
import {
BaseFiltersParser, BaseOperatorDateParser, Schemas, SchemaUtils,
} from 'forest-express';
import Operators from '../utils/operators';
import { NoMatchingOperatorError } from './errors';
const { getReferenceSchema, getReferenceField } = require('../utils/query');
function FiltersParser(modelSchema, timezone, options) {
this.OPERATORS = Operators.getInstance(options);
this.operatorDateParser = new BaseOperatorDateParser({ operators: this.OPERATORS, timezone });
this.perform = async (filtersString) =>
BaseFiltersParser.perform(
filtersString, this.formatAggregation, this.formatCondition, modelSchema,
);
this.formatAggregation = async (aggregator, formattedConditions) => {
const aggregatorOperator = this.formatAggregatorOperator(aggregator);
return { [aggregatorOperator]: formattedConditions };
};
this.formatCondition = async (condition, isSmartField = false) => {
const isTextField = this.isTextField(condition.field);
if (isSmartField) {
return this.formatOperatorValue(condition.operator, condition.value, isTextField);
}
const formattedField = this.formatField(condition.field);
if (this.operatorDateParser.isDateOperator(condition.operator)) {
return {
[formattedField]: this.operatorDateParser.getDateFilter(
condition.operator,
condition.value,
),
};
}
return {
[formattedField]: this.formatOperatorValue(condition.operator, condition.value, isTextField),
};
};
this.formatAggregatorOperator = (aggregatorOperator) => {
switch (aggregatorOperator) {
case 'and':
return this.OPERATORS.AND;
case 'or':
return this.OPERATORS.OR;
default:
throw new NoMatchingOperatorError();
}
};
this.formatOperatorValue = (operator, value, isTextField = false) => {
switch (operator) {
case 'not':
return { [this.OPERATORS.NOT]: value };
case 'greater_than':
case 'after':
return { [this.OPERATORS.GT]: value };
case 'less_than':
case 'before':
return { [this.OPERATORS.LT]: value };
case 'contains':
return { [this.OPERATORS.LIKE]: `%${value}%` };
case 'starts_with':
return { [this.OPERATORS.LIKE]: `${value}%` };
case 'ends_with':
return { [this.OPERATORS.LIKE]: `%${value}` };
case 'not_contains':
return { [this.OPERATORS.NOT_LIKE]: `%${value}%` };
case 'present':
return { [this.OPERATORS.NE]: null };
case 'not_equal':
return { [this.OPERATORS.NE]: value };
case 'blank':
return isTextField ? {
[this.OPERATORS.OR]: [{
[this.OPERATORS.EQ]: null,
}, {
[this.OPERATORS.EQ]: '',
}],
} : { [this.OPERATORS.EQ]: null };
case 'equal':
return { [this.OPERATORS.EQ]: value };
case 'includes_all':
return { [this.OPERATORS.CONTAINS]: value };
case 'in':
return typeof value === 'string'
? { [this.OPERATORS.IN]: value.split(',').map((elem) => elem.trim()) }
: { [this.OPERATORS.IN]: value };
default:
throw new NoMatchingOperatorError();
}
};
this.formatField = (field) => {
if (field.includes(':')) {
const [associationName, fieldName] = field.split(':');
return `$${getReferenceField(Schemas.schemas, modelSchema, associationName, fieldName)}$`;
}
return field;
};
this.isTextField = (field) => {
if (field.includes(':')) {
const [associationName, fieldName] = field.split(':');
const associationSchema = getReferenceSchema(
Schemas.schemas, modelSchema, associationName, fieldName,
);
if (associationSchema) {
return SchemaUtils.getFieldType(associationSchema, field) === 'String';
}
return false;
}
return SchemaUtils.getFieldType(modelSchema, field) === 'String';
};
// NOTICE: Look for a previous interval condition matching the following:
// - If the filter is a simple condition at the root the check is done right away.
// - There can't be a previous interval condition if the aggregator is 'or' (no meaning).
// - The condition's operator has to be elligible for a previous interval.
// - There can't be two previous interval condition.
this.getPreviousIntervalCondition = (filtersString) => {
const filters = BaseFiltersParser.parseFiltersString(filtersString);
let currentPreviousInterval = null;
// NOTICE: Leaf condition at root
if (filters && !filters.aggregator) {
if (this.operatorDateParser.hasPreviousDateInterval(filters.operator)) {
return filters;
}
return null;
}
// NOTICE: No previous interval condition when 'or' aggregator
if (filters.aggregator === 'and') {
for (let i = 0; i < filters.conditions.length; i += 1) {
const condition = filters.conditions[i];
// NOTICE: Nested filters
if (condition.aggregator) {
return null;
}
if (this.operatorDateParser.hasPreviousDateInterval(condition.operator)) {
// NOTICE: There can't be two previousInterval.
if (currentPreviousInterval) {
return null;
}
currentPreviousInterval = condition;
}
}
}
return currentPreviousInterval;
};
this.getAssociations = async (filtersString) => BaseFiltersParser.getAssociations(filtersString);
}
module.exports = FiltersParser;