Skip to content

Commit

Permalink
feat(asset): Added calibration forecast query builder (#69)
Browse files Browse the repository at this point in the history
Co-authored-by: Robert Aradei <sorin.robert.aradei@ni.com>
  • Loading branch information
saradei-ni and Robert Aradei authored Oct 2, 2024
1 parent dc17252 commit 99f2fff
Show file tree
Hide file tree
Showing 16 changed files with 853 additions and 442 deletions.
197 changes: 197 additions & 0 deletions src/core/query-builder.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import { QueryBuilderCustomOperation } from "smart-webcomponents-react";

export const queryBuilderMessages = {
en: {
propertyUnknownType: "'' property is with undefined 'type' member!",
propertyInvalidValue: "Invalid '!",
propertyInvalidValueType: "Invalid '!",
elementNotInDOM: 'Element does not exist in DOM! Please, add the element to the DOM, before invoking a method.',
moduleUndefined: 'Module is undefined.',
missingReference: '.',
htmlTemplateNotSuported: ": Browser doesn't support HTMLTemplate elements.",
invalidTemplate: "' property accepts a string that must match the id of an HTMLTemplate element from the DOM.",
add: 'Add',
addCondition: 'Add Condition',
addGroup: 'Add Group',
and: 'And',
notand: 'Not And',
or: 'Or',
notor: 'Not Or',
'=': 'Equals',
'<>': 'Does not equal',
'>': 'Greater than',
'>=': 'Greater than or equal to',
'<': 'Less than',
'<=': 'Less than or equal to',
startswith: 'Starts with',
endswith: 'Ends with',
contains: 'Contains',
notcontains: 'Does not contain',
isblank: 'Is blank',
isnotblank: 'Is not blank',
wrongParentGroupIndex: "' method.",
missingFields:
': Fields are required for proper condition\'s adding. Set "fields" source and then conditions will be added as expected.',
wrongElementNode: "' method.",
invalidDataStructure: ': Used invalid data structure in updateCondition/updateGroup method.',
dateTabLabel: 'DATE',
timeTabLabel: 'TIME',
queryLabel: '',
},
};

export const QueryBuilderOperations = {
EQUALS: {
label: 'Equals',
name: '=',
expressionTemplate: '{0} = "{1}"',
},
DOES_NOT_EQUAL: {
label: 'Does not equal',
name: '<>',
expressionTemplate: '{0} != "{1}"',
},
STARTS_WITH: {
label: 'Starts with',
name: 'startswith',
expressionTemplate: '{0}.StartsWith("{1}")',
},
ENDS_WITH: {
label: 'Ends with',
name: 'endswith',
expressionTemplate: '{0}.EndsWith("{1}")',
},
CONTAINS: {
label: 'Contains',
name: 'contains',
expressionTemplate: '{0}.Contains("{1}")',
},
DOES_NOT_CONTAIN: {
label: 'Does not contain',
name: 'notcontains',
expressionTemplate: '!({0}.Contains("{1}"))',
},
IS_BLANK: {
label: 'Is blank',
name: 'isblank',
expressionTemplate: 'string.IsNullOrEmpty({0})',
hideValue: true,
},
IS_NOT_BLANK: {
label: 'Is not blank',
name: 'isnotblank',
expressionTemplate: '!string.IsNullOrEmpty({0})',
hideValue: true,
},
GREATER_THAN: {
label: 'Greater than',
name: '>',
expressionTemplate: '{0} > "{1}"',
},
GREATER_THAN_OR_EQUAL_TO: {
label: 'Greater than or equal to',
name: '>=',
expressionTemplate: '{0} >= "{1}"',
},
LESS_THAN: {
label: 'Less than',
name: '<',
expressionTemplate: '{0} < "{1}"',
},
LESS_THAN_OR_EQUAL_TO: {
label: 'Less than or equal to',
name: '<=',
expressionTemplate: '{0} <= "{1}"',
},
// List expressions
LIST_EQUALS: {
label: 'Equals',
name: 'listequals',
expressionTemplate: '{0}.Contains("{1}")',
},
LIST_DOES_NOT_EQUAL: {
label: 'Does not equal',
name: 'listnotequals',
expressionTemplate: '!({0}.Contains("{1}"))',
},
LIST_CONTAINS: {
label: 'Contains',
name: 'listcontains',
expressionTemplate: '{0}.Any(it.Contains("{1}"))',
},
LIST_DOES_NOT_CONTAIN: {
label: 'Does not contain',
name: 'listnotcontains',
expressionTemplate: '{0}.Any(!it.Contains("{1}"))',
},
// Properties expressions
PROPERTY_EQUALS: {
label: 'Equals',
name: 'propertyequals',
expressionTemplate: 'properties["{0}"] = "{1}"',
},
PROPERTY_DOES_NOT_EQUAL: {
label: 'Does not equal',
name: 'propertynotequals',
expressionTemplate: 'properties["{0}"] != "{1}"',
},
PROPERTY_STARTS_WITH: {
label: 'Starts with',
name: 'propertystartswith',
expressionTemplate: 'properties["{0}"].StartsWith("{1}")',
},
PROPERTY_ENDS_WITH: {
label: 'Ends with',
name: 'propertyendswith',
expressionTemplate: 'properties["{0}"].EndsWith("{1}")',
},
PROPERTY_CONTAINS: {
label: 'Contains',
name: 'propertycontains',
expressionTemplate: 'properties["{0}"].Contains("{1}")',
},
PROPERTY_DOES_NOT_CONTAIN: {
label: 'Does not contains',
name: 'propertynotcontains',
expressionTemplate: '!(properties["{0}"].Contains("{1}"))',
},
PROPERTY_IS_BLANK: {
label: 'Is blank',
name: 'propertyisblank',
expressionTemplate: 'string.IsNullOrEmpty(properties["{0}"])',
hideValue: true,
},
PROPERTY_IS_NOT_BLANK: {
label: 'Is not blank',
name: 'propertyisnotblank',
expressionTemplate: '!string.IsNullOrEmpty(properties["{0}"])',
hideValue: true,
},
}

export const customOperations: QueryBuilderCustomOperation[] = [
QueryBuilderOperations.EQUALS,
QueryBuilderOperations.DOES_NOT_EQUAL,
QueryBuilderOperations.STARTS_WITH,
QueryBuilderOperations.ENDS_WITH,
QueryBuilderOperations.CONTAINS,
QueryBuilderOperations.DOES_NOT_CONTAIN,
QueryBuilderOperations.IS_BLANK,
QueryBuilderOperations.IS_NOT_BLANK,
QueryBuilderOperations.GREATER_THAN,
QueryBuilderOperations.GREATER_THAN_OR_EQUAL_TO,
QueryBuilderOperations.LESS_THAN,
QueryBuilderOperations.LESS_THAN_OR_EQUAL_TO,
QueryBuilderOperations.LIST_EQUALS,
QueryBuilderOperations.LIST_DOES_NOT_EQUAL,
QueryBuilderOperations.LIST_CONTAINS,
QueryBuilderOperations.LIST_DOES_NOT_CONTAIN,
QueryBuilderOperations.PROPERTY_EQUALS,
QueryBuilderOperations.PROPERTY_DOES_NOT_EQUAL,
QueryBuilderOperations.PROPERTY_STARTS_WITH,
QueryBuilderOperations.PROPERTY_ENDS_WITH,
QueryBuilderOperations.PROPERTY_CONTAINS,
QueryBuilderOperations.PROPERTY_DOES_NOT_CONTAIN,
QueryBuilderOperations.PROPERTY_IS_BLANK,
QueryBuilderOperations.PROPERTY_IS_NOT_BLANK,
];
100 changes: 100 additions & 0 deletions src/core/query-builder.utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { expressionBuilderCallback, expressionReaderCallback, transformComputedFieldsQuery } from "./query-builder.utils"

describe('QueryBuilderUtils', () => {
describe('transformComputedFieldsQuery', () => {
const computedDataFields = {
Object1: '(object1.prop1 = {value} || object1.prop2 = {value})',
Object2: '(object2.prop1 = {value} || object2.extra.prop2 = {value} || object2.prop3 = {value} || object2.prop4 = {value})',
Object3: '(object3.prop1 = {value} || object3.prop2 = {value} || object3.prop3 = {value})'
};

it('should transform a query with computed fields', () => {
const query = 'Object1 = "value1" AND Object2 = "value2"';
const result = transformComputedFieldsQuery(query, computedDataFields);
expect(result).toBe('(object1.prop1 = value1 || object1.prop2 = value1) AND (object2.prop1 = value2 || object2.extra.prop2 = value2 || object2.prop3 = value2 || object2.prop4 = value2)');
});

it('should return the original query if no computed fields are present', () => {
const query = 'field1 = "value1" AND field2 = "value2"';
const result = transformComputedFieldsQuery(query, computedDataFields);
expect(result).toBe(query);
});

it('should handle multiple computed fields correctly', () => {
const query = 'Object1 = "value1" AND Object3 = "value3"';
const result = transformComputedFieldsQuery(query, computedDataFields);
expect(result).toBe('(object1.prop1 = value1 || object1.prop2 = value1) AND (object3.prop1 = value3 || object3.prop2 = value3 || object3.prop3 = value3)');
});

it('should handle an empty query', () => {
const query = '';
const result = transformComputedFieldsQuery(query, computedDataFields);
expect(result).toBe(query);
});
});

describe('expressionBuilderCallback', () => {
const mockQueryBuilderCustomOperation = {
expressionTemplate: '{0} = {1}'
};

it('should build a valid expression for a single field', () => {
const options = {
'field1': [{ label: 'Option A', value: 'ValueA' }],
};

const result = expressionBuilderCallback(options).call(mockQueryBuilderCustomOperation, 'field1', 'someOperation', 'Option A');

expect(result).toBe('field1 = ValueA');
});

it('should return original value if no matching label found', () => {
const options = {
'field1': [{ label: 'Option A', value: 'ValueA' }],
};

const callback = expressionBuilderCallback(options).bind(mockQueryBuilderCustomOperation);
const result = callback('field1', 'someOperation', 'Option B');

expect(result).toBe('field1 = Option B');
});

it('should return original expression if no options are provided', () => {
const options = {};

const callback = expressionBuilderCallback(options).bind(mockQueryBuilderCustomOperation);
const result = callback('field1', 'someOperation', 'Any Value');

expect(result).toBe('field1 = Any Value');
});
})

describe('expressionReaderCallback', () => {
const options = {
'optionsObject1': [{ label: 'Label A', value: 'ValueA' }],
'optionsObject2': [{ label: 'Label B', value: 'ValueB' }],
};

it('should map value to label for a given field', () => {
const callback = expressionReaderCallback(options);
const result = callback('someExpression', ['optionsObject1', 'ValueA']);

expect(result).toEqual({ fieldName: 'optionsObject1', value: 'Label A' });
});

it('should return original field name and value if no matching label is found', () => {
const callback = expressionReaderCallback(options);
const result = callback('someExpression', ['field1', 'NonExistentValue']);

expect(result).toEqual({ fieldName: 'field1', value: 'NonExistentValue' });
});

it('should return original field name and value if no options are provided for the field', () => {
const emptyOptions = {};
const callback = expressionReaderCallback(emptyOptions);
const result = callback('someExpression', ['field1', 'ValueA']);

expect(result).toEqual({ fieldName: 'field1', value: 'ValueA' });
});
})
})
64 changes: 64 additions & 0 deletions src/core/query-builder.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { QueryBuilderCustomOperation } from "smart-webcomponents-react";
import { QueryBuilderOption } from "./types";

/**
* The function will replace the computed fields with their transformation
* Example: object = "value" => object1.prop1 = "value" || object1.prop2 = "value"
* @param query Query builder provided string
* @param computedDataFields Object with computed fields and their transformations
* @returns Updated query with computed fields transformed
*/
export function transformComputedFieldsQuery(query: string, computedDataFields: Record<string, string>) {
for (const [field, transformation] of Object.entries(computedDataFields)) {
const regex = new RegExp(`\\b${field}\\s*=\\s*"([^"]*)"`, 'g');
query = query.replace(regex, (_match, value) => transformation.replace(/{value}/g, value));
}

return query;
}

/**
* The callback will replace the option's label with it's value
* @param options Object with value, label object properties, that hold the dropdown values
* @returns callback to be used by query builder when building the query
*/
export function expressionBuilderCallback(options: Record<string, QueryBuilderOption[]>) {
return function (this: QueryBuilderCustomOperation, fieldName: string, _operation: string, value: string) {
const buildExpression = (field: string, value: string) => {
const fieldOptions = options[fieldName];
if (fieldOptions?.length) {
const labelValue = fieldOptions.find(option => option.label === value);

if (labelValue) {
value = labelValue.value;
}
}

return this.expressionTemplate?.replace('{0}', field).replace('{1}', value);
};

return buildExpression(fieldName, value);
};
}

/**
* The callback will replace the option's value with it's label
* @param options Object with value, label object properties, that hold the dropdown values
* @returns callback to be used by query builder when reading the query
*/
export function expressionReaderCallback(options: Record<string, QueryBuilderOption[]>) {
return function (_expression: string, [fieldName, value]: string[]) {
const fieldOptions = options[fieldName];

if (fieldOptions?.length) {
const valueLabel = fieldOptions.find(option => option.value === value);

if (valueLabel) {
value = valueLabel.label;
}
}

return { fieldName, value };
}
};

5 changes: 5 additions & 0 deletions src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,8 @@ export interface SystemLinkError {
name: string;
}
}

export interface QueryBuilderOption {
label: string;
value: string;
}
Loading

0 comments on commit 99f2fff

Please sign in to comment.