Skip to content

Commit

Permalink
Merge pull request #61 from bcgov/dev
Browse files Browse the repository at this point in the history
Release to Prod
  • Loading branch information
brysonjbest authored Dec 20, 2024
2 parents 48e2abc + 7b9a7a0 commit e8b30a1
Show file tree
Hide file tree
Showing 8 changed files with 152 additions and 54 deletions.
6 changes: 6 additions & 0 deletions .github/workflows/build-and-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Install OpenShift CLI
run: |
curl -LO https://mirror.openshift.com/pub/openshift-v4/clients/oc/latest/linux/oc.tar.gz
tar -xvf oc.tar.gz
sudo mv oc /usr/local/bin
- name: Authenticate and set context for OpenShift
uses: redhat-actions/oc-login@v1

Expand Down
28 changes: 21 additions & 7 deletions src/api/decisions/validations/validations.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,20 +182,25 @@ export class ValidationService {

private validateDateCriteria(field: any, input: string | string[]): void {
const { validationCriteria, validationType } = field;
const parseValue = (value: string) => {
const cleanValue = value?.trim()?.replace(/[\[\]()]/g, '');
return cleanValue?.toLowerCase() === 'today' ? new Date() : new Date(cleanValue);
};

const dateValues = validationCriteria
? validationCriteria
.replace(/[\[\]]/g, '')
.split(',')
.map((val: string) => new Date(val.trim()).getTime())
.map((val: string) => parseValue(val).getTime())
: [];

const dateValidationValue = new Date(validationCriteria).getTime() || new Date().getTime();
const dateValidationValue = parseValue(validationCriteria).getTime();
const [minDate, maxDate] =
dateValues.length > 1
? [dateValues[0], dateValues[dateValues.length - 1]]
: [new Date().getTime(), new Date().setFullYear(new Date().getFullYear() + 1)];

const dateInput = typeof input === 'string' ? new Date(input).getTime() : null;
const dateInput = typeof input === 'string' ? parseValue(input).getTime() : null;

switch (validationType) {
case '==':
Expand Down Expand Up @@ -269,6 +274,13 @@ export class ValidationService {
}
}

private normalizeText(text: string): string {
return text
?.trim()
?.replace(/[\[\]()'"''""]/g, '')
?.replace(/\s+/g, ' ');
}

private validateTextCriteria(field: any, input: string | string[]): void {
const { validationCriteria, validationType } = field;

Expand Down Expand Up @@ -310,16 +322,18 @@ export class ValidationService {
const validTextArray = validationCriteria
.replace(/[\[\]]/g, '')
.split(',')
.map((val: string) => val.trim());
.map((val: string) => this.normalizeText(val));

const inputArray = Array.isArray(input)
? input
? input.map((val) => this.normalizeText(val))
: input
.replace(/[\[\]]/g, '')
.split(',')
.map((val) => val.trim());
.map((val) => this.normalizeText(val));

if (!inputArray.every((inp: string) => validTextArray.includes(inp))) {
throw new ValidationError(
`Input ${field.field} must be on or many of the values from: ${validTextArray.join(', ')}`,
`Input ${field.field} must be one or many of the values from: ${validTextArray.join(', ')}`,
);
}
break;
Expand Down
6 changes: 6 additions & 0 deletions src/api/klamm/klamm.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ describe('KlammController', () => {
useValue: {
getKlammBREFields: jest.fn(() => Promise.resolve('expected result')),
getKlammBREFieldFromName: jest.fn((fieldName) => Promise.resolve(`result for ${fieldName}`)), // Mock implementation
_getAllKlammFields: jest.fn(() => Promise.resolve('rules result')),
},
},
],
Expand All @@ -35,4 +36,9 @@ describe('KlammController', () => {
expect(service.getKlammBREFieldFromName).toHaveBeenCalledWith(fieldName);
expect(service.getKlammBREFieldFromName).toHaveBeenCalledTimes(1);
});

it('should call _getAllKlammFields and return expected result', async () => {
expect(await controller.getKlammBRERules()).toBe('rules result');
expect(service._getAllKlammFields).toHaveBeenCalledTimes(1);
});
});
5 changes: 5 additions & 0 deletions src/api/klamm/klamm.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ export class KlammController {
return await this.klammService.getKlammBREFields(searchText);
}

@Get('/brerules')
async getKlammBRERules() {
return await this.klammService._getAllKlammFields();
}

@Get('/brefield/:fieldName')
async getKlammBREFieldFromName(@Param('fieldName') fieldName: string): Promise<KlammField[]> {
return await this.klammService.getKlammBREFieldFromName(fieldName);
Expand Down
48 changes: 40 additions & 8 deletions src/api/ruleMapping/ruleMapping.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,21 +96,53 @@ export class RuleMappingService {
return uniqueFields;
}

// check for presence of goRules expression in string
private isExpressionFunction(expression: string): boolean {
const functionPatterns = [
'none(',
'map(',
'flatMap(',
'filter(',
'some(',
'all(',
'count(',
'contains(',
'flatten(',
'sum(',
'avg(',
'min(',
'max(',
'mean(',
'mode(',
'len(',
'$root',
];
return functionPatterns.some((pattern) => expression.includes(pattern));
}

// Check if the key is being transformed (used in an operation)
private isTransformation(expression: string, key: string): boolean {
const cleanExpression = expression.replace(/\s/g, '');
if (cleanExpression === key) return true;
const operatorPattern = new RegExp(`${key}[^=]*[+\\-*/%?:]`);
return operatorPattern.test(expression);
}

// extract only the unique inputs from a list of nodes
// excludes inputs found in the outputs of other nodes
// inputs that are only transformed are still included as unique as marked as exception
async extractUniqueInputs(nodes: Node[]): Promise<{ uniqueInputs: any[] }> {
const { inputs, outputs } = await this.extractInputsAndOutputs(nodes);
const outputFields = new Set(
outputs
// check for exceptions where input is transformed and exclude from output fields
.filter((outputField) =>
outputField.exception
? outputField.exception.includes(outputField.key)
? outputField.exception === outputField.key
: true
: true,
)
.filter((outputField) => {
if (!outputField.exception) return true;
if (!outputField.key) return true;
if (this.isExpressionFunction(outputField.exception)) {
return true;
}
return !this.isTransformation(outputField.exception, outputField.key);
})
.map((outputField) => outputField.field),
);
const uniqueInputFields = this.findUniqueFields(inputs, outputFields);
Expand Down
35 changes: 23 additions & 12 deletions src/api/scenarioData/scenarioData.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,9 +267,17 @@ export class ScenarioDataService {
return scenarios;
}

private cleanValue(value: string): string {
return value?.trim()?.replace(/[\[\]()]/g, '') ?? '';
}

generatePossibleValues(input: any, defaultValue?: any): any[] {
const { type, dataType, validationCriteria, validationType, childFields } = input;
//Determine how many versions of each field to generate
const parseValue = (value: string) => {
const cleaned = this.cleanValue(value);
return cleaned?.toLowerCase() === 'today' ? new Date() : new Date(cleaned);
};

const complexityGeneration = 10;
if (defaultValue !== null && defaultValue !== undefined) return [defaultValue];

Expand All @@ -288,9 +296,8 @@ export class ScenarioDataService {
return scenarios;

case 'number-input':
const numberValues = validationCriteria?.split(',').map((val: string) => val.trim());
const numberValues = validationCriteria?.split(',').map((val: string) => this.cleanValue(val));
const minValue = (numberValues && parseInt(numberValues[0], 10)) || 0;

const maxValue =
numberValues && numberValues[numberValues?.length - 1] !== minValue.toString()
? numberValues[numberValues?.length - 1]
Expand Down Expand Up @@ -323,7 +330,9 @@ export class ScenarioDataService {
}

case 'date':
const dateValues = validationCriteria?.split(',').map((val: string) => new Date(val.trim()).getTime());
const dateValues = validationCriteria
?.split(',')
.map((val: string) => parseValue(this.cleanValue(val)).getTime());
const minDate = (dateValues && dateValues[0]) || new Date().getTime();
const maxDate =
dateValues && dateValues[dateValues?.length - 1] !== minDate
Expand All @@ -335,7 +344,6 @@ export class ScenarioDataService {
);
switch (validationType) {
case '>=':
return generateRandomDates(complexityGeneration);
case '<=':
return generateRandomDates(complexityGeneration);
case '>':
Expand All @@ -344,28 +352,31 @@ export class ScenarioDataService {
return generateRandomDates(complexityGeneration).filter((date) => new Date(date).getTime() < maxDate);
// range exclusive
case '(date)':
return generateRandomDates(complexityGeneration).filter(
(date) => new Date(date).getTime() > minDate && new Date(date).getTime() < maxDate,
);
// range inclusive
return generateRandomDates(complexityGeneration).filter((date) => {
const dateTime = parseValue(date).getTime();
return dateTime > minDate && dateTime < maxDate;
});
case '[date]':
return generateRandomDates(complexityGeneration);
case '[=date]':
case '[=dates]':
return validationCriteria.split(',').map((val: string) => val.trim());
return validationCriteria.split(',').map((val: string) => {
const parsedDate = parseValue(val.trim());
return parsedDate.toISOString().slice(0, 10);
});
default:
return generateRandomDates(complexityGeneration);
}

case 'text-input':
if (validationType === '[=texts]') {
const textOptionsArray = validationCriteria.split(',').map((val: string) => val.trim());
const textOptionsArray = validationCriteria.split(',').map((val: string) => this.cleanValue(val));
const arrayCombinations = generateCombinationsWithLimit(textOptionsArray);

return arrayCombinations;
}
if (validationType === '[=text]') {
return validationCriteria.split(',').map((val: string) => val.trim());
return validationCriteria.split(',').map((val: string) => this.cleanValue(val));
}
// TODO: Future update to include regex generation
const generateRandomText = (
Expand Down
45 changes: 30 additions & 15 deletions src/utils/csv.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,37 +58,52 @@ describe('CSV Utility Functions', () => {
});

describe('generateCombinationsWithLimit', () => {
it('should generate all combinations for a small input', () => {
const limit = 10;
it('should generate combinations from input array', () => {
const input = ['a', 'b', 'c'];
const result = generateCombinationsWithLimit(input);
const expectedResult = [['a'], ['a', 'b'], ['a', 'b', 'c'], ['a', 'c'], ['b'], ['b', 'c'], ['c']];
expect(result).toEqual(expectedResult);
const result = generateCombinationsWithLimit(input, limit);

// Check that results are arrays of strings from input
expect(result.length).toBeGreaterThan(0);
expect(
result.every((combo) => {
return Array.isArray(combo) && combo.length > 0 && combo.every((item) => input.includes(item));
}),
).toBe(true);
});

it('should respect the limit parameter', () => {
const input = ['a', 'b', 'c', 'd', 'e'];
const result = generateCombinationsWithLimit(input, 10);
expect(result.length).toBe(10);
const result = generateCombinationsWithLimit(input, limit);
expect(result.length).toBeLessThanOrEqual(limit);
});

it('should generate unique combinations', () => {
const input = ['a', 'b', 'c'];
const result = generateCombinationsWithLimit(input, limit);

const uniqueCombos = new Set(result.map((combo) => JSON.stringify(combo.sort())));
expect(uniqueCombos.size).toBe(result.length);
});

it('should handle empty input array', () => {
const input: string[] = [];
const result = generateCombinationsWithLimit(input);
const result = generateCombinationsWithLimit(input, limit);
expect(result).toEqual([]);
});

it('should handle single element input array', () => {
const input = ['a'];
const result = generateCombinationsWithLimit(input);
expect(result).toEqual([['a']]);
const result = generateCombinationsWithLimit(input, limit);
expect(result.length).toBe(1);
expect(result[0]).toEqual(['a']);
});

it('should handle large input without exceeding memory limits', () => {
const largeInput = Array(20)
.fill(0)
.map((_, i) => String.fromCharCode(97 + i));
const result = generateCombinationsWithLimit(largeInput, 1000000);
expect(result.length).toBe(1000000);
it('should generate combinations of varying lengths', () => {
const input = ['a', 'b', 'c', 'd'];
const result = generateCombinationsWithLimit(input, limit);
const hasVariableLengths = result.some((combo) => combo.length !== result[0].length);
expect(hasVariableLengths).toBe(true);
});
});

Expand Down
33 changes: 21 additions & 12 deletions src/utils/csv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,23 +126,32 @@ export const complexCartesianProduct = <T>(arrays: T[][], limit: number = 3000):
};

/**
* Generates all combinations of a given array with varying lengths.
* Generates random combinations of items from an array with varying lengths.
* @param arr The input array to generate combinations from.
* @param limit The maximum number of combinations to generate.
* @returns The generated product.
* @param limit The maximum number of combinations to generate (default: 1000).
* @returns Array of combinations, each containing 1 to n items from the input array.
*/
export const generateCombinationsWithLimit = (arr: string[], limit: number = 1000): string[][] => {
const result: string[][] = [];
if (arr.length == 0) return result;
const getRandomItems = (items: string[], minCount: number = 1): string[] => {
const count = Math.floor(Math.random() * (items.length - minCount + 1)) + minCount;
const shuffled = [...items].sort(() => Math.random() - 0.5);
return shuffled.slice(0, count);
};

while (result.length < limit) {
const combination = getRandomItems(arr);

const combine = (prefix: string[], remaining: string[], start: number) => {
if (result.length >= limit) return; // Stop when the limit is reached
for (let i = start; i < remaining.length; i++) {
const newPrefix = [...prefix, remaining[i]];
result.push(newPrefix);
combine(newPrefix, remaining, i + 1);
// Check for combination uniqueness
const combinationStr = JSON.stringify(combination.sort());
if (!result.some((existing) => JSON.stringify(existing.sort()) === combinationStr)) {
result.push(combination);
}
};

combine([], arr, 0);
return result.slice(0, limit);
// Break if no more unique combinations
if (result.length === Math.pow(2, arr.length) - 1) break;
}

return result;
};

0 comments on commit e8b30a1

Please sign in to comment.