Skip to content

Commit

Permalink
feat(asset): Added calibration forecast query builder
Browse files Browse the repository at this point in the history
  • Loading branch information
Robert Aradei committed Sep 26, 2024
1 parent 162d782 commit 41d7f38
Show file tree
Hide file tree
Showing 15 changed files with 884 additions and 439 deletions.
201 changes: 201 additions & 0 deletions src/core/query-builder.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
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,
];

export enum ComputedDataFields {
Location = 'Location.MinionId || Location.PhysicalLocation'
}
116 changes: 116 additions & 0 deletions src/core/query-builder.utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { expressionBuilderCallback, expressionReaderCallback, setQueryComputedFields } from "./query-builder.utils"

jest.mock('./query-builder.constants.ts', () => ({
...jest.requireActual('./query-builder.constants'),
ComputedDataFields: {
Object1: 'object1.prop1 || object1.prop2',
Object2: 'object2.prop1 || object2.extra.prop2 || object2.prop3 || object2.prop4 || object2.prop5 || object2.prop6',
Object3: 'object3.prop1 || object3.prop2 || object3.prop3'
}
}));

describe('QueryBuilderUtils', () => {
describe('setQueryComputedFields', () => {
it('should not modify the query if there are no computed fields', () => {
const query = 'field1 = "value1" AND field2 = "value2"';
const result = setQueryComputedFields(query);
expect(result).toBe(query);
});

it('should keep only first query of a computed field', () => {
const query1 = 'object1.prop1 = "value" || object1.prop2 = "value"';
const query2 = 'object2.prop1 = "value" || object2.extra.prop2 = "value" || object2.prop3 = "value" || object2.prop4 = "value" || object2.prop5 = "value" || object2.prop6 = "value"';
const query3 = 'object3.prop1 = "value" || object3.prop2 = "value" || object3.prop3 = "value"';

expect(setQueryComputedFields(query1)).toBe('object1.prop1 = "value"');
expect(setQueryComputedFields(query2)).toBe('object2.prop1 = "value"');
expect(setQueryComputedFields(query3)).toBe('object3.prop1 = "value"');

});

it('should keep the right query in computed queries', () => {
const query4 = 'object4.prop1 = "val" && (object1.prop1 = "value" || object1.prop2 = "value")';
const query5 = '(object1.prop1 = "value" || object1.prop2 = "value") && object5.prop1 = "val" || object3.prop1 = "value" || object3.prop2 = "value" || object3.prop3 = "value"';

expect(setQueryComputedFields(query4)).toBe('object4.prop1 = "val" && (object1.prop1 = "value")');
expect(setQueryComputedFields(query5)).toBe('(object1.prop1 = "value") && object5.prop1 = "val" || object3.prop1 = "value"');
});

it('should return undefined if the query is undefined', () => {
const result = setQueryComputedFields(undefined);
expect(result).toBeUndefined();
});
})

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 replace field name with computed field name if applicable', () => {
const callback = expressionReaderCallback(options);
const result = callback('someExpression', ['object1.prop1', 'ValueA']);

expect(result).toEqual({ fieldName: 'object1.prop1 || object1.prop2', value: 'ValueA' });
});

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' });
});
})
})
77 changes: 77 additions & 0 deletions src/core/query-builder.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { QueryBuilderCustomOperation } from "smart-webcomponents-react";
import { QueryBuilderOption } from "./types";
import { ComputedDataFields } from "./query-builder.constants";

/**
* Removes extra queries from computed fields so the query builder will read only one
* Ex: Location.MinionId = "value" || Location.PhysicalLocation = "value" will become Location.MinionId = "value"
* @param query actual query that has been sent
* @returns query that the query builder can read
*/
export function setQueryComputedFields(query?: string) {
const propertiesToExclude = Object.values(ComputedDataFields)
.flatMap(field => field.split(' || ').slice(1))
.join('|');

const operations = '(=|!=)'; // Update if additional operations need support
const computedDataFieldsRegex = new RegExp(`\\s\\|\\| (${propertiesToExclude}) ${operations} *"[^"]*"`, "g");

return query?.replace(computedDataFieldsRegex, '');
}

/**
* 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);
};

if (fieldName.includes('||')) {
return `(${fieldName.split(' || ').map(prop => buildExpression(prop, value)).join(' || ')})`;
}

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[]) {
for (const computedFieldName of Object.values(ComputedDataFields)) {
if (computedFieldName.includes(fieldName)) {
fieldName = computedFieldName;
break;
}
}

const fieldOptions = options[fieldName];

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

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

return { fieldName, value };
}
};

Loading

0 comments on commit 41d7f38

Please sign in to comment.