Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(asset): Added calibration forecast query builder #69

Merged
merged 1 commit into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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}"',
saradei-ni marked this conversation as resolved.
Show resolved Hide resolved
},
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: {
saradei-ni marked this conversation as resolved.
Show resolved Hide resolved
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}"',
saradei-ni marked this conversation as resolved.
Show resolved Hide resolved
},
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);

saradei-ni marked this conversation as resolved.
Show resolved Hide resolved
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
Loading