@c8y/client
Version:
Client application programming interface to access the Cumulocity IoT-Platform REST services.
272 lines • 11.2 kB
JavaScript
export class QueriesUtil {
constructor() {
this.operatorFns = {
__not: operand => {
return `not(${this.buildQueryFilter(operand, null)})`;
},
__and: operand => {
return this.buildQueryFilter(operand, null, 'and');
},
__or: operand => {
return this.buildQueryFilter(operand, null, 'or');
},
__eq: (operand, contextKey) => {
if (typeof operand === 'object' && operand !== null) {
return this.buildQueryFilter(operand, contextKey);
}
return `${contextKey} eq ${this.quoteString(operand)}`;
},
__gt: (operand, contextKey) => {
return `${contextKey} gt ${this.useAsFloat(operand)}`;
},
__ge: (operand, contextKey) => {
return `${contextKey} ge ${this.useAsFloat(operand)}`;
},
__lt: (operand, contextKey) => {
return `${contextKey} lt ${this.useAsFloat(operand)}`;
},
__le: (operand, contextKey) => {
return `${contextKey} le ${this.useAsFloat(operand)}`;
},
__in: (operand, contextKey) => {
const statements = operand
.filter(op => !!op)
.map(op => {
return `${contextKey} eq ${this.quoteString(op)}`;
});
return this.glue(statements, 'or');
},
__bygroupid: (operand) => {
return `bygroupid(${operand})`;
},
__has: (operand) => {
return `has(${operand})`;
},
__isinhierarchyof: (operand) => {
return `isinhierarchyof(${operand})`;
},
__hasany: (operand) => {
return `hasany(${operand})`;
},
__useFilterQueryString: (queryString) => {
var _a;
// match everything inside the most exterior parentheses, including them
const query = (_a = queryString.match(/\(.*\)/)) === null || _a === void 0 ? void 0 : _a[0];
// get rid of the most exterior parentheses
return query === null || query === void 0 ? void 0 : query.substring(1, query.length - 1);
}
};
}
/**
* Builds query string from provided query object.
*
* @param query Object containing filters and sort order for querying managed objects. Supported filters are:
* - **__and** - Specifies conditions, e.g. `{__and: [{__has: 'c8y_IsDevice'}, {'count': {__gt: 0}}]}`.
* - **__or** - Specifies alternative conditions, e.g. `{__or: [{__bygroupid: 10300}, {__bygroupid: 10400}]}`.
* - **__eq** - Specified fragment must be equal to given value, e.g. `{'status': 'AVAILABLE'}` (no nested object required).
* - **__lt** - Specified fragment must be less then given value, e.g. `{'count': {__lt: 10}}`.
* - **__gt** - Specified fragment must be greater then given value, e.g. `{'count': {__gt: 0}}`.
* - **__in** - Specified fragment must be equal to one of values in the list, e.g. `{'status': {__in: ['AVAILABLE', 'UNAVAILABLE']}}`.
* - **__not** - Negates condition, e.g. `{__not: {'status': 'AVAILABLE'}}`.
* - **__bygroupid** - True if filtered managed object is assigned to given group, e.g. `{__bygroupid: 10300}`.
* - **__has** - Specified fragment must have a value defined, e.g. `{__has: 'c8y_IsDevice'}`.
* - **__hasany** - Matches objects having at least one of the fragments defined, e.g. `{__has: ['c8y_IsDevice', 'c8y_Dashboard']}`.
* - **__isinhierarchyof** - Matches objects that are in hierarchy of given managed object, e.g. `{__isinhierarchyof: '12345'}`.
* - **__useFilterQueryString** - Gets rid of the `$filter=()… $orderby=…` parts of a query and keeps only what's between the most
* exterior parentheses of the $filter.
* EXAMPLE: takes a query of the form
* `$filter=(name eq 'RaspPi*') $orderby=name asc`
* and turns it into
* `name eq 'RaspPi*'`
* This is necessary for searching for smart groups, which are identified by their own query
* that needs to be passed through.
*
* Note: if you want to specify the order, you need to wrap your filters within `__filter` property and then add `__orderby` with the array of field paths and sort directions (1 for ascending, -1 for descending), for example:
* - `{ __filter: { ... }, __orderby: [{ 'creationTime': -1 }, { 'name': 1 }] }`
*
* @returns {string} Returns a query string ready to be sent in request params to backend.
*
* **Example**
* ```typescript
* const query = {
* __filter: {
* 'name': 'My Device*',
* 'c8y_Availability.status': {
* __in: ['AVAILABLE', 'UNAVAILABLE']
* },
* 'creationTime': {
* __lt: '2015-11-30T13:28:123Z'
* },
* 'c8y_ActiveAlarmsStatus.critical': {
* __gt: 0
* },
* __or: [
* {__not: {__has: 'c8y_ActiveAlarmsStatus.major'}},
* {
* __or: [
* {__bygroupid: 10300},
* {__bygroupid: 10400}
* ]
* }
* ]
* },
* __orderby: [
* {'name': 1},
* {'creationTime': -1},
* {'c8y_ActiveAlarmsStatus.critical': -1}
* ]
* };
*
* const params = {
* query: queriesUtil.buildQuery(query)
* };
* ```
*/
buildQuery(query) {
const q = [];
const queryObjectRoot = query.__filter || query;
const filter = this.buildQueryFilter(queryObjectRoot);
const orderBy = this.buildQueryOrderby(query.__orderby);
if (filter) {
q.push(`$filter=(${filter})`);
}
if (orderBy) {
q.push(`$orderby=${orderBy}`);
}
return q.join(' ');
}
buildQueryFilter(queryFilter, _queryKey, _glueType) {
const queryKey = _queryKey || null;
const glueType = _glueType || 'and';
const q = new Array();
if (Array.isArray(queryFilter)) {
for (const qFilter of queryFilter) {
const _q = this.buildQueryFilter(qFilter, null, glueType);
if (_q) {
q.push(_q);
}
}
}
else {
for (const k of Object.keys(queryFilter)) {
if (this.operatorFns[k] !== undefined) {
const _q = this.operatorFns[k](queryFilter[k], queryKey);
if (_q) {
q.push(_q);
}
}
else {
const _q = this.operatorFns.__eq(queryFilter[k], k);
if (_q) {
q.push(_q);
}
}
}
}
return this.glue(q, glueType);
}
buildQueryOrderby(queryOrderbys) {
const o = [];
if (queryOrderbys) {
for (const q of queryOrderbys) {
for (const k of Object.keys(q)) {
if (q[k] !== 0) {
o.push(`${k} ${q[k] > 0 ? 'asc' : 'desc'}`);
}
}
}
}
return o.join(',');
}
addAndFilter(query, filter) {
return this.addFilter(query, filter, 'and');
}
addOrFilter(query, filter) {
return this.addFilter(query, filter, 'or');
}
addFilter(query, filter, operator) {
const oldFilter = query.__orderby
? (query.__filter || {})
: (query.__filter || query);
const newFilter = { [`__${operator}`]: this.skipEmptyObjects([oldFilter, filter]) };
if (!query.__filter && !query.__orderby) {
return newFilter;
}
query.__filter = newFilter;
return query;
}
prependOrderbys(query, orderbys) {
return this.addOrderbys(query, orderbys, 'prepend');
}
appendOrderbys(query, orderbys) {
return this.addOrderbys(query, orderbys, 'append');
}
addOrderbys(query, orderbys, how) {
const oldFilter = query.__orderby
? (query.__filter || {})
: (query.__filter || query);
const oldOrderbys = query.__orderby || [];
const newOrderbys = how === 'prepend' ? [...orderbys, ...oldOrderbys] : [...oldOrderbys, ...orderbys];
const newQuery = {
__orderby: this.skipEmptyObjects(newOrderbys)
};
if (!this.isEmptyObject(oldFilter)) {
newQuery.__filter = oldFilter;
}
return newQuery;
}
extractAndMergeOrderBys(queries) {
if ((queries === null || queries === void 0 ? void 0 : queries.length) > 0) {
const orderByQuery = queries
.map(query => {
const token = '$orderby=';
const tokenIndex = query.lastIndexOf(token);
return tokenIndex !== -1 ? query.substring(tokenIndex + token.length) : null;
})
.filter(orderBy => orderBy !== null)
.join(',');
return orderByQuery ? `$orderby=${orderByQuery}` : '';
}
}
glue(statements, type) {
return statements.length > 1 ? `(${statements.join(`) ${type} (`)})` : statements[0];
}
useAsFloat(operand) {
operand = this.quoteString(operand);
if (typeof operand === 'number') {
return `${operand}f`;
}
return operand;
}
quoteString(s) {
if (typeof s === 'string') {
return `'${this.escapeString(s)}'`;
}
return s;
}
skipEmptyObjects(objs) {
return objs.filter(obj => !this.isEmptyObject(obj));
}
isEmptyObject(obj) {
return Object.keys(obj).length === 0;
}
/**
* Escapes a string to be used in OData query.
*
* - OData does not support single quotes in the query. We need to replace all single quotes with double quotes.
* - OData uses parentheses for grouping expressions and functions. We need to escape any parentheses in the string.
* - OData uses backslash as an escape character. We need to escape any backslashes in the string.
*
* Spec: http://docs.oasis-open.org/odata/odata/v4.01/cs01/part2-url-conventions/odata-v4.01-cs01-part2-url-conventions.html#sec_URLComponents
*
* @param s String to be escaped.
* @returns Escaped string.
*/
escapeString(s) {
return s
.replace(/'/g, "''")
.replace(/\\/g, '\\\\')
.replace(/(\[|\{|\(|\]|\}|\))/g, '\\$1');
}
}
//# sourceMappingURL=QueriesUtil.js.map