Permalink
Browse files

feat(v2): add support for nested restrictions with boolean operators

  • Loading branch information...
j-white committed Jun 22, 2017
1 parent eb1b165 commit 09cd705a82a4d78f70073c6214ec047d477dbc05
View
@@ -23,17 +23,18 @@ import {
} from 'dist/opennms.min.js';
new Client().connect('Demo', 'https://demo.opennms.org/opennms', 'demo', 'demo').then((client) => {
const idRestriction = new Restriction('id', Comparators.GE, 1);
const filter = new Filter(idRestriction);
const filter = new Filter()
.withOrRestriction(new Restriction('id', Comparators.GE, 1));
// query all alarms with an ID greater than or equal to 1
return client.alarms().find(filter).then((alarms) => {
console.log('got ' + alarms.length + ' alarms.');
// return all the node IDs associated with the matching alarms
return alarms.map((alarm) => {
// return all the (unique) node IDs associated with the matching alarms
const allNodeIds = alarms.map((alarm) => {
return alarm.nodeId;
}).filter((nodeId) => {
return nodeId !== undefined;
});
return allNodeIds.filter((v,i,a)=>allNodeIds.indexOf(v)===i);
}).then((nodeIds) => {
// for each node ID, request the node info
return Promise.all(nodeIds.map((nodeId) => {
View
@@ -1,5 +1,6 @@
import {Comparator, Comparators} from './api/Comparator';
import {Filter} from './api/Filter';
import {NestedRestriction} from './api/NestedRestriction';
import {OnmsAuthConfig} from './api/OnmsAuthConfig';
import {OnmsError} from './api/OnmsError';
import {OnmsHTTPOptions} from './api/OnmsHTTPOptions';
@@ -53,6 +54,7 @@ const API = Object.freeze({
Comparator,
Comparators,
Filter,
NestedRestriction,
OnmsAuthConfig,
OnmsError,
OnmsHTTPOptions,
View
@@ -202,7 +202,7 @@ function CLI() {
if (API.Comparators.hasOwnProperty(type)) {
const comp = API.Comparators[type];
if (comp.matches(comparator)) {
filter.restrictions.push(new API.Restriction(attribute, comp, value));
filter.withOrRestriction(new API.Restriction(attribute, comp, value));
}
}
}
View
@@ -0,0 +1,21 @@
import {Operator} from './Operator';
import {Restriction} from './Restriction';
import {NestedRestriction} from './NestedRestriction';
/**
* A restriction and boolean operator pair.
* @module Clause
*/ /** */
export class Clause {
/** the associated restriction */
public restriction: Restriction|NestedRestriction;
/** the boolean operator */
public operator: Operator;
constructor(restriction: Restriction|NestedRestriction, operator: Operator) {
this.restriction = restriction;
this.operator = operator;
}
}
View
@@ -1,20 +1,14 @@
import {Restriction} from './Restriction';
import {NestedRestriction} from './NestedRestriction';
/**
* A query filter for DAOs.
* @module Filter
* @param T the model type (OnmsAlarm, OnmsEvent, etc.)
*/ /** */
export class Filter {
export class Filter extends NestedRestriction {
/** how many results to get back by default */
public limit = 1000;
/** TODO: add (multiple) orderBy/order support */
/** the query restrictions to use when making requests */
public restrictions = [] as Restriction[];
constructor(...restrictions: Restriction[]) {
this.restrictions = restrictions;
}
}
@@ -0,0 +1,33 @@
import {Operators} from './Operator';
import {Clause} from './Clause';
import {Restriction} from './Restriction';
/**
* Nested query restrictions.
* @module NestedRestriction
*/
export class NestedRestriction {
/** the clauses containing the nested restrictions and their logical operators */
public clauses = [] as Clause[];
constructor(...clauses: Clause[]) {
this.clauses = clauses;
}
/** adds an additional restriction using the logical OR operator */
public withOrRestriction(restriction: Restriction|NestedRestriction) {
return this.withClause(new Clause(restriction, Operators.OR));
}
/** adds an additional restriction using the logical AND operator */
public withAndRestriction(restriction: Restriction|NestedRestriction) {
return this.withClause(new Clause(restriction, Operators.AND));
}
/** adds an additional clause */
private withClause(clause: Clause) {
this.clauses.push(clause);
return this;
}
}
View
@@ -0,0 +1,29 @@
import {OnmsEnum} from '../internal/OnmsEnum';
/**
* Represents a filter comparator.
* @module Comparator
*/ /** */
export class Operator extends OnmsEnum<number> {
/** aliases for the command-line */
private aliases = [] as string[];
constructor(id: number, label: string, ...aliases: string[]) {
super(id, label);
this.aliases = aliases;
}
/** whether this comparator matches the given comparator string */
public matches(comparator: string) {
return (comparator.toLowerCase() === this.label.toLowerCase())
|| this.aliases.indexOf(comparator) >= 0;
}
}
/* tslint:disable:object-literal-sort-keys */
/** @hidden */
export const Operators = Object.freeze({
AND: new Operator(1, 'AND'),
OR: new Operator(2, 'OR'),
});
@@ -5,7 +5,10 @@ import {IFilterProcessor} from '../api/IFilterProcessor';
import {Filter} from '../api/Filter';
import {Comparators} from '../api/Comparator';
import {Operators} from '../api/Operator';
import {OnmsError} from '../api/OnmsError';
import {Restriction} from '../api/Restriction';
import {NestedRestriction} from '../api/NestedRestriction';
/** @hidden */
const nonExclusiveComparators = [
@@ -31,7 +34,16 @@ export class V1FilterProcessor implements IFilterProcessor {
ret.limit = '' + filter.limit;
}
for (const restriction of filter.restrictions) {
for (const clause of filter.clauses) {
if (clause.operator !== Operators.OR) {
throw new OnmsError('V1 only supports OR operators!');
}
if (clause.restriction instanceof NestedRestriction) {
throw new OnmsError('V1 does not support nested restrictions!');
}
const restriction = clause.restriction as Restriction;
switch (restriction.comparator) {
case Comparators.NULL: {
ret[restriction.attribute] = 'null';
@@ -5,7 +5,10 @@ import {IFilterProcessor} from '../api/IFilterProcessor';
import {Filter} from '../api/Filter';
import {Comparator, Comparators} from '../api/Comparator';
import {Restriction} from '../api/Restriction';
import {NestedRestriction} from '../api/NestedRestriction';
import {OnmsError} from '../api/OnmsError';
import {Operator, Operators} from '../api/Operator';
import {Clause} from '../api/Clause';
/**
* OpenNMS V2 ReST filter processor
@@ -26,7 +29,7 @@ export class V2FilterProcessor implements IFilterProcessor {
* given a comparator, convert it to a correspond comparator
* that can be used in the FIQL expression
*/
private static getFIQLComparator(comparator: Comparator) {
private static toFIQLComparator(comparator: Comparator) {
switch (comparator) {
case Comparators.EQ:
case Comparators.NULL:
@@ -50,10 +53,8 @@ export class V2FilterProcessor implements IFilterProcessor {
}
}
/**
* given a restriction, compute the value to use in the FIQL expression
*/
private static getFIQLValue(restriction: Restriction) {
/** given a restriction, compute the value to use in the FIQL expression */
private static toFIQLValue(restriction: Restriction) {
switch (restriction.comparator) {
case Comparators.NULL:
case Comparators.NOTNULL:
@@ -65,6 +66,38 @@ export class V2FilterProcessor implements IFilterProcessor {
}
}
/** given an operator, convert it to the corresponding FIQL operator */
private static toFIQLOperator(operator: Operator) {
switch (operator) {
case Operators.AND:
return ';';
case Operators.OR:
return ',';
default:
throw new OnmsError('Unsupported operator type: ' + operator);
}
}
/** given a list of clauses, recursively generate the FIQL query string */
private static toFIQL(clauses: Clause[]) {
let search = '';
for (const clause of clauses) {
if (search.length > 0) {
search += V2FilterProcessor.toFIQLOperator(clause.operator);
}
if (clause.restriction instanceof NestedRestriction) {
search += '(' + V2FilterProcessor.toFIQL(clause.restriction.clauses) + ')';
} else {
const restriction = clause.restriction as Restriction;
const comp = V2FilterProcessor.toFIQLComparator(restriction.comparator);
const value = V2FilterProcessor.toFIQLValue(restriction);
search += [restriction.attribute, comp, value].join('');
}
}
return search;
}
/** given a filter, return a hash of URL parameters */
public getParameters(filter: Filter) {
const ret = {} as IHash<string>;
@@ -73,15 +106,9 @@ export class V2FilterProcessor implements IFilterProcessor {
ret.limit = '' + filter.limit;
}
const terms = [];
for (const restriction of filter.restrictions) {
const comp = V2FilterProcessor.getFIQLComparator(restriction.comparator);
const value = V2FilterProcessor.getFIQLValue(restriction);
terms.push([restriction.attribute, comp, value].join(''));
}
if (terms.length > 0) {
ret.search = terms.join(';');
const search = V2FilterProcessor.toFIQL(filter.clauses);
if (search.length > 0) {
ret._s = search;
}
return ret;
@@ -44,7 +44,7 @@ describe('AlarmDAO', () => {
});
it('AlarmDAO.find(id=404725)', () => {
const filter = new Filter();
filter.restrictions.push(new Restriction('id', Comparators.EQ, 404725));
filter.withOrRestriction(new Restriction('id', Comparators.EQ, 404725));
return dao.find(filter).then((alarms) => {
expect(alarms.length).toEqual(1);
});
View
@@ -87,7 +87,7 @@ describe('NodeDAO', () => {
});
it('NodeDAO.find(id=43)', () => {
const filter = new Filter();
filter.restrictions.push(new Restriction('id', Comparators.EQ, 43));
filter.withOrRestriction(new Restriction('id', Comparators.EQ, 43));
return dao.find(filter).then((nodes) => {
expect(nodes.length).toEqual(1);
});
@@ -5,8 +5,6 @@ import {LogLevel} from 'typescript-logging';
setLogLevel(LogLevel.Debug, catRoot);
import {IFilterProcessor} from '../../src/api/IFilterProcessor';
import {OnmsError} from '../../src/api/OnmsError';
import {Comparator, Comparators} from '../../src/api/Comparator';
import {Filter} from '../../src/api/Filter';
@@ -34,16 +32,16 @@ describe('V1FilterProcessor', () => {
});
it('alarm filter: id=notnull', () => {
const filter = new Filter<OnmsAlarm>();
filter.restrictions.push(new Restriction('id', Comparators.NOTNULL));
filter.withOrRestriction(new Restriction('id', Comparators.NOTNULL));
const proc = new V1FilterProcessor();
const params = proc.getParameters(filter);
expect(Object.keys(params).length).toEqual(2);
expect(params.id).toEqual('notnull');
});
it('alarm filter: id=notnull, ackTime=null', () => {
const filter = new Filter<OnmsAlarm>();
filter.restrictions.push(new Restriction('id', Comparators.NOTNULL));
filter.restrictions.push(new Restriction('ackTime', Comparators.NULL));
filter.withOrRestriction(new Restriction('id', Comparators.NOTNULL));
filter.withOrRestriction(new Restriction('ackTime', Comparators.NULL));
const proc = new V1FilterProcessor();
const params = proc.getParameters(filter);
expect(Object.keys(params).length).toEqual(3);
@@ -52,8 +50,8 @@ describe('V1FilterProcessor', () => {
});
it('alarm filter: id=notnull, severity="MINOR"', () => {
const filter = new Filter<OnmsAlarm>();
filter.restrictions.push(new Restriction('id', Comparators.NOTNULL));
filter.restrictions.push(new Restriction('severity', Comparators.EQ, 'MINOR'));
filter.withOrRestriction(new Restriction('id', Comparators.NOTNULL));
filter.withOrRestriction(new Restriction('severity', Comparators.EQ, 'MINOR'));
const proc = new V1FilterProcessor();
const params = proc.getParameters(filter);
expect(Object.keys(params).length).toEqual(4);
@@ -63,8 +61,8 @@ describe('V1FilterProcessor', () => {
});
it('alarm filter: id=notnull, severity=OnmsSeverity.MINOR', () => {
const filter = new Filter<OnmsAlarm>();
filter.restrictions.push(new Restriction('id', Comparators.NOTNULL));
filter.restrictions.push(new Restriction('severity', Comparators.EQ, Severities.MINOR));
filter.withOrRestriction(new Restriction('id', Comparators.NOTNULL));
filter.withOrRestriction(new Restriction('severity', Comparators.EQ, Severities.MINOR));
const proc = new V1FilterProcessor();
const params = proc.getParameters(filter);
expect(Object.keys(params).length).toEqual(4);
@@ -74,8 +72,8 @@ describe('V1FilterProcessor', () => {
});
it('alarm filter: severity=OnmsSeverity.MINOR, id!=0', () => {
const filter = new Filter<OnmsAlarm>();
filter.restrictions.push(new Restriction('severity', Comparators.EQ, Severities.MINOR));
filter.restrictions.push(new Restriction('id', Comparators.NE, 0));
filter.withOrRestriction(new Restriction('severity', Comparators.EQ, Severities.MINOR));
filter.withOrRestriction(new Restriction('id', Comparators.NE, 0));
const proc = new V1FilterProcessor();
expect(() => {
proc.getParameters(filter);
Oops, something went wrong.

0 comments on commit 09cd705

Please sign in to comment.