@bcc-code/feathers-arangodb
Version:
ArangoDB Service/Adapter for FeathersJS
248 lines (247 loc) • 9.75 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.QueryBuilder = exports.LogicalOperator = void 0;
const isObject_1 = __importDefault(require("lodash/isObject"));
const get_1 = __importDefault(require("lodash/get"));
const set_1 = __importDefault(require("lodash/set"));
const arangojs_1 = require("arangojs");
const searchBuilder_1 = require("./searchBuilder");
const logger_1 = __importDefault(require("./logger"));
const errors_1 = require("@feathersjs/errors");
var LogicalOperator;
(function (LogicalOperator) {
LogicalOperator["And"] = " AND ";
LogicalOperator["Or"] = " OR ";
})(LogicalOperator || (exports.LogicalOperator = LogicalOperator = {}));
class QueryBuilder {
;
constructor(params, docName = "doc", returnDocName = "doc", searchFields = []) {
this.reserved = [
"$select",
"$limit",
"$skip",
"$sort",
"$in",
"$nin",
"$lt",
"$lte",
"$gt",
"$gte",
"$ne",
"$not",
"$or",
"$and",
"$aql",
"$resolve",
"$search",
"$elemMatch",
];
this.bindVars = {};
this.maxLimit = 1000000000; // A billion records...
this._limit = -1;
this._countNeed = "";
this._skip = 0;
this.docName = "doc";
this.returnDocName = "doc";
this.varCount = 0;
this.searchFields = searchFields;
this.docName = docName;
this.returnDocName = returnDocName;
this.create(params);
}
getParameterizedPath(path, basePath) {
const pathArray = path.split('.').map((field) => (0, arangojs_1.aql) `[${field}]`);
return arangojs_1.aql.join([
arangojs_1.aql.literal(basePath),
...pathArray
], '');
}
projectRecursive(o) {
const result = Object.keys(o).map((field, ind) => {
const v = (0, get_1.default)(o, field);
return arangojs_1.aql.join([
(0, arangojs_1.aql) `[${field}]:`,
(0, isObject_1.default)(v)
? arangojs_1.aql.join([
arangojs_1.aql.literal("{"),
this.projectRecursive(v),
arangojs_1.aql.literal("}"),
])
: this.getParameterizedPath(v, this.returnDocName),
], " ");
});
return arangojs_1.aql.join(result, ", ");
}
selectBuilder(params) {
var _a;
const select = (_a = params.query) === null || _a === void 0 ? void 0 : _a.$select;
if (!(select === null || select === void 0 ? void 0 : select.length))
return arangojs_1.aql.literal(`RETURN ${this.returnDocName}`);
const ret = {};
select.forEach((fieldName) => {
(0, set_1.default)(ret, fieldName, fieldName);
});
(0, set_1.default)(ret, '_key', '_key');
return arangojs_1.aql.join([
(0, arangojs_1.aql) `RETURN {`,
this.projectRecursive(ret),
(0, arangojs_1.aql) `}`,
], " ");
}
create(params) {
this.returnFilter = this.selectBuilder(params);
const query = (0, get_1.default)(params, "query", null);
logger_1.default.debug("Query object received from client:", query);
this._runCheck(query);
return this;
}
_runCheck(query) {
if (!isQueryObject(query))
return this;
if (query.$limit !== undefined)
this._limit = parseIntTypeSafe(query.$limit);
if (query.$skip !== undefined)
this._skip = parseIntTypeSafe(query.$skip);
if (query.$sort !== undefined)
this._sort = this.addSort(query.$sort);
if (query.$search !== undefined)
this._search = this.addSearch(this.searchFields, query.$search);
this._filter = this._aqlFilterFromFeathersQuery(query, arangojs_1.aql.literal(this.docName));
}
_aqlFilterFromFeathersQuery(feathersQuery, aqlFilterVar) {
if (typeof feathersQuery !== "object" || feathersQuery === null) {
return arangojs_1.aql.join([aqlFilterVar, (0, arangojs_1.aql) `${feathersQuery}`], " == ");
}
const aqlFilters = [];
for (const [key, value] of Object.entries(feathersQuery)) {
let operator;
switch (key) {
case "$in":
operator = " ANY == ";
break;
case "$nin":
operator = " NONE == ";
break;
case "$not":
case "$ne":
operator = " != ";
break;
case "$lt":
operator = " > ";
break;
case "$lte":
operator = " >= ";
break;
case "$gt":
operator = " < ";
break;
case "$gte":
operator = " <= ";
break;
case "$elemMatch":
aqlFilters.push(this._aqlFilterArrayElement(value, aqlFilterVar));
continue;
case "$or":
aqlFilters.push(this._aqlFilterFromFeathersQueryArray(value, aqlFilterVar, LogicalOperator.Or));
continue;
case "$and":
aqlFilters.push(this._aqlFilterFromFeathersQueryArray(value, aqlFilterVar, LogicalOperator.And));
continue;
case "$size":
aqlFilters.push(this._aqlFilterFromFeathersQuery(value, (0, arangojs_1.aql) `LENGTH(${aqlFilterVar})`));
continue;
}
if (operator) {
aqlFilters.push(arangojs_1.aql.join([
(0, arangojs_1.aql) `${value}`,
aqlFilterVar,
], operator));
continue;
}
if (!this.reserved.includes(key)) {
aqlFilters.push(this._aqlFilterFromFeathersQuery(value, arangojs_1.aql.join([aqlFilterVar, (0, arangojs_1.aql) `[${key}]`], '')));
}
}
return this._joinAqlFiltersWithOperator(aqlFilters, LogicalOperator.And);
}
_aqlFilterArrayElement(elementQuery, aqlFilterVar) {
const elementFilter = (0, arangojs_1.aql) `FILTER ${this._aqlFilterFromFeathersQuery(elementQuery, (0, arangojs_1.aql) `CURRENT`)}`;
return (0, arangojs_1.aql) `LENGTH(${aqlFilterVar}[* ${elementFilter} RETURN CURRENT])`;
}
_aqlFilterFromFeathersQueryArray(feathersQueries, aqlFilterVar, operator) {
const aqlFilters = feathersQueries.map((f) => this._aqlFilterFromFeathersQuery(f, aqlFilterVar));
return this._joinAqlFiltersWithOperator(aqlFilters, operator);
}
_joinAqlFiltersWithOperator(aqlFilters, operator) {
const filtered = aqlFilters.filter((c) => c !== undefined);
if (!filtered.length)
return undefined;
const combined = arangojs_1.aql.join(filtered, operator);
if (operator === LogicalOperator.And)
return combined;
return (0, arangojs_1.aql) `(${combined})`;
}
addSort(sort) {
if (!isQueryObject(sort))
throw new errors_1.BadRequest("Sort has incorrect type");
if (Object.keys(sort).length > 0) {
return arangojs_1.aql.join(Object.keys(sort).map((key) => {
return arangojs_1.aql.join([
this.getParameterizedPath(key, this.docName),
arangojs_1.aql.literal(parseIntTypeSafe(sort[key]) === -1 ? "DESC" : "")
], ' ');
}), ", ");
}
}
addSearch(searchFields, search) {
if (!searchFields.length)
throw new errors_1.BadRequest('A search has been attempted on a collection where no search logic has been defined');
if (!(0, searchBuilder_1.isQueryTypeCorrect)(search)) {
throw new errors_1.BadRequest('Invalid query type');
}
return (0, searchBuilder_1.generateFuzzyStatement)(searchFields, search);
}
get limit() {
if (this._limit === -1 && this._skip === 0)
return (0, arangojs_1.aql) ``;
const realLimit = this._limit > -1 ? this._limit : this.maxLimit;
return (0, arangojs_1.aql) `LIMIT ${this._skip}, ${realLimit}`;
}
get sort() {
if (this._search) {
return (0, arangojs_1.aql) `SORT BM25(${arangojs_1.aql.literal(this.docName)}) desc`;
}
if (this._sort) {
return (0, arangojs_1.aql) `SORT ${this._sort}`;
}
return (0, arangojs_1.aql) ``;
}
get filter() {
const filterParts = [];
if (this._search) {
filterParts.push((0, arangojs_1.aql) `(${this._search})`);
}
if (this._filter) {
filterParts.push((0, arangojs_1.aql) `(${this._filter})`);
}
if (!filterParts.length)
return undefined;
return arangojs_1.aql.join(filterParts, LogicalOperator.And);
}
}
exports.QueryBuilder = QueryBuilder;
function isQueryObject(query) {
if (!query)
return false;
return typeof query === "object";
}
function parseIntTypeSafe(value) {
if (typeof value === "number")
return value;
if (typeof value !== "string")
return NaN;
return parseInt(value);
}