@n4it/crud-request
Version:
NestJs CRUD for RESTful APIs - request query builder
314 lines • 11.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RequestQueryParser = void 0;
const crud_util_1 = require("@n4it/crud-util");
const exceptions_1 = require("./exceptions");
const request_query_builder_1 = require("./request-query.builder");
const request_query_validator_1 = require("./request-query.validator");
const qs_1 = require("qs");
class RequestQueryParser {
fields = [];
paramsFilter = [];
authPersist = undefined;
classTransformOptions = undefined;
search;
filter = [];
or = [];
join = [];
sort = [];
limit;
offset;
page;
cache;
includeDeleted;
extra;
_params;
_query;
_paramNames;
_paramsOptions;
_joinConditionParseOptions = {
delimiter: this._options.delimStr,
};
get _options() {
return request_query_builder_1.RequestQueryBuilder.getOptions();
}
static create() {
return new RequestQueryParser();
}
getParsed() {
return {
fields: this.fields,
paramsFilter: this.paramsFilter,
authPersist: this.authPersist,
classTransformOptions: this.classTransformOptions,
search: this.search,
filter: this.filter,
or: this.or,
join: this.join,
sort: this.sort,
limit: this.limit,
offset: this.offset,
page: this.page,
cache: this.cache,
includeDeleted: this.includeDeleted,
extra: this.extra,
};
}
normalizeIndexedParams(input) {
const result = {};
for (const [key, value] of Object.entries(input)) {
const match = key.match(/^([^\[\]]+)\[(\d+)]$/);
if (match) {
const baseKey = match[1];
const index = Number(match[2]);
if (!Array.isArray(result[baseKey])) {
result[baseKey] = [];
}
result[baseKey][index] = value;
}
else {
result[key] = value;
}
}
for (const key in result) {
if (Array.isArray(result[key])) {
result[key] = result[key].filter((v) => v !== undefined);
}
}
return result;
}
parseQuery(query, customOperators = {}) {
if ((0, crud_util_1.isObject)(query)) {
const normalizedQuery = this.normalizeIndexedParams(query);
const paramNames = (0, crud_util_1.objKeys)(normalizedQuery);
if ((0, crud_util_1.hasLength)(paramNames)) {
this._query = normalizedQuery;
this._paramNames = paramNames;
const searchData = this._query[this.getParamNames('search')[0]];
this.search = this.parseSearchQueryParam(searchData);
if ((0, crud_util_1.isNil)(this.search)) {
this.filter = this.parseQueryParam('filter', this.conditionParser.bind(this, 'filter', customOperators));
this.or = this.parseQueryParam('or', this.conditionParser.bind(this, 'or', customOperators));
}
this.fields =
this.parseQueryParam('fields', this.fieldsParser.bind(this))[0] || [];
this.join = this.parseQueryParam('join', this.joinParser.bind(this));
this.sort = this.parseQueryParam('sort', this.sortParser.bind(this));
this.limit = this.parseQueryParam('limit', this.numericParser.bind(this, 'limit'))[0];
this.offset = this.parseQueryParam('offset', this.numericParser.bind(this, 'offset'))[0];
this.page = this.parseQueryParam('page', this.numericParser.bind(this, 'page'))[0];
this.cache = this.parseQueryParam('cache', this.numericParser.bind(this, 'cache'))[0];
this.includeDeleted = this.parseQueryParam('includeDeleted', this.numericParser.bind(this, 'includeDeleted'))[0];
this.extra = this.parseExtraFromQueryParam();
}
}
return this;
}
parseParams(params, options) {
if ((0, crud_util_1.isObject)(params)) {
const paramNames = (0, crud_util_1.objKeys)(params);
if ((0, crud_util_1.hasLength)(paramNames)) {
this._params = params;
this._paramsOptions = options;
this.paramsFilter = paramNames
.map((name) => this.paramParser(name))
.filter((filter) => filter);
}
}
return this;
}
setAuthPersist(persist = {}) {
this.authPersist = persist || {};
}
setClassTransformOptions(options = {}) {
this.classTransformOptions = options || {};
}
convertFilterToSearch(filter) {
const isEmptyValue = {
isnull: true,
notnull: true,
};
return filter
? {
[filter.field]: {
[filter.operator]: isEmptyValue[filter.operator]
? isEmptyValue[filter.operator]
: filter.value,
},
}
: {};
}
getParamNames(type) {
return this._paramNames.filter((p) => {
const name = this._options.paramNamesMap[type];
return (0, crud_util_1.isString)(name) ? name === p : name.some((m) => m === p);
});
}
getParamValues(value, parser) {
if ((0, crud_util_1.isStringFull)(value)) {
return [parser.call(this, value)];
}
if ((0, crud_util_1.isArrayFull)(value)) {
return value.map((val) => parser(val));
}
return [];
}
parseQueryParam(type, parser) {
const param = this.getParamNames(type);
if ((0, crud_util_1.isArrayFull)(param)) {
return param.reduce((a, name) => [...a, ...this.getParamValues(this._query[name], parser)], []);
}
return [];
}
parseExtraFromQueryParam() {
const params = Array.isArray(this._options.paramNamesMap.extra)
? this._options.paramNamesMap.extra
: [this._options.paramNamesMap.extra];
const extraKeys = Object.keys(this._query || {})
.filter((k) => params.find((p) => k?.startsWith(p)))
.reduce((o, k) => {
const key = k.replace('extra.', '');
this.parseDotChainToObject(this._query[k], key, o);
return o;
}, {});
return Object.keys(extraKeys).length > 0 ? extraKeys : undefined;
}
parseDotChainToObject(data, key, result = {}) {
if (key.includes('.')) {
const keys = key.split('.');
const firstKey = keys.shift();
result[firstKey] = {};
this.parseDotChainToObject(data, keys.join('.'), result[firstKey]);
}
else {
result[key] = this.parseValue(data);
}
}
parseValue(val) {
try {
const parsed = JSON.parse(val);
if (!(0, crud_util_1.isDate)(parsed) && (0, crud_util_1.isObject)(parsed)) {
return val;
}
else if (typeof parsed === 'number' &&
parsed.toLocaleString('fullwide', { useGrouping: false }) !== val) {
return val;
}
return parsed;
}
catch (ignored) {
if ((0, crud_util_1.isDateString)(val)) {
return new Date(val);
}
return val;
}
}
parseValues(vals) {
if ((0, crud_util_1.isArrayFull)(vals)) {
return vals.map((v) => this.parseValue(v));
}
else {
return this.parseValue(vals);
}
}
fieldsParser(data) {
return data.split(this._options.delimStr);
}
parseSearchQueryParam(d) {
try {
if ((0, crud_util_1.isNil)(d)) {
return undefined;
}
const data = JSON.parse(d);
if (!(0, crud_util_1.isObject)(data)) {
throw new Error();
}
return data;
}
catch (_) {
throw new exceptions_1.RequestQueryException('Invalid search param. JSON expected');
}
}
conditionParser(cond, customOperators, data) {
const isArrayValue = [
'in',
'notin',
'between',
'$in',
'$notin',
'$between',
'$inL',
'$notinL',
'$contArr',
'$intersectsArr',
].concat(Object.keys(customOperators).filter((op) => customOperators[op].isArray));
const isEmptyValue = ['isnull', 'notnull', '$isnull', '$notnull'];
const param = data.split(this._options.delim);
const field = param[0];
const operator = param[1];
let value = param[2] || '';
if (isArrayValue.some((name) => name === operator)) {
value = value.split(this._options.delimStr);
}
value = this.parseValues(value);
if (!isEmptyValue.some((name) => name === operator) && !(0, crud_util_1.hasValue)(value)) {
throw new exceptions_1.RequestQueryException(`Invalid ${cond} value`);
}
const condition = { field, operator, value };
(0, request_query_validator_1.validateCondition)(condition, cond, customOperators);
return condition;
}
parseJoinConditions(conditionsString) {
const conditions = (0, qs_1.parse)(conditionsString, this._joinConditionParseOptions)['on'];
return conditions.map((cond) => this.conditionParser('filter', {}, cond));
}
joinParser(data) {
const param = data.split(this._options.delim);
const field = param[0];
const selectString = param[1];
const conditions = param.slice(2).join(this._options.delim);
const join = {
field,
select: selectString ? selectString.split(this._options.delimStr) : undefined,
on: (0, crud_util_1.isStringFull)(conditions) ? this.parseJoinConditions(conditions) : undefined,
};
(0, request_query_validator_1.validateJoin)(join);
return join;
}
sortParser(data) {
const param = data.split(this._options.delimStr);
const sort = {
field: param[0],
order: param[1],
};
(0, request_query_validator_1.validateSort)(sort);
return sort;
}
numericParser(num, data) {
const val = this.parseValue(data);
(0, request_query_validator_1.validateNumeric)(val, num);
return val;
}
paramParser(name) {
(0, request_query_validator_1.validateParamOption)(this._paramsOptions, name);
const option = this._paramsOptions[name];
if (option.disabled) {
return undefined;
}
let value = this._params[name];
switch (option.type) {
case 'number':
value = this.parseValue(value);
(0, request_query_validator_1.validateNumeric)(value, `param ${name}`);
break;
case 'uuid':
(0, request_query_validator_1.validateUUID)(value, name);
break;
default:
break;
}
return { field: option.field, operator: '$eq', value };
}
}
exports.RequestQueryParser = RequestQueryParser;
//# sourceMappingURL=request-query.parser.js.map