From 52069a9adac21db55956a76b3bdc2f69926bd657 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Wed, 12 Jun 2024 16:07:42 -0700 Subject: [PATCH 01/52] Initiate scenario data endpoint. --- .../scenarioData.controller.spec.ts | 170 +++++++++++ .../scenarioData/scenarioData.controller.ts | 74 +++++ src/api/scenarioData/scenarioData.schema.ts | 25 ++ .../scenarioData/scenarioData.service.spec.ts | 263 ++++++++++++++++++ src/api/scenarioData/scenarioData.service.ts | 80 ++++++ src/app.module.ts | 18 +- 6 files changed, 628 insertions(+), 2 deletions(-) create mode 100644 src/api/scenarioData/scenarioData.controller.spec.ts create mode 100644 src/api/scenarioData/scenarioData.controller.ts create mode 100644 src/api/scenarioData/scenarioData.schema.ts create mode 100644 src/api/scenarioData/scenarioData.service.spec.ts create mode 100644 src/api/scenarioData/scenarioData.service.ts diff --git a/src/api/scenarioData/scenarioData.controller.spec.ts b/src/api/scenarioData/scenarioData.controller.spec.ts new file mode 100644 index 0000000..032551d --- /dev/null +++ b/src/api/scenarioData/scenarioData.controller.spec.ts @@ -0,0 +1,170 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ScenarioDataController } from './scenarioData.controller'; +import { ScenarioDataService } from './scenarioData.service'; +import { ScenarioData } from './scenarioData.schema'; +import { HttpException } from '@nestjs/common'; +import { Types } from 'mongoose'; + +describe('ScenarioDataController', () => { + let controller: ScenarioDataController; + let service: ScenarioDataService; + + const testObjectId = new Types.ObjectId(); + + const mockScenarioDataService = { + getAllScenarioData: jest.fn(), + getScenariosByRuleId: jest.fn(), + getScenariosByFilename: jest.fn(), + getScenarioData: jest.fn(), + createScenarioData: jest.fn(), + updateScenarioData: jest.fn(), + deleteScenarioData: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ScenarioDataController], + providers: [ + { + provide: ScenarioDataService, + useValue: mockScenarioDataService, + }, + ], + }).compile(); + + controller = module.get(ScenarioDataController); + service = module.get(ScenarioDataService); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + describe('getAllScenarioData', () => { + it('should return an array of scenarios', async () => { + const result: ScenarioData[] = []; + jest.spyOn(service, 'getAllScenarioData').mockResolvedValue(result); + + expect(await controller.getAllScenarioData()).toBe(result); + }); + + it('should throw an error if service fails', async () => { + jest.spyOn(service, 'getAllScenarioData').mockRejectedValue(new Error('Service error')); + + await expect(controller.getAllScenarioData()).rejects.toThrow(HttpException); + }); + }); + + describe('getScenariosByRuleId', () => { + it('should return scenarios by rule ID', async () => { + const result: ScenarioData[] = []; + jest.spyOn(service, 'getScenariosByRuleId').mockResolvedValue(result); + + expect(await controller.getScenariosByRuleId('someRuleId')).toBe(result); + }); + + it('should throw an error if service fails', async () => { + jest.spyOn(service, 'getScenariosByRuleId').mockRejectedValue(new Error('Service error')); + + await expect(controller.getScenariosByRuleId('someRuleId')).rejects.toThrow(HttpException); + }); + }); + + describe('getScenariosByFilename', () => { + it('should return scenarios by filename', async () => { + const result: ScenarioData[] = []; + jest.spyOn(service, 'getScenariosByFilename').mockResolvedValue(result); + + expect(await controller.getScenariosByFilename('someFilename')).toBe(result); + }); + + it('should throw an error if service fails', async () => { + jest.spyOn(service, 'getScenariosByFilename').mockRejectedValue(new Error('Service error')); + + await expect(controller.getScenariosByFilename('someFilename')).rejects.toThrow(HttpException); + }); + }); + + describe('getScenariosByRuleId', () => { + it('should return scenarios by rule ID', async () => { + const result: ScenarioData[] = []; + jest.spyOn(service, 'getScenariosByRuleId').mockResolvedValue(result); + + expect(await controller.getScenariosByRuleId('ruleID')).toBe(result); + }); + + it('should throw an error if service fails', async () => { + jest.spyOn(service, 'getScenariosByRuleId').mockRejectedValue(new Error('Service error')); + + await expect(controller.getScenariosByRuleId('ruleID')).rejects.toThrow(HttpException); + }); + }); + + describe('createScenarioData', () => { + it('should create a scenario', async () => { + const result: ScenarioData = { + _id: testObjectId, + title: 'title', + ruleID: 'ruleID', + goRulesJSONFilename: 'filename', + }; + jest.spyOn(service, 'createScenarioData').mockResolvedValue(result); + + expect(await controller.createScenarioData(result)).toBe(result); + }); + + it('should throw an error if service fails', async () => { + jest.spyOn(service, 'createScenarioData').mockRejectedValue(new Error('Service error')); + + await expect( + controller.createScenarioData({ + _id: testObjectId, + title: 'title', + ruleID: 'ruleID', + goRulesJSONFilename: 'filename', + }), + ).rejects.toThrow(HttpException); + }); + }); + + describe('updateScenarioData', () => { + it('should update a scenario', async () => { + const result: ScenarioData = { + _id: testObjectId, + title: 'title', + ruleID: 'rule1', + goRulesJSONFilename: 'filename', + }; + jest.spyOn(service, 'updateScenarioData').mockResolvedValue(result); + + expect(await controller.updateScenarioData(testObjectId.toHexString(), result)).toBe(result); + }); + + it('should throw an error if service fails', async () => { + jest.spyOn(service, 'updateScenarioData').mockRejectedValue(new Error('Service error')); + + await expect( + controller.updateScenarioData(testObjectId.toHexString(), { + _id: testObjectId, + title: 'title', + ruleID: 'rule1', + goRulesJSONFilename: 'filename', + }), + ).rejects.toThrow(HttpException); + }); + }); + + describe('deleteScenarioData', () => { + it('should delete a scenario', async () => { + jest.spyOn(service, 'deleteScenarioData').mockResolvedValue(undefined); + + await expect(controller.deleteScenarioData(testObjectId.toHexString())).resolves.toBeUndefined(); + }); + + it('should throw an error if service fails', async () => { + jest.spyOn(service, 'deleteScenarioData').mockRejectedValue(new Error('Service error')); + + await expect(controller.deleteScenarioData(testObjectId.toHexString())).rejects.toThrow(HttpException); + }); + }); +}); diff --git a/src/api/scenarioData/scenarioData.controller.ts b/src/api/scenarioData/scenarioData.controller.ts new file mode 100644 index 0000000..4d74b36 --- /dev/null +++ b/src/api/scenarioData/scenarioData.controller.ts @@ -0,0 +1,74 @@ +import { Controller, Get, Param, Post, Body, Put, Delete, HttpException, HttpStatus } from '@nestjs/common'; +import { ScenarioDataService } from './scenarioData.service'; +import { ScenarioData } from './scenarioData.schema'; + +@Controller('api/scenario') +export class ScenarioDataController { + constructor(private readonly scenarioDataService: ScenarioDataService) {} + + @Get('/list') + async getAllScenarioData(): Promise { + try { + return await this.scenarioDataService.getAllScenarioData(); + } catch (error) { + throw new HttpException('Error getting all scenario data', HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @Get('/by-rule/:ruleId') + async getScenariosByRuleId(@Param('ruleId') ruleId: string): Promise { + try { + return await this.scenarioDataService.getScenariosByRuleId(ruleId); + } catch (error) { + throw new HttpException('Error getting scenarios by rule ID', HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @Get('/by-filename/:goRulesJSONFilename') + async getScenariosByFilename(@Param('goRulesJSONFilename') goRulesJSONFilename: string): Promise { + try { + return await this.scenarioDataService.getScenariosByFilename(goRulesJSONFilename); + } catch (error) { + throw new HttpException('Error getting scenarios by filename', HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @Get('/:scenarioId') + async getScenarioData(@Param('scenarioId') scenarioId: string): Promise { + try { + return await this.scenarioDataService.getScenarioData(scenarioId); + } catch (error) { + throw new HttpException('Error getting scenario data', HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @Post() + async createScenarioData(@Body() scenarioData: ScenarioData): Promise { + try { + return await this.scenarioDataService.createScenarioData(scenarioData); + } catch (error) { + throw new HttpException('Error creating scenario data', HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @Put('/:scenarioId') + async updateScenarioData( + @Param('scenarioId') scenarioId: string, + @Body() scenarioData: ScenarioData, + ): Promise { + try { + return await this.scenarioDataService.updateScenarioData(scenarioId, scenarioData); + } catch (error) { + throw new HttpException('Error updating scenario data', HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @Delete('/:scenarioId') + async deleteScenarioData(@Param('scenarioId') scenarioId: string): Promise { + try { + await this.scenarioDataService.deleteScenarioData(scenarioId); + } catch (error) { + throw new HttpException('Error deleting scenario data', HttpStatus.INTERNAL_SERVER_ERROR); + } + } +} diff --git a/src/api/scenarioData/scenarioData.schema.ts b/src/api/scenarioData/scenarioData.schema.ts new file mode 100644 index 0000000..4fb0a20 --- /dev/null +++ b/src/api/scenarioData/scenarioData.schema.ts @@ -0,0 +1,25 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { Document, Types } from 'mongoose'; + +export type ScenarioDataDocument = ScenarioData & Document; + +@Schema() +export class ScenarioData { + @Prop({ required: true, description: 'The scenario ID', type: Types.ObjectId }) + _id: Types.ObjectId; + + @Prop({ description: 'The title of the scenario' }) + title: string; + + @Prop({ + ref: 'RuleData', + required: true, + description: 'The ID of the rule', + }) + ruleID: string; + + @Prop({ required: true, description: 'The filename of the JSON file containing the rule' }) + goRulesJSONFilename: string; +} + +export const ScenarioDataSchema = SchemaFactory.createForClass(ScenarioData); diff --git a/src/api/scenarioData/scenarioData.service.spec.ts b/src/api/scenarioData/scenarioData.service.spec.ts new file mode 100644 index 0000000..24d9174 --- /dev/null +++ b/src/api/scenarioData/scenarioData.service.spec.ts @@ -0,0 +1,263 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ScenarioDataService } from './scenarioData.service'; +import { getModelToken } from '@nestjs/mongoose'; +import { Types, Model } from 'mongoose'; +import { ScenarioData, ScenarioDataDocument } from './scenarioData.schema'; + +describe('ScenarioDataService', () => { + let service: ScenarioDataService; + let model: Model; + + const testObjectId = new Types.ObjectId(); + + const mockScenarioData: ScenarioData = { + _id: testObjectId, + title: 'Test Title', + ruleID: 'ruleID', + goRulesJSONFilename: 'test.json', + }; + + class MockScenarioDataModel { + constructor(private data: ScenarioData) {} + save = jest.fn().mockResolvedValue(this.data); + static find = jest.fn(); + static findOne = jest.fn(); + static findOneAndDelete = jest.fn(); + } + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ScenarioDataService, + { + provide: getModelToken(ScenarioData.name), + useValue: MockScenarioDataModel, + }, + ], + }).compile(); + + service = module.get(ScenarioDataService); + model = module.get>(getModelToken(ScenarioData.name)); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('getAllScenarioData', () => { + it('should return all scenario data', async () => { + const scenarioDataList: ScenarioData[] = [mockScenarioData]; + MockScenarioDataModel.find = jest.fn().mockReturnValue({ exec: jest.fn().mockResolvedValue(scenarioDataList) }); + + const result = await service.getAllScenarioData(); + + expect(result).toEqual(scenarioDataList); + expect(MockScenarioDataModel.find).toHaveBeenCalled(); + }); + + it('should throw an error if an error occurs while retrieving scenario data', async () => { + const errorMessage = 'DB Error'; + MockScenarioDataModel.find = jest + .fn() + .mockReturnValue({ exec: jest.fn().mockRejectedValue(new Error(errorMessage)) }); + + await expect(async () => { + await service.getAllScenarioData(); + }).rejects.toThrowError(`Error getting all scenario data: ${errorMessage}`); + + expect(MockScenarioDataModel.find).toHaveBeenCalled(); + }); + }); + + describe('getScenarioData', () => { + it('should return scenario data by id', async () => { + const scenarioId = testObjectId.toString(); + const scenarioData: ScenarioData = mockScenarioData; + MockScenarioDataModel.findOne = jest.fn().mockReturnValue({ exec: jest.fn().mockResolvedValue(scenarioData) }); + + const result = await service.getScenarioData(scenarioId); + + expect(result).toEqual(scenarioData); + expect(MockScenarioDataModel.findOne).toHaveBeenCalledWith({ _id: scenarioId }); + }); + + it('should throw an error if scenario data is not found', async () => { + const scenarioId = testObjectId.toString(); + MockScenarioDataModel.findOne = jest.fn().mockReturnValue({ exec: jest.fn().mockResolvedValue(null) }); + + await expect(async () => { + await service.getScenarioData(scenarioId); + }).rejects.toThrowError('Scenario data not found'); + + expect(MockScenarioDataModel.findOne).toHaveBeenCalledWith({ _id: scenarioId }); + }); + + it('should throw an error if an error occurs while retrieving scenario data', async () => { + const scenarioId = testObjectId.toString(); + const errorMessage = 'DB Error'; + MockScenarioDataModel.findOne = jest + .fn() + .mockReturnValue({ exec: jest.fn().mockRejectedValue(new Error(errorMessage)) }); + + await expect(async () => { + await service.getScenarioData(scenarioId); + }).rejects.toThrowError(`Error getting scenario data: ${errorMessage}`); + + expect(MockScenarioDataModel.findOne).toHaveBeenCalledWith({ _id: scenarioId }); + }); + }); + + describe('createScenarioData', () => { + it('should create scenario data', async () => { + const scenarioData: ScenarioData = mockScenarioData; + const modelInstance = new model(scenarioData); + modelInstance.save = jest.fn().mockResolvedValue(scenarioData); + const result = await service.createScenarioData(scenarioData); + expect(result).toEqual(scenarioData); + }); + }); + + describe('updateScenarioData', () => { + it('should update scenario data', async () => { + const scenarioId = testObjectId.toString(); + const updatedData: Partial = { title: 'Updated Title' }; + const existingScenarioData = new model(mockScenarioData); + existingScenarioData.save = jest.fn().mockResolvedValue({ ...mockScenarioData, ...updatedData }); + + MockScenarioDataModel.findOne = jest + .fn() + .mockReturnValue({ exec: jest.fn().mockResolvedValue(existingScenarioData) }); + + const result = await service.updateScenarioData(scenarioId, updatedData); + + expect(result).toEqual({ ...mockScenarioData, ...updatedData }); + expect(MockScenarioDataModel.findOne).toHaveBeenCalledWith({ _id: scenarioId }); + expect(existingScenarioData.save).toHaveBeenCalled(); + }); + + it('should throw an error if scenario data is not found', async () => { + const scenarioId = testObjectId.toString(); + const updatedData: Partial = { title: 'Updated Title' }; + + MockScenarioDataModel.findOne = jest.fn().mockReturnValue({ exec: jest.fn().mockResolvedValue(null) }); + + await expect(async () => { + await service.updateScenarioData(scenarioId, updatedData); + }).rejects.toThrowError('Scenario data not found'); + + expect(MockScenarioDataModel.findOne).toHaveBeenCalledWith({ _id: scenarioId }); + }); + + it('should throw an error if an error occurs while updating scenario data', async () => { + const scenarioId = testObjectId.toString(); + const updatedData: Partial = { title: 'Updated Title' }; + const errorMessage = 'DB Error'; + + MockScenarioDataModel.findOne = jest + .fn() + .mockReturnValue({ exec: jest.fn().mockRejectedValue(new Error(errorMessage)) }); + + await expect(async () => { + await service.updateScenarioData(scenarioId, updatedData); + }).rejects.toThrowError(`Failed to update scenario data: ${errorMessage}`); + + expect(MockScenarioDataModel.findOne).toHaveBeenCalledWith({ _id: scenarioId }); + }); + }); + + describe('deleteScenarioData', () => { + it('should delete scenario data', async () => { + const scenarioId = testObjectId.toString(); + MockScenarioDataModel.findOneAndDelete = jest + .fn() + .mockReturnValue({ exec: jest.fn().mockResolvedValue(mockScenarioData) }); + + await service.deleteScenarioData(scenarioId); + + expect(MockScenarioDataModel.findOneAndDelete).toHaveBeenCalledWith({ _id: scenarioId }); + }); + + it('should throw an error if scenario data is not found', async () => { + const scenarioId = testObjectId.toString(); + + MockScenarioDataModel.findOneAndDelete = jest.fn().mockReturnValue({ exec: jest.fn().mockResolvedValue(null) }); + + await expect(async () => { + await service.deleteScenarioData(scenarioId); + }).rejects.toThrowError('Scenario data not found'); + + expect(MockScenarioDataModel.findOneAndDelete).toHaveBeenCalledWith({ _id: scenarioId }); + }); + + it('should throw an error if an error occurs while deleting scenario data', async () => { + const scenarioId = testObjectId.toString(); + const errorMessage = 'DB Error'; + + MockScenarioDataModel.findOneAndDelete = jest + .fn() + .mockReturnValue({ exec: jest.fn().mockRejectedValue(new Error(errorMessage)) }); + + await expect(async () => { + await service.deleteScenarioData(scenarioId); + }).rejects.toThrowError(`Failed to delete scenario data: ${errorMessage}`); + + expect(MockScenarioDataModel.findOneAndDelete).toHaveBeenCalledWith({ _id: scenarioId }); + }); + }); + + describe('getScenariosByRuleId', () => { + it('should return scenarios by rule ID', async () => { + const ruleId = testObjectId.toString(); + const scenarioDataList: ScenarioData[] = [mockScenarioData]; + MockScenarioDataModel.find = jest.fn().mockReturnValue({ exec: jest.fn().mockResolvedValue(scenarioDataList) }); + + const result = await service.getScenariosByRuleId(ruleId); + + expect(result).toEqual(scenarioDataList); + expect(MockScenarioDataModel.find).toHaveBeenCalledWith({ ruleID: ruleId }); + }); + + it('should throw an error if an error occurs while retrieving scenarios by rule ID', async () => { + const ruleId = testObjectId.toString(); + const errorMessage = 'DB Error'; + + MockScenarioDataModel.find = jest + .fn() + .mockReturnValue({ exec: jest.fn().mockRejectedValue(new Error(errorMessage)) }); + + await expect(async () => { + await service.getScenariosByRuleId(ruleId); + }).rejects.toThrowError(`Error getting scenarios by rule ID: ${errorMessage}`); + + expect(MockScenarioDataModel.find).toHaveBeenCalledWith({ ruleID: ruleId }); + }); + }); + + describe('getScenariosByFilename', () => { + it('should return scenarios by filename', async () => { + const goRulesJSONFilename = 'test.json'; + const scenarioDataList: ScenarioData[] = [mockScenarioData]; + MockScenarioDataModel.find = jest.fn().mockReturnValue({ exec: jest.fn().mockResolvedValue(scenarioDataList) }); + + const result = await service.getScenariosByFilename(goRulesJSONFilename); + + expect(result).toEqual(scenarioDataList); + expect(MockScenarioDataModel.find).toHaveBeenCalledWith({ goRulesJSONFilename }); + }); + + it('should throw an error if an error occurs while retrieving scenarios by filename', async () => { + const goRulesJSONFilename = 'test.json'; + const errorMessage = 'DB Error'; + + MockScenarioDataModel.find = jest + .fn() + .mockReturnValue({ exec: jest.fn().mockRejectedValue(new Error(errorMessage)) }); + + await expect(async () => { + await service.getScenariosByFilename(goRulesJSONFilename); + }).rejects.toThrowError(`Error getting scenarios by filename: ${errorMessage}`); + + expect(MockScenarioDataModel.find).toHaveBeenCalledWith({ goRulesJSONFilename }); + }); + }); +}); diff --git a/src/api/scenarioData/scenarioData.service.ts b/src/api/scenarioData/scenarioData.service.ts new file mode 100644 index 0000000..a28a90b --- /dev/null +++ b/src/api/scenarioData/scenarioData.service.ts @@ -0,0 +1,80 @@ +import { Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model } from 'mongoose'; +import { ScenarioData, ScenarioDataDocument } from './scenarioData.schema'; + +@Injectable() +export class ScenarioDataService { + constructor(@InjectModel(ScenarioData.name) private scenarioDataModel: Model) {} + + async getAllScenarioData(): Promise { + try { + const scenarioDataList = await this.scenarioDataModel.find().exec(); + return scenarioDataList; + } catch (error) { + throw new Error(`Error getting all scenario data: ${error.message}`); + } + } + + async getScenarioData(scenarioId: string): Promise { + try { + const scenarioData = await this.scenarioDataModel.findOne({ _id: scenarioId }).exec(); + if (!scenarioData) { + throw new Error('Scenario data not found'); + } + return scenarioData; + } catch (error) { + throw new Error(`Error getting scenario data: ${error.message}`); + } + } + + async createScenarioData(scenarioData: ScenarioData): Promise { + try { + const newScenarioData = new this.scenarioDataModel(scenarioData); + const response = await newScenarioData.save(); + return response; + } catch (error) { + throw new Error(`Failed to add scenario data: ${error.message}`); + } + } + + async updateScenarioData(scenarioId: string, updatedData: Partial): Promise { + try { + const existingScenarioData = await this.scenarioDataModel.findOne({ _id: scenarioId }).exec(); + if (!existingScenarioData) { + throw new Error('Scenario data not found'); + } + Object.assign(existingScenarioData, updatedData); + return await existingScenarioData.save(); + } catch (error) { + throw new Error(`Failed to update scenario data: ${error.message}`); + } + } + + async deleteScenarioData(scenarioId: string): Promise { + try { + const deletedScenarioData = await this.scenarioDataModel.findOneAndDelete({ _id: scenarioId }).exec(); + if (!deletedScenarioData) { + throw new Error('Scenario data not found'); + } + } catch (error) { + throw new Error(`Failed to delete scenario data: ${error.message}`); + } + } + + async getScenariosByRuleId(ruleId: string): Promise { + try { + return await this.scenarioDataModel.find({ ruleID: ruleId }).exec(); + } catch (error) { + throw new Error(`Error getting scenarios by rule ID: ${error.message}`); + } + } + + async getScenariosByFilename(goRulesJSONFilename: string): Promise { + try { + return await this.scenarioDataModel.find({ goRulesJSONFilename: goRulesJSONFilename }).exec(); + } catch (error) { + throw new Error(`Error getting scenarios by filename: ${error.message}`); + } + } +} diff --git a/src/app.module.ts b/src/app.module.ts index 81dc55f..2244295 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -12,6 +12,9 @@ import { SubmissionsController } from './api/submissions/submissions.controller' import { SubmissionsService } from './api/submissions/submissions.service'; import { RuleMappingController } from './api/ruleMapping/ruleMapping.controller'; import { RuleMappingService } from './api/ruleMapping/ruleMapping.service'; +import { ScenarioDataSchema } from './api/scenarioData/scenarioData.schema'; +import { ScenarioDataController } from './api/scenarioData/scenarioData.controller'; +import { ScenarioDataService } from './api/scenarioData/scenarioData.service'; @Module({ imports: [ @@ -24,7 +27,10 @@ import { RuleMappingService } from './api/ruleMapping/ruleMapping.service'; ], }), MongooseModule.forRoot(process.env.MONGODB_URL), - MongooseModule.forFeature([{ name: RuleData.name, schema: RuleDataSchema }]), + MongooseModule.forFeature([ + { name: RuleData.name, schema: RuleDataSchema }, + { name: 'ScenarioData', schema: ScenarioDataSchema }, + ]), ], controllers: [ RuleDataController, @@ -32,7 +38,15 @@ import { RuleMappingService } from './api/ruleMapping/ruleMapping.service'; DocumentsController, SubmissionsController, RuleMappingController, + ScenarioDataController, + ], + providers: [ + RuleDataService, + DecisionsService, + DocumentsService, + SubmissionsService, + RuleMappingService, + ScenarioDataService, ], - providers: [RuleDataService, DecisionsService, DocumentsService, SubmissionsService, RuleMappingService], }) export class AppModule {} From 71e2574c29daade08a142dbea1192fe4fff455ae Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Wed, 12 Jun 2024 16:29:29 -0700 Subject: [PATCH 02/52] Add variables to scenario object. --- .../scenarioData/scenarioData.controller.spec.ts | 4 ++++ src/api/scenarioData/scenarioData.schema.ts | 13 ++++++++++++- src/api/scenarioData/scenarioData.service.spec.ts | 1 + 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/api/scenarioData/scenarioData.controller.spec.ts b/src/api/scenarioData/scenarioData.controller.spec.ts index 032551d..54c4bde 100644 --- a/src/api/scenarioData/scenarioData.controller.spec.ts +++ b/src/api/scenarioData/scenarioData.controller.spec.ts @@ -106,6 +106,7 @@ describe('ScenarioDataController', () => { _id: testObjectId, title: 'title', ruleID: 'ruleID', + variables: [], goRulesJSONFilename: 'filename', }; jest.spyOn(service, 'createScenarioData').mockResolvedValue(result); @@ -121,6 +122,7 @@ describe('ScenarioDataController', () => { _id: testObjectId, title: 'title', ruleID: 'ruleID', + variables: [], goRulesJSONFilename: 'filename', }), ).rejects.toThrow(HttpException); @@ -133,6 +135,7 @@ describe('ScenarioDataController', () => { _id: testObjectId, title: 'title', ruleID: 'rule1', + variables: [], goRulesJSONFilename: 'filename', }; jest.spyOn(service, 'updateScenarioData').mockResolvedValue(result); @@ -148,6 +151,7 @@ describe('ScenarioDataController', () => { _id: testObjectId, title: 'title', ruleID: 'rule1', + variables: [], goRulesJSONFilename: 'filename', }), ).rejects.toThrow(HttpException); diff --git a/src/api/scenarioData/scenarioData.schema.ts b/src/api/scenarioData/scenarioData.schema.ts index 4fb0a20..f9d66db 100644 --- a/src/api/scenarioData/scenarioData.schema.ts +++ b/src/api/scenarioData/scenarioData.schema.ts @@ -2,7 +2,11 @@ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Document, Types } from 'mongoose'; export type ScenarioDataDocument = ScenarioData & Document; - +export interface Variable { + name: string; + value: any; + type: string; +} @Schema() export class ScenarioData { @Prop({ required: true, description: 'The scenario ID', type: Types.ObjectId }) @@ -18,6 +22,13 @@ export class ScenarioData { }) ruleID: string; + @Prop({ + required: true, + description: 'The variables of the scenario', + type: [{ name: String, value: {}, type: String }], + }) + variables: Variable[]; + @Prop({ required: true, description: 'The filename of the JSON file containing the rule' }) goRulesJSONFilename: string; } diff --git a/src/api/scenarioData/scenarioData.service.spec.ts b/src/api/scenarioData/scenarioData.service.spec.ts index 24d9174..2bba130 100644 --- a/src/api/scenarioData/scenarioData.service.spec.ts +++ b/src/api/scenarioData/scenarioData.service.spec.ts @@ -14,6 +14,7 @@ describe('ScenarioDataService', () => { _id: testObjectId, title: 'Test Title', ruleID: 'ruleID', + variables: [], goRulesJSONFilename: 'test.json', }; From 729c5fd2e7e850495fad4be505bb1c81df551c52 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Wed, 12 Jun 2024 17:01:59 -0700 Subject: [PATCH 03/52] Update variables schema. --- src/api/scenarioData/scenarioData.schema.ts | 17 ++++++++++++++++- src/api/scenarioData/scenarioData.service.ts | 15 +++++++++++++++ src/app.module.ts | 4 ++-- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/api/scenarioData/scenarioData.schema.ts b/src/api/scenarioData/scenarioData.schema.ts index f9d66db..afa4b50 100644 --- a/src/api/scenarioData/scenarioData.schema.ts +++ b/src/api/scenarioData/scenarioData.schema.ts @@ -7,6 +7,21 @@ export interface Variable { value: any; type: string; } + +@Schema() +export class VariableSchema { + @Prop({ required: true, type: String }) + name: string; + + @Prop({ required: true, type: {} }) + value: any; + + @Prop({ required: true, type: String }) + type: string; +} + +export const VariableModel = SchemaFactory.createForClass(VariableSchema); + @Schema() export class ScenarioData { @Prop({ required: true, description: 'The scenario ID', type: Types.ObjectId }) @@ -25,7 +40,7 @@ export class ScenarioData { @Prop({ required: true, description: 'The variables of the scenario', - type: [{ name: String, value: {}, type: String }], + type: [VariableSchema], }) variables: Variable[]; diff --git a/src/api/scenarioData/scenarioData.service.ts b/src/api/scenarioData/scenarioData.service.ts index a28a90b..7d47f6c 100644 --- a/src/api/scenarioData/scenarioData.service.ts +++ b/src/api/scenarioData/scenarioData.service.ts @@ -28,12 +28,27 @@ export class ScenarioDataService { } } + isValidVariableStructure(variables: any[]): boolean { + for (const variable of variables) { + if ( + !variable || + typeof variable.name !== 'string' || + typeof variable.type !== 'string' || + (variable.value !== null && typeof variable.value === 'undefined') + ) { + return false; + } + } + return true; + } + async createScenarioData(scenarioData: ScenarioData): Promise { try { const newScenarioData = new this.scenarioDataModel(scenarioData); const response = await newScenarioData.save(); return response; } catch (error) { + console.error('Error in createScenarioData:', error); throw new Error(`Failed to add scenario data: ${error.message}`); } } diff --git a/src/app.module.ts b/src/app.module.ts index 2244295..c72454e 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -12,7 +12,7 @@ import { SubmissionsController } from './api/submissions/submissions.controller' import { SubmissionsService } from './api/submissions/submissions.service'; import { RuleMappingController } from './api/ruleMapping/ruleMapping.controller'; import { RuleMappingService } from './api/ruleMapping/ruleMapping.service'; -import { ScenarioDataSchema } from './api/scenarioData/scenarioData.schema'; +import { ScenarioData, ScenarioDataSchema } from './api/scenarioData/scenarioData.schema'; import { ScenarioDataController } from './api/scenarioData/scenarioData.controller'; import { ScenarioDataService } from './api/scenarioData/scenarioData.service'; @@ -29,7 +29,7 @@ import { ScenarioDataService } from './api/scenarioData/scenarioData.service'; MongooseModule.forRoot(process.env.MONGODB_URL), MongooseModule.forFeature([ { name: RuleData.name, schema: RuleDataSchema }, - { name: 'ScenarioData', schema: ScenarioDataSchema }, + { name: ScenarioData.name, schema: ScenarioDataSchema }, ]), ], controllers: [ From 6133496d75ee70464d3be47995f235514cd5b340 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Thu, 13 Jun 2024 08:21:36 -0700 Subject: [PATCH 04/52] Update schema to generate id for scenario by default. --- src/api/scenarioData/scenarioData.schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/scenarioData/scenarioData.schema.ts b/src/api/scenarioData/scenarioData.schema.ts index afa4b50..1a5bb2d 100644 --- a/src/api/scenarioData/scenarioData.schema.ts +++ b/src/api/scenarioData/scenarioData.schema.ts @@ -24,7 +24,7 @@ export const VariableModel = SchemaFactory.createForClass(VariableSchema); @Schema() export class ScenarioData { - @Prop({ required: true, description: 'The scenario ID', type: Types.ObjectId }) + @Prop({ required: true, description: 'The scenario ID', type: Types.ObjectId, default: () => new Types.ObjectId() }) _id: Types.ObjectId; @Prop({ description: 'The title of the scenario' }) From 7cd8903a536ccd26cfa3ff192f6d46e6a572982e Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Thu, 13 Jun 2024 08:46:04 -0700 Subject: [PATCH 05/52] Update schema to generate type of variable based on value input. --- src/api/scenarioData/scenarioData.schema.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/api/scenarioData/scenarioData.schema.ts b/src/api/scenarioData/scenarioData.schema.ts index 1a5bb2d..b713006 100644 --- a/src/api/scenarioData/scenarioData.schema.ts +++ b/src/api/scenarioData/scenarioData.schema.ts @@ -5,7 +5,7 @@ export type ScenarioDataDocument = ScenarioData & Document; export interface Variable { name: string; value: any; - type: string; + type?: string; } @Schema() @@ -16,11 +16,19 @@ export class VariableSchema { @Prop({ required: true, type: {} }) value: any; - @Prop({ required: true, type: String }) + @Prop({ required: false, type: String, default: '' }) type: string; } -export const VariableModel = SchemaFactory.createForClass(VariableSchema); +const VariableModelSchema = SchemaFactory.createForClass(VariableSchema); + +// impute the type of the value if not provided +VariableModelSchema.pre('save', function (next) { + if (!this.type) { + this.type = typeof this.value; + } + next(); +}); @Schema() export class ScenarioData { @@ -40,7 +48,7 @@ export class ScenarioData { @Prop({ required: true, description: 'The variables of the scenario', - type: [VariableSchema], + type: [VariableModelSchema], }) variables: Variable[]; From 8bf98eeff97eaf8fd2201f0f5606e805c7c6b010 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Mon, 17 Jun 2024 16:26:45 -0700 Subject: [PATCH 06/52] Update scenario deletion. --- .../scenarioData/scenarioData.service.spec.ts | 37 ++++++++----------- src/api/scenarioData/scenarioData.service.ts | 6 ++- 2 files changed, 19 insertions(+), 24 deletions(-) diff --git a/src/api/scenarioData/scenarioData.service.spec.ts b/src/api/scenarioData/scenarioData.service.spec.ts index 2bba130..5d47c9c 100644 --- a/src/api/scenarioData/scenarioData.service.spec.ts +++ b/src/api/scenarioData/scenarioData.service.spec.ts @@ -167,42 +167,35 @@ describe('ScenarioDataService', () => { }); describe('deleteScenarioData', () => { - it('should delete scenario data', async () => { + it('should delete scenario data successfully', async () => { const scenarioId = testObjectId.toString(); - MockScenarioDataModel.findOneAndDelete = jest - .fn() - .mockReturnValue({ exec: jest.fn().mockResolvedValue(mockScenarioData) }); - - await service.deleteScenarioData(scenarioId); + const objectId = new Types.ObjectId(scenarioId); + MockScenarioDataModel.findOneAndDelete = jest.fn().mockResolvedValue({ _id: objectId }); + await expect(service.deleteScenarioData(scenarioId)).resolves.toBeUndefined(); - expect(MockScenarioDataModel.findOneAndDelete).toHaveBeenCalledWith({ _id: scenarioId }); + expect(MockScenarioDataModel.findOneAndDelete).toHaveBeenCalledWith({ _id: objectId }); }); it('should throw an error if scenario data is not found', async () => { const scenarioId = testObjectId.toString(); + const objectId = new Types.ObjectId(scenarioId); - MockScenarioDataModel.findOneAndDelete = jest.fn().mockReturnValue({ exec: jest.fn().mockResolvedValue(null) }); + MockScenarioDataModel.findOneAndDelete = jest.fn().mockResolvedValue(null); - await expect(async () => { - await service.deleteScenarioData(scenarioId); - }).rejects.toThrowError('Scenario data not found'); + await expect(service.deleteScenarioData(scenarioId)).rejects.toThrowError('Scenario data not found'); - expect(MockScenarioDataModel.findOneAndDelete).toHaveBeenCalledWith({ _id: scenarioId }); + expect(MockScenarioDataModel.findOneAndDelete).toHaveBeenCalledWith({ _id: objectId }); }); it('should throw an error if an error occurs while deleting scenario data', async () => { const scenarioId = testObjectId.toString(); + const objectId = new Types.ObjectId(scenarioId); const errorMessage = 'DB Error'; - - MockScenarioDataModel.findOneAndDelete = jest - .fn() - .mockReturnValue({ exec: jest.fn().mockRejectedValue(new Error(errorMessage)) }); - - await expect(async () => { - await service.deleteScenarioData(scenarioId); - }).rejects.toThrowError(`Failed to delete scenario data: ${errorMessage}`); - - expect(MockScenarioDataModel.findOneAndDelete).toHaveBeenCalledWith({ _id: scenarioId }); + MockScenarioDataModel.findOneAndDelete = jest.fn().mockRejectedValue(new Error(errorMessage)); + await expect(service.deleteScenarioData(scenarioId)).rejects.toThrowError( + `Failed to delete scenario data: ${errorMessage}`, + ); + expect(MockScenarioDataModel.findOneAndDelete).toHaveBeenCalledWith({ _id: objectId }); }); }); diff --git a/src/api/scenarioData/scenarioData.service.ts b/src/api/scenarioData/scenarioData.service.ts index 7d47f6c..611fee3 100644 --- a/src/api/scenarioData/scenarioData.service.ts +++ b/src/api/scenarioData/scenarioData.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; -import { Model } from 'mongoose'; +import { Model, Types } from 'mongoose'; import { ScenarioData, ScenarioDataDocument } from './scenarioData.schema'; @Injectable() @@ -68,10 +68,12 @@ export class ScenarioDataService { async deleteScenarioData(scenarioId: string): Promise { try { - const deletedScenarioData = await this.scenarioDataModel.findOneAndDelete({ _id: scenarioId }).exec(); + const objectId = new Types.ObjectId(scenarioId); + const deletedScenarioData = await this.scenarioDataModel.findOneAndDelete({ _id: objectId }); if (!deletedScenarioData) { throw new Error('Scenario data not found'); } + return; } catch (error) { throw new Error(`Failed to delete scenario data: ${error.message}`); } From 042a019cc9def9b1aeff5cd0d0c5b4b82547679f Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Tue, 18 Jun 2024 15:14:58 -0700 Subject: [PATCH 07/52] Add bulk scenario assessment function. --- .../scenarioData/scenarioData.controller.ts | 15 ++- src/api/scenarioData/scenarioData.service.ts | 110 +++++++++++++++++- 2 files changed, 122 insertions(+), 3 deletions(-) diff --git a/src/api/scenarioData/scenarioData.controller.ts b/src/api/scenarioData/scenarioData.controller.ts index 4d74b36..ebcdf8e 100644 --- a/src/api/scenarioData/scenarioData.controller.ts +++ b/src/api/scenarioData/scenarioData.controller.ts @@ -1,4 +1,5 @@ -import { Controller, Get, Param, Post, Body, Put, Delete, HttpException, HttpStatus } from '@nestjs/common'; +import { Controller, Get, Param, Post, Body, Put, Delete, HttpException, HttpStatus, Res } from '@nestjs/common'; +import { Response } from 'express'; import { ScenarioDataService } from './scenarioData.service'; import { ScenarioData } from './scenarioData.schema'; @@ -71,4 +72,16 @@ export class ScenarioDataController { throw new HttpException('Error deleting scenario data', HttpStatus.INTERNAL_SERVER_ERROR); } } + + @Get('/evaluation/:goRulesJSONFilename') + async getCSVForRuleRun(@Param('goRulesJSONFilename') goRulesJSONFilename: string, @Res() res: Response) { + const fileContent = await this.scenarioDataService.getCSVForRuleRun(goRulesJSONFilename); + try { + res.setHeader('Content-Type', 'text/csv'); + res.setHeader('Content-Disposition', `attachment; filename=${goRulesJSONFilename.replace(/\.json$/, '.csv')}`); + res.send(fileContent); + } catch (error) { + throw new HttpException('Error getting scenarios by rule ID', HttpStatus.INTERNAL_SERVER_ERROR); + } + } } diff --git a/src/api/scenarioData/scenarioData.service.ts b/src/api/scenarioData/scenarioData.service.ts index 611fee3..8551ace 100644 --- a/src/api/scenarioData/scenarioData.service.ts +++ b/src/api/scenarioData/scenarioData.service.ts @@ -2,10 +2,21 @@ import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model, Types } from 'mongoose'; import { ScenarioData, ScenarioDataDocument } from './scenarioData.schema'; - +import { DecisionsService } from '../decisions/decisions.service'; +import { RuleMappingService } from '../ruleMapping/ruleMapping.service'; +import { ConfigService } from '@nestjs/config'; @Injectable() export class ScenarioDataService { - constructor(@InjectModel(ScenarioData.name) private scenarioDataModel: Model) {} + // constructor(@InjectModel(ScenarioData.name) private scenarioDataModel: Model) {} + rulesDirectory: string; + constructor( + private decisionsService: DecisionsService, + private ruleMappingService: RuleMappingService, + private configService: ConfigService, + @InjectModel(ScenarioData.name) private scenarioDataModel: Model, + ) { + this.rulesDirectory = this.configService.get('RULES_DIRECTORY'); + } async getAllScenarioData(): Promise { try { @@ -94,4 +105,99 @@ export class ScenarioDataService { throw new Error(`Error getting scenarios by filename: ${error.message}`); } } + + async runDecisionsForScenarios(goRulesJSONFilename: string): Promise<{ [scenarioId: string]: any }> { + const scenarios = await this.getScenariosByFilename(goRulesJSONFilename); + const ruleSchema = await this.ruleMappingService.ruleSchemaFile(goRulesJSONFilename); + const results: { [scenarioId: string]: any } = {}; + + const getPropertyById = (id: string, type: 'input' | 'output') => { + const schema = type === 'input' ? ruleSchema.inputs : ruleSchema.finalOutputs; + const item = schema.find((item: any) => item.id === id); + return item ? item.property : null; + }; + + for (const scenario of scenarios) { + const variablesObject = scenario?.variables?.reduce((acc: any, obj: any) => { + acc[obj.name] = obj.value; + return acc; + }, {}); + + try { + const decisionResult = await this.decisionsService.runDecisionByFile( + scenario.goRulesJSONFilename, + variablesObject, + { trace: true }, + ); + + // Map inputs and outputs based on the trace + const scenarioResult = { inputs: {}, outputs: {} }; + for (const trace of Object.values(decisionResult.trace)) { + // Map inputs + if (trace.input) { + for (const [key, value] of Object.entries(trace.input)) { + const property = getPropertyById(key, 'input'); + if (property) { + scenarioResult.inputs[property] = value; + } else { + // Direct match without id + const directMatch = ruleSchema.inputs.find((input: any) => input.property === key); + if (directMatch) { + scenarioResult.inputs[directMatch.property] = value; + } + } + } + } + + // Map outputs + if (trace.output) { + for (const [key, value] of Object.entries(trace.output)) { + const property = getPropertyById(key, 'output'); + if (property) { + scenarioResult.outputs[property] = value; + } else { + // Direct match without id + const directMatch = ruleSchema.finalOutputs.find((output: any) => output.property === key); + if (directMatch) { + scenarioResult.outputs[directMatch.property] = value; + } + } + } + } + } + + results[scenario.title.toString()] = scenarioResult; + } catch (error) { + // Handle errors if needed + console.error(`Error running decision for scenario ${scenario._id}: ${error.message}`); + results[scenario._id.toString()] = { error: error.message }; + } + } + + return results; + } + + async getCSVForRuleRun(goRulesJSONFilename: string): Promise { + const ruleRunResults = await this.runDecisionsForScenarios(goRulesJSONFilename); + const inputKeys = Array.from( + new Set(Object.values(ruleRunResults).flatMap((scenario) => Object.keys(scenario.inputs))), + ); + const outputKeys = Array.from( + new Set(Object.values(ruleRunResults).flatMap((scenario) => Object.keys(scenario.outputs))), + ); + + const headers = [ + 'Scenario', + ...inputKeys.map((key) => `Input: ${key}`), + ...outputKeys.map((key) => `Output: ${key}`), + ]; + const rows = Object.entries(ruleRunResults).map(([scenarioName, scenarioData]) => [ + scenarioName, + ...inputKeys.map((key) => (scenarioData.inputs[key] !== undefined ? scenarioData.inputs[key] : '')), + ...outputKeys.map((key) => (scenarioData.outputs[key] !== undefined ? scenarioData.outputs[key] : '')), + ]); + + const csvContent = [headers, ...rows].map((row) => row.join(',')).join('\n'); + return csvContent; + } } From d56301960cac1ac48945e228df1d5ec55763fd37 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Wed, 19 Jun 2024 09:38:57 -0700 Subject: [PATCH 08/52] Remove unused components and update testing. --- src/api/scenarioData/scenarioData.service.spec.ts | 13 +++++++++++++ src/api/scenarioData/scenarioData.service.ts | 9 ++------- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/api/scenarioData/scenarioData.service.spec.ts b/src/api/scenarioData/scenarioData.service.spec.ts index 5d47c9c..b9b2ecf 100644 --- a/src/api/scenarioData/scenarioData.service.spec.ts +++ b/src/api/scenarioData/scenarioData.service.spec.ts @@ -3,6 +3,10 @@ import { ScenarioDataService } from './scenarioData.service'; import { getModelToken } from '@nestjs/mongoose'; import { Types, Model } from 'mongoose'; import { ScenarioData, ScenarioDataDocument } from './scenarioData.schema'; +import { DecisionsService } from '../decisions/decisions.service'; +import { RuleMappingService } from '../ruleMapping/ruleMapping.service'; +import { ConfigService } from '@nestjs/config'; +import { DocumentsService } from '../documents/documents.service'; describe('ScenarioDataService', () => { let service: ScenarioDataService; @@ -29,11 +33,20 @@ describe('ScenarioDataService', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ + DecisionsService, + RuleMappingService, ScenarioDataService, { provide: getModelToken(ScenarioData.name), useValue: MockScenarioDataModel, }, + DocumentsService, + { + provide: ConfigService, + useValue: { + get: jest.fn().mockReturnValue('mocked_value'), // Replace with your mocked config values + }, + }, ], }).compile(); diff --git a/src/api/scenarioData/scenarioData.service.ts b/src/api/scenarioData/scenarioData.service.ts index 8551ace..235feb6 100644 --- a/src/api/scenarioData/scenarioData.service.ts +++ b/src/api/scenarioData/scenarioData.service.ts @@ -4,19 +4,14 @@ import { Model, Types } from 'mongoose'; import { ScenarioData, ScenarioDataDocument } from './scenarioData.schema'; import { DecisionsService } from '../decisions/decisions.service'; import { RuleMappingService } from '../ruleMapping/ruleMapping.service'; -import { ConfigService } from '@nestjs/config'; @Injectable() export class ScenarioDataService { - // constructor(@InjectModel(ScenarioData.name) private scenarioDataModel: Model) {} - rulesDirectory: string; constructor( private decisionsService: DecisionsService, private ruleMappingService: RuleMappingService, - private configService: ConfigService, + @InjectModel(ScenarioData.name) private scenarioDataModel: Model, - ) { - this.rulesDirectory = this.configService.get('RULES_DIRECTORY'); - } + ) {} async getAllScenarioData(): Promise { try { From f719bde0e170a5a2724a617dc2c4c218f8d69fac Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Wed, 19 Jun 2024 10:37:06 -0700 Subject: [PATCH 09/52] Update testing with run decisionsforscenarios addition. --- .../scenarioData/scenarioData.service.spec.ts | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/src/api/scenarioData/scenarioData.service.spec.ts b/src/api/scenarioData/scenarioData.service.spec.ts index b9b2ecf..1524f24 100644 --- a/src/api/scenarioData/scenarioData.service.spec.ts +++ b/src/api/scenarioData/scenarioData.service.spec.ts @@ -7,9 +7,12 @@ import { DecisionsService } from '../decisions/decisions.service'; import { RuleMappingService } from '../ruleMapping/ruleMapping.service'; import { ConfigService } from '@nestjs/config'; import { DocumentsService } from '../documents/documents.service'; +import { ZenEngine, ZenEngineResponse } from '@gorules/zen-engine'; describe('ScenarioDataService', () => { let service: ScenarioDataService; + let decisionsService: DecisionsService; + let ruleMappingService: RuleMappingService; let model: Model; const testObjectId = new Types.ObjectId(); @@ -51,6 +54,8 @@ describe('ScenarioDataService', () => { }).compile(); service = module.get(ScenarioDataService); + decisionsService = module.get(DecisionsService); + ruleMappingService = module.get(RuleMappingService); model = module.get>(getModelToken(ScenarioData.name)); }); @@ -267,4 +272,138 @@ describe('ScenarioDataService', () => { expect(MockScenarioDataModel.find).toHaveBeenCalledWith({ goRulesJSONFilename }); }); }); + describe('runDecisionsForScenarios', () => { + it('should run decisions for scenarios and map inputs/outputs correctly', async () => { + const goRulesJSONFilename = 'test.json'; + const scenarios = [ + { + _id: testObjectId, + title: 'Scenario 1', + variables: [{ name: 'familyComposition', value: 'single' }], + ruleID: 'ruleID', + goRulesJSONFilename: 'test.json', + }, + { + _id: testObjectId, + title: 'Scenario 2', + variables: [{ name: 'numberOfChildren', value: 2 }], + ruleID: 'ruleID', + goRulesJSONFilename: 'test.json', + }, + ]; + const ruleSchema = { + inputs: [ + { id: 'id1', name: 'Family Composition', property: 'familyComposition' }, + { id: 'id2', name: 'Number of Children', property: 'numberOfChildren' }, + ], + finalOutputs: [ + { id: 'id3', name: 'Is Eligible', property: 'isEligible' }, + { id: 'id4', name: 'Base Amount', property: 'baseAmount' }, + ], + }; + const decisionResult = { + performance: '0.7', + result: { status: 'pass' }, + trace: { + trace1: { + id: 'trace1', + name: 'trace1', + input: { familyComposition: 'single' }, + output: { isEligible: true }, + }, + trace2: { + id: 'trace2', + name: 'trace2', + input: { numberOfChildren: 2 }, + output: { baseAmount: 100 }, + performance: '0.7', + }, + }, + }; + + jest.spyOn(service, 'getScenariosByFilename').mockResolvedValue(scenarios); + jest.spyOn(ruleMappingService, 'ruleSchemaFile').mockResolvedValue(ruleSchema); + jest.spyOn(decisionsService, 'runDecisionByFile').mockResolvedValue(decisionResult); + + const results = await service.runDecisionsForScenarios(goRulesJSONFilename); + + expect(results).toEqual({ + 'Scenario 1': { + inputs: { familyComposition: 'single', numberOfChildren: 2 }, + outputs: { baseAmount: 100, isEligible: true }, + }, + 'Scenario 2': { + inputs: { familyComposition: 'single', numberOfChildren: 2 }, + outputs: { baseAmount: 100, isEligible: true }, + }, + }); + }); + it('should handle errors in decision execution', async () => { + const goRulesJSONFilename = 'test.json'; + const scenarios = [ + { + _id: testObjectId, + title: 'Scenario 1', + variables: [{ name: 'familyComposition', value: 'single' }], + ruleID: 'ruleID', + goRulesJSONFilename: 'test.json', + }, + ]; + const ruleSchema = { + inputs: [{ id: 'id1', name: 'Family Composition', property: 'familyComposition' }], + finalOutputs: [{ id: 'id3', name: 'Is Eligible', property: 'isEligible' }], + }; + + jest.spyOn(service, 'getScenariosByFilename').mockResolvedValue(scenarios); + jest.spyOn(ruleMappingService, 'ruleSchemaFile').mockResolvedValue(ruleSchema); + jest.spyOn(decisionsService, 'runDecisionByFile').mockRejectedValue(new Error('Decision execution error')); + + const results = await service.runDecisionsForScenarios(goRulesJSONFilename); + expect(results).toEqual({ + [testObjectId.toString()]: { error: 'Decision execution error' }, + }); + }); + + it('should handle scenarios with no variables', async () => { + const goRulesJSONFilename = 'test.json'; + const scenarios = [ + { + _id: testObjectId, + title: 'Scenario 1', + variables: [], + ruleID: 'ruleID', + goRulesJSONFilename: 'test.json', + }, + ]; + const ruleSchema = { + inputs: [], + finalOutputs: [{ id: 'id3', name: 'Is Eligible', property: 'isEligible' }], + }; + const decisionResult = { + performance: '0.7', + result: { status: 'pass' }, + trace: { + trace1: { + id: 'trace1', + name: 'trace1', + input: {}, + output: { isEligible: true }, + }, + }, + }; + + jest.spyOn(service, 'getScenariosByFilename').mockResolvedValue(scenarios); + jest.spyOn(ruleMappingService, 'ruleSchemaFile').mockResolvedValue(ruleSchema); + jest.spyOn(decisionsService, 'runDecisionByFile').mockResolvedValue(decisionResult); + + const results = await service.runDecisionsForScenarios(goRulesJSONFilename); + + expect(results).toEqual({ + 'Scenario 1': { + inputs: {}, + outputs: { isEligible: true }, + }, + }); + }); + }); }); From e565df9675ad270bce8f402ec96c812c4ae4c608 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Wed, 19 Jun 2024 11:08:51 -0700 Subject: [PATCH 10/52] Update testing with run getcsvforrulerun addition. --- .../scenarioData/scenarioData.service.spec.ts | 96 ++++++++++++++++++- 1 file changed, 95 insertions(+), 1 deletion(-) diff --git a/src/api/scenarioData/scenarioData.service.spec.ts b/src/api/scenarioData/scenarioData.service.spec.ts index 1524f24..df61512 100644 --- a/src/api/scenarioData/scenarioData.service.spec.ts +++ b/src/api/scenarioData/scenarioData.service.spec.ts @@ -7,7 +7,6 @@ import { DecisionsService } from '../decisions/decisions.service'; import { RuleMappingService } from '../ruleMapping/ruleMapping.service'; import { ConfigService } from '@nestjs/config'; import { DocumentsService } from '../documents/documents.service'; -import { ZenEngine, ZenEngineResponse } from '@gorules/zen-engine'; describe('ScenarioDataService', () => { let service: ScenarioDataService; @@ -406,4 +405,99 @@ describe('ScenarioDataService', () => { }); }); }); + + describe('getCSVForRuleRun', () => { + it('should generate a CSV with correct headers and data', async () => { + const goRulesJSONFilename = 'test.json'; + const ruleRunResults = { + 'Scenario 1': { + inputs: { familyComposition: 'single', numberOfChildren: 2 }, + outputs: { isEligible: true, baseAmount: 100 }, + }, + 'Scenario 2': { + inputs: { familyComposition: 'couple', numberOfChildren: 3 }, + outputs: { isEligible: false, baseAmount: 200 }, + }, + }; + + jest.spyOn(service, 'runDecisionsForScenarios').mockResolvedValue(ruleRunResults); + + const csvContent = await service.getCSVForRuleRun(goRulesJSONFilename); + + const expectedCsvContent = `Scenario,Input: familyComposition,Input: numberOfChildren,Output: isEligible,Output: baseAmount\nScenario 1,single,2,true,100\nScenario 2,couple,3,false,200`; + + expect(csvContent.trim()).toBe(expectedCsvContent.trim()); + }); + + it('should generate a CSV with missing inputs/outputs filled as empty strings', async () => { + const goRulesJSONFilename = 'test.json'; + const ruleRunResults = { + 'Scenario 1': { + inputs: { familyComposition: 'single' }, + outputs: { isEligible: true }, + }, + 'Scenario 2': { + inputs: { familyComposition: 'couple', numberOfChildren: 3 }, + outputs: { baseAmount: 200 }, + }, + }; + + jest.spyOn(service, 'runDecisionsForScenarios').mockResolvedValue(ruleRunResults); + + const csvContent = await service.getCSVForRuleRun(goRulesJSONFilename); + + const expectedCsvContent = `Scenario,Input: familyComposition,Input: numberOfChildren,Output: isEligible,Output: baseAmount\nScenario 1,single,,true,\nScenario 2,couple,3,,200`; + + expect(csvContent.trim()).toBe(expectedCsvContent.trim()); + }); + + it('should generate a CSV with only one scenario', async () => { + const goRulesJSONFilename = 'test.json'; + const ruleRunResults = { + 'Scenario 1': { + inputs: { familyComposition: 'single', numberOfChildren: 2 }, + outputs: { isEligible: true, baseAmount: 100 }, + }, + }; + + jest.spyOn(service, 'runDecisionsForScenarios').mockResolvedValue(ruleRunResults); + + const csvContent = await service.getCSVForRuleRun(goRulesJSONFilename); + + const expectedCsvContent = `Scenario,Input: familyComposition,Input: numberOfChildren,Output: isEligible,Output: baseAmount\nScenario 1,single,2,true,100`; + + expect(csvContent.trim()).toBe(expectedCsvContent.trim()); + }); + + it('should generate an empty CSV if no scenarios are present', async () => { + const goRulesJSONFilename = 'test.json'; + const ruleRunResults = {}; + + jest.spyOn(service, 'runDecisionsForScenarios').mockResolvedValue(ruleRunResults); + + const csvContent = await service.getCSVForRuleRun(goRulesJSONFilename); + + const expectedCsvContent = `Scenario`; + + expect(csvContent.trim()).toBe(expectedCsvContent.trim()); + }); + + it('should handle scenarios with no variables or outputs', async () => { + const goRulesJSONFilename = 'test.json'; + const ruleRunResults = { + 'Scenario 1': { + inputs: {}, + outputs: {}, + }, + }; + + jest.spyOn(service, 'runDecisionsForScenarios').mockResolvedValue(ruleRunResults); + + const csvContent = await service.getCSVForRuleRun(goRulesJSONFilename); + + const expectedCsvContent = `Scenario\nScenario 1`; + + expect(csvContent.trim()).toBe(expectedCsvContent.trim()); + }); + }); }); From f0146d819ec9c5259aa00a5cfdf90a76a209c418 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Wed, 19 Jun 2024 12:04:31 -0700 Subject: [PATCH 11/52] Update testing with csvrulerun controller. --- .../scenarioData.controller.spec.ts | 51 ++++++++++++++++++- .../scenarioData/scenarioData.controller.ts | 4 +- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/src/api/scenarioData/scenarioData.controller.spec.ts b/src/api/scenarioData/scenarioData.controller.spec.ts index 54c4bde..b43d490 100644 --- a/src/api/scenarioData/scenarioData.controller.spec.ts +++ b/src/api/scenarioData/scenarioData.controller.spec.ts @@ -2,7 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { ScenarioDataController } from './scenarioData.controller'; import { ScenarioDataService } from './scenarioData.service'; import { ScenarioData } from './scenarioData.schema'; -import { HttpException } from '@nestjs/common'; +import { HttpException, HttpStatus } from '@nestjs/common'; import { Types } from 'mongoose'; describe('ScenarioDataController', () => { @@ -19,6 +19,7 @@ describe('ScenarioDataController', () => { createScenarioData: jest.fn(), updateScenarioData: jest.fn(), deleteScenarioData: jest.fn(), + getCSVForRuleRun: jest.fn(), }; beforeEach(async () => { @@ -171,4 +172,52 @@ describe('ScenarioDataController', () => { await expect(controller.deleteScenarioData(testObjectId.toHexString())).rejects.toThrow(HttpException); }); }); + + describe('getCSVForRuleRun', () => { + it('should return CSV content with correct headers', async () => { + const goRulesJSONFilename = 'test.json'; + const csvContent = `Scenario,Input: familyComposition,Input: numberOfChildren,Output: isEligible,Output: baseAmount +Scenario 1,single,,true, +Scenario 2,couple,3,,200`; + + jest.spyOn(service, 'getCSVForRuleRun').mockResolvedValue(csvContent); + + const mockResponse = { + status: jest.fn().mockReturnThis(), + send: jest.fn(), + setHeader: jest.fn(), + }; + + await controller.getCSVForRuleRun(goRulesJSONFilename, mockResponse as any); + + expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.OK); + expect(mockResponse.setHeader).toHaveBeenCalledWith('Content-Type', 'text/csv'); + expect(mockResponse.setHeader).toHaveBeenCalledWith( + 'Content-Disposition', + `attachment; filename=${goRulesJSONFilename.replace(/\.json$/, '.csv')}`, + ); + expect(mockResponse.send).toHaveBeenCalledWith(csvContent); + }); + + it('should throw an error if service fails', async () => { + const errorMessage = 'Error generating CSV for rule run'; + const goRulesJSONFilename = 'test.json'; + jest.spyOn(service, 'getCSVForRuleRun').mockRejectedValue(new Error(errorMessage)); + + const mockResponse = { + status: jest.fn().mockReturnThis(), + send: jest.fn(), + }; + + await expect(async () => { + await controller.getCSVForRuleRun(goRulesJSONFilename, mockResponse as any); + }).rejects.toThrow(Error); + + try { + await controller.getCSVForRuleRun(goRulesJSONFilename, mockResponse as any); + } catch (error) { + expect(error.message).toBe('Error generating CSV for rule run'); + } + }); + }); }); diff --git a/src/api/scenarioData/scenarioData.controller.ts b/src/api/scenarioData/scenarioData.controller.ts index ebcdf8e..a58d12b 100644 --- a/src/api/scenarioData/scenarioData.controller.ts +++ b/src/api/scenarioData/scenarioData.controller.ts @@ -79,9 +79,9 @@ export class ScenarioDataController { try { res.setHeader('Content-Type', 'text/csv'); res.setHeader('Content-Disposition', `attachment; filename=${goRulesJSONFilename.replace(/\.json$/, '.csv')}`); - res.send(fileContent); + res.status(HttpStatus.OK).send(fileContent); } catch (error) { - throw new HttpException('Error getting scenarios by rule ID', HttpStatus.INTERNAL_SERVER_ERROR); + throw new HttpException('Error generating CSV for rule run', HttpStatus.INTERNAL_SERVER_ERROR); } } } From 4b0f41dd99620d9bb6149338a3e73b774a0af97b Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Wed, 19 Jun 2024 12:18:26 -0700 Subject: [PATCH 12/52] Refactor and comment for clarity of complex scenario functions. --- src/api/scenarioData/scenarioData.service.ts | 75 +++++++++++--------- 1 file changed, 41 insertions(+), 34 deletions(-) diff --git a/src/api/scenarioData/scenarioData.service.ts b/src/api/scenarioData/scenarioData.service.ts index 235feb6..5ec8eb6 100644 --- a/src/api/scenarioData/scenarioData.service.ts +++ b/src/api/scenarioData/scenarioData.service.ts @@ -101,6 +101,11 @@ export class ScenarioDataService { } } + /** + * Runs decisions for multiple scenarios based on the provided rules JSON file. + * Retrieves scenarios, retrieves rule schema, and executes decisions for each scenario. + * Maps inputs and outputs from decision traces to structured results. + */ async runDecisionsForScenarios(goRulesJSONFilename: string): Promise<{ [scenarioId: string]: any }> { const scenarios = await this.getScenariosByFilename(goRulesJSONFilename); const ruleSchema = await this.ruleMappingService.ruleSchemaFile(goRulesJSONFilename); @@ -112,6 +117,26 @@ export class ScenarioDataService { return item ? item.property : null; }; + const mapTraceToResult = (trace: any, type: 'input' | 'output') => { + const result: { [key: string]: any } = {}; + const schema = type === 'input' ? ruleSchema.inputs : ruleSchema.finalOutputs; + + for (const [key, value] of Object.entries(trace)) { + const property = getPropertyById(key, type); + if (property) { + result[property] = value; + } else { + // Direct match without id + const directMatch = schema.find((item: any) => item.property === key); + if (directMatch) { + result[directMatch.property] = value; + } + } + } + + return result; + }; + for (const scenario of scenarios) { const variablesObject = scenario?.variables?.reduce((acc: any, obj: any) => { acc[obj.name] = obj.value; @@ -125,45 +150,20 @@ export class ScenarioDataService { { trace: true }, ); - // Map inputs and outputs based on the trace const scenarioResult = { inputs: {}, outputs: {} }; + + // Map inputs and outputs based on the trace for (const trace of Object.values(decisionResult.trace)) { - // Map inputs if (trace.input) { - for (const [key, value] of Object.entries(trace.input)) { - const property = getPropertyById(key, 'input'); - if (property) { - scenarioResult.inputs[property] = value; - } else { - // Direct match without id - const directMatch = ruleSchema.inputs.find((input: any) => input.property === key); - if (directMatch) { - scenarioResult.inputs[directMatch.property] = value; - } - } - } + Object.assign(scenarioResult.inputs, mapTraceToResult(trace.input, 'input')); } - - // Map outputs if (trace.output) { - for (const [key, value] of Object.entries(trace.output)) { - const property = getPropertyById(key, 'output'); - if (property) { - scenarioResult.outputs[property] = value; - } else { - // Direct match without id - const directMatch = ruleSchema.finalOutputs.find((output: any) => output.property === key); - if (directMatch) { - scenarioResult.outputs[directMatch.property] = value; - } - } - } + Object.assign(scenarioResult.outputs, mapTraceToResult(trace.output, 'output')); } } results[scenario.title.toString()] = scenarioResult; } catch (error) { - // Handle errors if needed console.error(`Error running decision for scenario ${scenario._id}: ${error.message}`); results[scenario._id.toString()] = { error: error.message }; } @@ -172,6 +172,11 @@ export class ScenarioDataService { return results; } + /** + * Generates a CSV string based on the results of running decisions for scenarios. + * Retrieves scenario results, extracts unique input and output keys, and maps them to CSV rows. + * Constructs CSV headers and rows based on input and output keys. + */ async getCSVForRuleRun(goRulesJSONFilename: string): Promise { const ruleRunResults = await this.runDecisionsForScenarios(goRulesJSONFilename); const inputKeys = Array.from( @@ -186,11 +191,13 @@ export class ScenarioDataService { ...inputKeys.map((key) => `Input: ${key}`), ...outputKeys.map((key) => `Output: ${key}`), ]; - const rows = Object.entries(ruleRunResults).map(([scenarioName, scenarioData]) => [ - scenarioName, - ...inputKeys.map((key) => (scenarioData.inputs[key] !== undefined ? scenarioData.inputs[key] : '')), - ...outputKeys.map((key) => (scenarioData.outputs[key] !== undefined ? scenarioData.outputs[key] : '')), - ]); + const rows = Object.entries(ruleRunResults).map(([scenarioName, scenarioData]) => { + const inputs = inputKeys.map((key) => (scenarioData.inputs[key] !== undefined ? scenarioData.inputs[key] : '')); + const outputs = outputKeys.map((key) => + scenarioData.outputs[key] !== undefined ? scenarioData.outputs[key] : '', + ); + return [scenarioName, ...inputs, ...outputs]; + }); const csvContent = [headers, ...rows].map((row) => row.join(',')).join('\n'); return csvContent; From 3aae607bc17782962f448c9423da936aac82c2df Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Wed, 19 Jun 2024 12:20:48 -0700 Subject: [PATCH 13/52] Remove unused function. --- src/api/scenarioData/scenarioData.service.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/api/scenarioData/scenarioData.service.ts b/src/api/scenarioData/scenarioData.service.ts index 5ec8eb6..d7b17b2 100644 --- a/src/api/scenarioData/scenarioData.service.ts +++ b/src/api/scenarioData/scenarioData.service.ts @@ -34,20 +34,6 @@ export class ScenarioDataService { } } - isValidVariableStructure(variables: any[]): boolean { - for (const variable of variables) { - if ( - !variable || - typeof variable.name !== 'string' || - typeof variable.type !== 'string' || - (variable.value !== null && typeof variable.value === 'undefined') - ) { - return false; - } - } - return true; - } - async createScenarioData(scenarioData: ScenarioData): Promise { try { const newScenarioData = new this.scenarioDataModel(scenarioData); From f91c20c7c8a7ff000e7b5c7058c409702249385f Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Thu, 20 Jun 2024 09:26:06 -0700 Subject: [PATCH 14/52] Add csv input processing of scenarios. --- package-lock.json | 125 +++++++++++++++++- package.json | 5 +- .../scenarioData/scenarioData.controller.ts | 55 +++++++- src/api/scenarioData/scenarioData.service.ts | 87 +++++++++++- 4 files changed, 258 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index 63059a1..21e0169 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,11 +14,14 @@ "@nestjs/config": "^3.2.0", "@nestjs/core": "^10.0.0", "@nestjs/mongoose": "^10.0.5", - "@nestjs/platform-express": "^10.3.7", + "@nestjs/platform-express": "^10.3.9", "axios": "^1.6.8", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "csv-parser": "^3.0.0", + "fast-csv": "^5.0.1", "mongoose": "^8.3.0", + "multer": "^1.4.5-lts.1", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1" }, @@ -1021,6 +1024,31 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@fast-csv/format": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-5.0.0.tgz", + "integrity": "sha512-IyMpHwYIOGa2f0BJi6Wk55UF0oBA5urdIydoEDYxPo88LFbeb3Yr4rgpu98OAO1glUWheSnNtUgS80LE+/dqmw==", + "dependencies": { + "lodash.escaperegexp": "^4.1.2", + "lodash.isboolean": "^3.0.3", + "lodash.isequal": "^4.5.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0" + } + }, + "node_modules/@fast-csv/parse": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@fast-csv/parse/-/parse-5.0.0.tgz", + "integrity": "sha512-ecF8tCm3jVxeRjEB6VPzmA+1wGaJ5JgaUX2uesOXdXD6qQp0B3EdshOIed4yT1Xlj/F2f8v4zHSo0Oi31L697g==", + "dependencies": { + "lodash.escaperegexp": "^4.1.2", + "lodash.groupby": "^4.6.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0", + "lodash.isundefined": "^3.0.1", + "lodash.uniq": "^4.5.0" + } + }, "node_modules/@gorules/zen-engine": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@gorules/zen-engine/-/zen-engine-0.23.0.tgz", @@ -1985,9 +2013,9 @@ } }, "node_modules/@nestjs/platform-express": { - "version": "10.3.7", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.7.tgz", - "integrity": "sha512-noNJ+PyIxQJLCKfuXz0tcQtlVAynfLIuKy62g70lEZ86UrIqSrZFqvWs/rFUgkbT6J8H7Rmv11hASOnX+7M2rA==", + "version": "10.3.9", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.9.tgz", + "integrity": "sha512-si/UzobP6YUtYtCT1cSyQYHHzU3yseqYT6l7OHSMVvfG1+TqxaAqI6nmrix02LO+l1YntHRXEs3p+v9a7EfrSQ==", "dependencies": { "body-parser": "1.20.2", "cors": "2.8.5", @@ -2004,6 +2032,23 @@ "@nestjs/core": "^10.0.0" } }, + "node_modules/@nestjs/platform-express/node_modules/multer": { + "version": "1.4.4-lts.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4-lts.1.tgz", + "integrity": "sha512-WeSGziVj6+Z2/MwQo3GvqzgR+9Uc+qt8SwHKh3gvNPiISKfsMfG4SvCOFYlxxgkXt7yIV2i1yczehm0EOKIxIg==", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/@nestjs/schematics": { "version": "10.1.1", "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.1.1.tgz", @@ -3861,6 +3906,20 @@ "node": ">= 8" } }, + "node_modules/csv-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/csv-parser/-/csv-parser-3.0.0.tgz", + "integrity": "sha512-s6OYSXAK3IdKqYO33y09jhypG/bSDHPuyCme/IdEHfWpLf/jKcpitVFyOC6UemgGk8v7Q5u2XE0vvwmanxhGlQ==", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "csv-parser": "bin/csv-parser" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -4552,6 +4611,18 @@ "node": ">=4" } }, + "node_modules/fast-csv": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/fast-csv/-/fast-csv-5.0.1.tgz", + "integrity": "sha512-Q43zC4NdQD5MAWOVQOF8KA+D6ddvTJjX2ib8zqysm74jZhtk6+dc8C75/OqRV6Y9CLc4kgvbC3PLG8YL4YZfgw==", + "dependencies": { + "@fast-csv/format": "5.0.0", + "@fast-csv/parse": "5.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -6434,6 +6505,41 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==" + }, + "node_modules/lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" + }, + "node_modules/lodash.isfunction": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", + "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==" + }, + "node_modules/lodash.isnil": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz", + "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==" + }, + "node_modules/lodash.isundefined": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", + "integrity": "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -6446,6 +6552,11 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==" + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -6809,9 +6920,9 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/multer": { - "version": "1.4.4-lts.1", - "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4-lts.1.tgz", - "integrity": "sha512-WeSGziVj6+Z2/MwQo3GvqzgR+9Uc+qt8SwHKh3gvNPiISKfsMfG4SvCOFYlxxgkXt7yIV2i1yczehm0EOKIxIg==", + "version": "1.4.5-lts.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", + "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", "dependencies": { "append-field": "^1.0.0", "busboy": "^1.0.0", diff --git a/package.json b/package.json index 30cb3d1..143336a 100644 --- a/package.json +++ b/package.json @@ -26,11 +26,14 @@ "@nestjs/config": "^3.2.0", "@nestjs/core": "^10.0.0", "@nestjs/mongoose": "^10.0.5", - "@nestjs/platform-express": "^10.3.7", + "@nestjs/platform-express": "^10.3.9", "axios": "^1.6.8", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "csv-parser": "^3.0.0", + "fast-csv": "^5.0.1", "mongoose": "^8.3.0", + "multer": "^1.4.5-lts.1", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1" }, diff --git a/src/api/scenarioData/scenarioData.controller.ts b/src/api/scenarioData/scenarioData.controller.ts index a58d12b..a55a8b2 100644 --- a/src/api/scenarioData/scenarioData.controller.ts +++ b/src/api/scenarioData/scenarioData.controller.ts @@ -1,4 +1,20 @@ -import { Controller, Get, Param, Post, Body, Put, Delete, HttpException, HttpStatus, Res } from '@nestjs/common'; +import { + Controller, + Get, + Param, + Post, + Body, + Put, + Delete, + HttpException, + HttpStatus, + Res, + UploadedFile, + UseInterceptors, +} from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; +import * as csvParser from 'csv-parser'; +import * as fastCsv from 'fast-csv'; import { Response } from 'express'; import { ScenarioDataService } from './scenarioData.service'; import { ScenarioData } from './scenarioData.schema'; @@ -84,4 +100,41 @@ export class ScenarioDataController { throw new HttpException('Error generating CSV for rule run', HttpStatus.INTERNAL_SERVER_ERROR); } } + + @Get('/run-decisions/:goRulesJSONFilename') + async runDecisionsForScenarios( + @Param('goRulesJSONFilename') goRulesJSONFilename: string, + ): Promise<{ [scenarioId: string]: any }> { + try { + return await this.scenarioDataService.runDecisionsForScenarios(goRulesJSONFilename); + } catch (error) { + throw new HttpException('Error running scenario decisions', HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @Post('/evaluation/upload/:goRulesJSONFilename') + @UseInterceptors(FileInterceptor('file')) + async uploadCSVAndProcess( + @UploadedFile() file, + @Res() res: Response, + @Param('goRulesJSONFilename') goRulesJSONFilename: string, + ) { + // console.log(file); + if (!file) { + throw new HttpException('No file uploaded', HttpStatus.BAD_REQUEST); + } + + try { + const scenarios = await this.scenarioDataService.processProvidedScenarios(goRulesJSONFilename, file); + const csvContent = await this.scenarioDataService.getCSVForRuleRun(goRulesJSONFilename, scenarios); + console.log(csvContent, 'this is csv content'); + + res.setHeader('Content-Type', 'text/csv'); + res.setHeader('Content-Disposition', `attachment; filename=processed_data.csv`); + // res.status(HttpStatus.OK).send(scenarios); + res.status(HttpStatus.OK).send(csvContent); + } catch (error) { + throw new HttpException('Error processing CSV file', HttpStatus.INTERNAL_SERVER_ERROR); + } + } } diff --git a/src/api/scenarioData/scenarioData.service.ts b/src/api/scenarioData/scenarioData.service.ts index d7b17b2..0964c5b 100644 --- a/src/api/scenarioData/scenarioData.service.ts +++ b/src/api/scenarioData/scenarioData.service.ts @@ -1,9 +1,10 @@ import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model, Types } from 'mongoose'; -import { ScenarioData, ScenarioDataDocument } from './scenarioData.schema'; +import { ScenarioData, ScenarioDataDocument, Variable, VariableSchema } from './scenarioData.schema'; import { DecisionsService } from '../decisions/decisions.service'; import { RuleMappingService } from '../ruleMapping/ruleMapping.service'; +import * as csvParser from 'csv-parser'; @Injectable() export class ScenarioDataService { constructor( @@ -92,8 +93,12 @@ export class ScenarioDataService { * Retrieves scenarios, retrieves rule schema, and executes decisions for each scenario. * Maps inputs and outputs from decision traces to structured results. */ - async runDecisionsForScenarios(goRulesJSONFilename: string): Promise<{ [scenarioId: string]: any }> { - const scenarios = await this.getScenariosByFilename(goRulesJSONFilename); + async runDecisionsForScenarios( + goRulesJSONFilename: string, + newScenarios?: ScenarioData[], + ): Promise<{ [scenarioId: string]: any }> { + const scenarios = newScenarios || (await this.getScenariosByFilename(goRulesJSONFilename)); + console.log(scenarios, 'this is scenarios input'); const ruleSchema = await this.ruleMappingService.ruleSchemaFile(goRulesJSONFilename); const results: { [scenarioId: string]: any } = {}; @@ -149,6 +154,7 @@ export class ScenarioDataService { } results[scenario.title.toString()] = scenarioResult; + console.log(scenarioResult, 'scenario result'); } catch (error) { console.error(`Error running decision for scenario ${scenario._id}: ${error.message}`); results[scenario._id.toString()] = { error: error.message }; @@ -163,8 +169,8 @@ export class ScenarioDataService { * Retrieves scenario results, extracts unique input and output keys, and maps them to CSV rows. * Constructs CSV headers and rows based on input and output keys. */ - async getCSVForRuleRun(goRulesJSONFilename: string): Promise { - const ruleRunResults = await this.runDecisionsForScenarios(goRulesJSONFilename); + async getCSVForRuleRun(goRulesJSONFilename: string, newScenarios?: ScenarioData[]): Promise { + const ruleRunResults = await this.runDecisionsForScenarios(goRulesJSONFilename, newScenarios); const inputKeys = Array.from( new Set(Object.values(ruleRunResults).flatMap((scenario) => Object.keys(scenario.inputs))), ); @@ -188,4 +194,75 @@ export class ScenarioDataService { const csvContent = [headers, ...rows].map((row) => row.join(',')).join('\n'); return csvContent; } + + async processProvidedScenarios(goRulesJSONFilename: string, csvContent: string): Promise { + const parsedData = await this.parseCSV(csvContent); + const headers = parsedData[0]; + const inputKeys = headers + .filter((header) => header.startsWith('Input: ')) + .map((header) => header.replace('Input: ', '')); + const outputKeys = headers + .filter((header) => header.startsWith('Output: ')) + .map((header) => header.replace('Output: ', '')); + + const scenarios: ScenarioData[] = []; + + for (let i = 1; i < parsedData.length; i++) { + const row = parsedData[i]; + const scenarioTitle = row[0]; + + function formatValue(value: string): boolean | number | string { + // Check for boolean values + if (value.toLowerCase() === 'true') { + return true; + } else if (value.toLowerCase() === 'false') { + return false; + } + // Check for number values + const numberValue = parseFloat(value); + if (!isNaN(numberValue)) { + return numberValue; + } + // Default to string + return value; + } + + const inputs: Variable[] = inputKeys.map((key, index) => ({ + name: key, + value: formatValue(row[index + 1]), + type: typeof formatValue(row[index + 1]), + })); + + const outputs: { [key: string]: any } = {}; + outputKeys.forEach((key, index) => { + outputs[key] = row[inputKeys.length + 1 + index]; + }); + + const scenario: ScenarioData = { + _id: new Types.ObjectId(), + title: scenarioTitle, + ruleID: '', + variables: inputs, + goRulesJSONFilename: goRulesJSONFilename, + }; + + scenarios.push(scenario); + } + + return scenarios; + } + + async parseCSV(file): Promise { + return new Promise((resolve, reject) => { + const results: string[][] = []; + const stream = csvParser({ headers: false }); + + stream.on('data', (data) => results.push(Object.values(data))); + stream.on('end', () => resolve(results)); + stream.on('error', (error) => reject(error)); + + stream.write(file.buffer); + stream.end(); + }); + } } From 352ada8c02bbb650f7e2415d692279719fa2b1ec Mon Sep 17 00:00:00 2001 From: timwekkenbc Date: Thu, 20 Jun 2024 09:38:35 -0700 Subject: [PATCH 15/52] Fixed eslint issues --- .github/workflows/eslint.yml | 5 +---- src/api/documents/documents.service.spec.ts | 3 --- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml index bdb3b3e..123bac0 100644 --- a/.github/workflows/eslint.yml +++ b/.github/workflows/eslint.yml @@ -32,10 +32,7 @@ jobs: continue-on-error: true - name: Upload ESLint report - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: eslint-report path: eslint-report.html - - - name: Display ESLint report link - run: echo "::set-output name=eslint-report::${{ steps.upload.outputs.artifact_path }}" diff --git a/src/api/documents/documents.service.spec.ts b/src/api/documents/documents.service.spec.ts index 9a5fce6..6341b42 100644 --- a/src/api/documents/documents.service.spec.ts +++ b/src/api/documents/documents.service.spec.ts @@ -1,9 +1,6 @@ import { HttpException, HttpStatus } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import * as fs from 'fs'; -import * as util from 'util'; -import * as path from 'path'; import { DocumentsService } from './documents.service'; import { readFileSafely } from '../../utils/readFile'; From 8e33cb3999374180f5a4d49b6a8508706b38b6ed Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Thu, 20 Jun 2024 10:13:37 -0700 Subject: [PATCH 16/52] Add csv upload process for testing scenarios. --- src/api/scenarioData/scenarioData.controller.ts | 4 ---- src/api/scenarioData/scenarioData.service.ts | 2 -- 2 files changed, 6 deletions(-) diff --git a/src/api/scenarioData/scenarioData.controller.ts b/src/api/scenarioData/scenarioData.controller.ts index a55a8b2..260c745 100644 --- a/src/api/scenarioData/scenarioData.controller.ts +++ b/src/api/scenarioData/scenarioData.controller.ts @@ -119,7 +119,6 @@ export class ScenarioDataController { @Res() res: Response, @Param('goRulesJSONFilename') goRulesJSONFilename: string, ) { - // console.log(file); if (!file) { throw new HttpException('No file uploaded', HttpStatus.BAD_REQUEST); } @@ -127,11 +126,8 @@ export class ScenarioDataController { try { const scenarios = await this.scenarioDataService.processProvidedScenarios(goRulesJSONFilename, file); const csvContent = await this.scenarioDataService.getCSVForRuleRun(goRulesJSONFilename, scenarios); - console.log(csvContent, 'this is csv content'); - res.setHeader('Content-Type', 'text/csv'); res.setHeader('Content-Disposition', `attachment; filename=processed_data.csv`); - // res.status(HttpStatus.OK).send(scenarios); res.status(HttpStatus.OK).send(csvContent); } catch (error) { throw new HttpException('Error processing CSV file', HttpStatus.INTERNAL_SERVER_ERROR); diff --git a/src/api/scenarioData/scenarioData.service.ts b/src/api/scenarioData/scenarioData.service.ts index 0964c5b..0966b3e 100644 --- a/src/api/scenarioData/scenarioData.service.ts +++ b/src/api/scenarioData/scenarioData.service.ts @@ -98,7 +98,6 @@ export class ScenarioDataService { newScenarios?: ScenarioData[], ): Promise<{ [scenarioId: string]: any }> { const scenarios = newScenarios || (await this.getScenariosByFilename(goRulesJSONFilename)); - console.log(scenarios, 'this is scenarios input'); const ruleSchema = await this.ruleMappingService.ruleSchemaFile(goRulesJSONFilename); const results: { [scenarioId: string]: any } = {}; @@ -154,7 +153,6 @@ export class ScenarioDataService { } results[scenario.title.toString()] = scenarioResult; - console.log(scenarioResult, 'scenario result'); } catch (error) { console.error(`Error running decision for scenario ${scenario._id}: ${error.message}`); results[scenario._id.toString()] = { error: error.message }; From 795c8c6f0b27a2d327405d09e12829b1b6382aea Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Thu, 20 Jun 2024 13:07:53 -0700 Subject: [PATCH 17/52] Update with nestjs file type. --- package-lock.json | 10 ++++++++++ package.json | 1 + src/api/scenarioData/scenarioData.controller.ts | 2 +- src/api/scenarioData/scenarioData.service.ts | 2 +- 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 21e0169..78fa0f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "@nestjs/testing": "^10.0.0", "@types/express": "^4.17.17", "@types/jest": "^29.5.2", + "@types/multer": "^1.4.11", "@types/node": "^20.3.1", "@types/supertest": "^6.0.0", "@typescript-eslint/eslint-plugin": "^6.0.0", @@ -2403,6 +2404,15 @@ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "dev": true }, + "node_modules/@types/multer": { + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.11.tgz", + "integrity": "sha512-svK240gr6LVWvv3YGyhLlA+6LRRWA4mnGIU7RcNmgjBYFl6665wcXrRfxGp5tEPVHUNm5FMcmq7too9bxCwX/w==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/node": { "version": "20.11.20", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz", diff --git a/package.json b/package.json index 143336a..1003557 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "@nestjs/testing": "^10.0.0", "@types/express": "^4.17.17", "@types/jest": "^29.5.2", + "@types/multer": "^1.4.11", "@types/node": "^20.3.1", "@types/supertest": "^6.0.0", "@typescript-eslint/eslint-plugin": "^6.0.0", diff --git a/src/api/scenarioData/scenarioData.controller.ts b/src/api/scenarioData/scenarioData.controller.ts index 260c745..61a78a5 100644 --- a/src/api/scenarioData/scenarioData.controller.ts +++ b/src/api/scenarioData/scenarioData.controller.ts @@ -115,7 +115,7 @@ export class ScenarioDataController { @Post('/evaluation/upload/:goRulesJSONFilename') @UseInterceptors(FileInterceptor('file')) async uploadCSVAndProcess( - @UploadedFile() file, + @UploadedFile() file: Express.Multer.File, @Res() res: Response, @Param('goRulesJSONFilename') goRulesJSONFilename: string, ) { diff --git a/src/api/scenarioData/scenarioData.service.ts b/src/api/scenarioData/scenarioData.service.ts index 0966b3e..ca7d0c8 100644 --- a/src/api/scenarioData/scenarioData.service.ts +++ b/src/api/scenarioData/scenarioData.service.ts @@ -250,7 +250,7 @@ export class ScenarioDataService { return scenarios; } - async parseCSV(file): Promise { + async parseCSV(file: Express.Multer.File): Promise { return new Promise((resolve, reject) => { const results: string[][] = []; const stream = csvParser({ headers: false }); From 3218caf72763d5d607246b0d32f347db093698d4 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Thu, 20 Jun 2024 13:26:05 -0700 Subject: [PATCH 18/52] Update types and reduced unused function components. --- .../scenarioData/scenarioData.controller.ts | 2 +- src/api/scenarioData/scenarioData.service.ts | 78 +++++++++---------- 2 files changed, 39 insertions(+), 41 deletions(-) diff --git a/src/api/scenarioData/scenarioData.controller.ts b/src/api/scenarioData/scenarioData.controller.ts index 61a78a5..f5fa841 100644 --- a/src/api/scenarioData/scenarioData.controller.ts +++ b/src/api/scenarioData/scenarioData.controller.ts @@ -115,7 +115,7 @@ export class ScenarioDataController { @Post('/evaluation/upload/:goRulesJSONFilename') @UseInterceptors(FileInterceptor('file')) async uploadCSVAndProcess( - @UploadedFile() file: Express.Multer.File, + @UploadedFile() file: Express.Multer.File | undefined, @Res() res: Response, @Param('goRulesJSONFilename') goRulesJSONFilename: string, ) { diff --git a/src/api/scenarioData/scenarioData.service.ts b/src/api/scenarioData/scenarioData.service.ts index ca7d0c8..b1b349c 100644 --- a/src/api/scenarioData/scenarioData.service.ts +++ b/src/api/scenarioData/scenarioData.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model, Types } from 'mongoose'; -import { ScenarioData, ScenarioDataDocument, Variable, VariableSchema } from './scenarioData.schema'; +import { ScenarioData, ScenarioDataDocument, Variable } from './scenarioData.schema'; import { DecisionsService } from '../decisions/decisions.service'; import { RuleMappingService } from '../ruleMapping/ruleMapping.service'; import * as csvParser from 'csv-parser'; @@ -193,49 +193,61 @@ export class ScenarioDataService { return csvContent; } - async processProvidedScenarios(goRulesJSONFilename: string, csvContent: string): Promise { + async parseCSV(file: Express.Multer.File | undefined): Promise { + return new Promise((resolve, reject) => { + const results: string[][] = []; + const stream = csvParser({ headers: false }); + + stream.on('data', (data) => results.push(Object.values(data))); + stream.on('end', () => resolve(results)); + stream.on('error', (error) => reject(error)); + + stream.write(file.buffer); + stream.end(); + }); + } + + /** + * Processes a CSV file containing scenario data and returns an array of ScenarioData objects based on the inputs. + * @param goRulesJSONFilename The name of the Go rules JSON file. + * @param csvContent The CSV file content. + * @returns An array of ScenarioData objects. + */ + async processProvidedScenarios( + goRulesJSONFilename: string, + csvContent: Express.Multer.File, + ): Promise { const parsedData = await this.parseCSV(csvContent); const headers = parsedData[0]; const inputKeys = headers .filter((header) => header.startsWith('Input: ')) .map((header) => header.replace('Input: ', '')); - const outputKeys = headers - .filter((header) => header.startsWith('Output: ')) - .map((header) => header.replace('Output: ', '')); const scenarios: ScenarioData[] = []; + function formatValue(value: string): boolean | number | string { + if (value.toLowerCase() === 'true') { + return true; + } else if (value.toLowerCase() === 'false') { + return false; + } + const numberValue = parseFloat(value); + if (!isNaN(numberValue)) { + return numberValue; + } + return value; + } + for (let i = 1; i < parsedData.length; i++) { const row = parsedData[i]; const scenarioTitle = row[0]; - function formatValue(value: string): boolean | number | string { - // Check for boolean values - if (value.toLowerCase() === 'true') { - return true; - } else if (value.toLowerCase() === 'false') { - return false; - } - // Check for number values - const numberValue = parseFloat(value); - if (!isNaN(numberValue)) { - return numberValue; - } - // Default to string - return value; - } - const inputs: Variable[] = inputKeys.map((key, index) => ({ name: key, value: formatValue(row[index + 1]), type: typeof formatValue(row[index + 1]), })); - const outputs: { [key: string]: any } = {}; - outputKeys.forEach((key, index) => { - outputs[key] = row[inputKeys.length + 1 + index]; - }); - const scenario: ScenarioData = { _id: new Types.ObjectId(), title: scenarioTitle, @@ -249,18 +261,4 @@ export class ScenarioDataService { return scenarios; } - - async parseCSV(file: Express.Multer.File): Promise { - return new Promise((resolve, reject) => { - const results: string[][] = []; - const stream = csvParser({ headers: false }); - - stream.on('data', (data) => results.push(Object.values(data))); - stream.on('end', () => resolve(results)); - stream.on('error', (error) => reject(error)); - - stream.write(file.buffer); - stream.end(); - }); - } } From 43272436bde7e218e0f8af808aaf918fc2af487b Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Thu, 20 Jun 2024 13:27:07 -0700 Subject: [PATCH 19/52] Remove unused imports. --- src/api/scenarioData/scenarioData.controller.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/api/scenarioData/scenarioData.controller.ts b/src/api/scenarioData/scenarioData.controller.ts index f5fa841..4a5d220 100644 --- a/src/api/scenarioData/scenarioData.controller.ts +++ b/src/api/scenarioData/scenarioData.controller.ts @@ -13,8 +13,6 @@ import { UseInterceptors, } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; -import * as csvParser from 'csv-parser'; -import * as fastCsv from 'fast-csv'; import { Response } from 'express'; import { ScenarioDataService } from './scenarioData.service'; import { ScenarioData } from './scenarioData.schema'; From 25846b2b8b75b74b3f7c51484d88b72a0b48a0d0 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Thu, 20 Jun 2024 13:40:04 -0700 Subject: [PATCH 20/52] Add test for parsing csv. --- .../scenarioData/scenarioData.service.spec.ts | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/api/scenarioData/scenarioData.service.spec.ts b/src/api/scenarioData/scenarioData.service.spec.ts index df61512..2522eac 100644 --- a/src/api/scenarioData/scenarioData.service.spec.ts +++ b/src/api/scenarioData/scenarioData.service.spec.ts @@ -7,6 +7,8 @@ import { DecisionsService } from '../decisions/decisions.service'; import { RuleMappingService } from '../ruleMapping/ruleMapping.service'; import { ConfigService } from '@nestjs/config'; import { DocumentsService } from '../documents/documents.service'; +import * as csvParser from 'csv-parser'; +import { Readable } from 'stream'; describe('ScenarioDataService', () => { let service: ScenarioDataService; @@ -500,4 +502,31 @@ describe('ScenarioDataService', () => { expect(csvContent.trim()).toBe(expectedCsvContent.trim()); }); }); + + describe('parseCSV', () => { + it('should parse a CSV file and return the parsed data as a 2D array', async () => { + const fileBuffer = Buffer.from('a,b,c\n1,2,3\n4,5,6\n'); + const file: Express.Multer.File = { + buffer: fileBuffer, + fieldname: '', + originalname: '', + encoding: '', + mimetype: '', + size: 0, + stream: null, + destination: '', + filename: '', + path: '', + }; + + const expectedOutput = [ + ['a', 'b', 'c'], + ['1', '2', '3'], + ['4', '5', '6'], + ]; + + const result = await service.parseCSV(file); + expect(result).toEqual(expectedOutput); + }); + }); }); From bc4d183097dd2ebd0440e3792346a51737db3d38 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Thu, 20 Jun 2024 13:48:59 -0700 Subject: [PATCH 21/52] Update testing for processing csv input into scenarios. --- .../scenarioData/scenarioData.service.spec.ts | 202 +++++++++++++++++- 1 file changed, 201 insertions(+), 1 deletion(-) diff --git a/src/api/scenarioData/scenarioData.service.spec.ts b/src/api/scenarioData/scenarioData.service.spec.ts index 2522eac..0e6de69 100644 --- a/src/api/scenarioData/scenarioData.service.spec.ts +++ b/src/api/scenarioData/scenarioData.service.spec.ts @@ -2,7 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { ScenarioDataService } from './scenarioData.service'; import { getModelToken } from '@nestjs/mongoose'; import { Types, Model } from 'mongoose'; -import { ScenarioData, ScenarioDataDocument } from './scenarioData.schema'; +import { ScenarioData, ScenarioDataDocument, Variable } from './scenarioData.schema'; import { DecisionsService } from '../decisions/decisions.service'; import { RuleMappingService } from '../ruleMapping/ruleMapping.service'; import { ConfigService } from '@nestjs/config'; @@ -529,4 +529,204 @@ describe('ScenarioDataService', () => { expect(result).toEqual(expectedOutput); }); }); + + describe('processProvidedScenarios', () => { + it('should process CSV content and return scenario data', async () => { + const csvContent = Buffer.from('Title,Input: Age,Input: Name\nScenario 1,25,John\nScenario 2,30,Jane'); + const file: Express.Multer.File = { + buffer: csvContent, + fieldname: '', + originalname: '', + encoding: '', + mimetype: '', + size: 0, + stream: null, + destination: '', + filename: '', + path: '', + }; + + const expectedScenarios: ScenarioData[] = [ + { + _id: expect.any(Types.ObjectId), + title: 'Scenario 1', + ruleID: '', + variables: [ + { name: 'Age', value: 25, type: 'number' }, + { name: 'Name', value: 'John', type: 'string' }, + ], + goRulesJSONFilename: 'test.json', + }, + { + _id: expect.any(Types.ObjectId), + title: 'Scenario 2', + ruleID: '', + variables: [ + { name: 'Age', value: 30, type: 'number' }, + { name: 'Name', value: 'Jane', type: 'string' }, + ], + goRulesJSONFilename: 'test.json', + }, + ]; + + jest.spyOn(service, 'parseCSV').mockResolvedValue([ + ['Title', 'Input: Age', 'Input: Name'], + ['Scenario 1', '25', 'John'], + ['Scenario 2', '30', 'Jane'], + ]); + + const result = await service.processProvidedScenarios('test.json', file); + + expect(result).toEqual(expectedScenarios); + }); + + it('should handle boolean values correctly', async () => { + const csvContent = Buffer.from('Title,Input: Active\nScenario 1,True\nScenario 2,False'); + const file: Express.Multer.File = { + buffer: csvContent, + fieldname: '', + originalname: '', + encoding: '', + mimetype: '', + size: 0, + stream: null, + destination: '', + filename: '', + path: '', + }; + + const expectedScenarios: ScenarioData[] = [ + { + _id: expect.any(Types.ObjectId), + title: 'Scenario 1', + ruleID: '', + variables: [{ name: 'Active', value: true, type: 'boolean' }], + goRulesJSONFilename: 'test.json', + }, + { + _id: expect.any(Types.ObjectId), + title: 'Scenario 2', + ruleID: '', + variables: [{ name: 'Active', value: false, type: 'boolean' }], + goRulesJSONFilename: 'test.json', + }, + ]; + + jest.spyOn(service, 'parseCSV').mockResolvedValue([ + ['Title', 'Input: Active'], + ['Scenario 1', 'True'], + ['Scenario 2', 'False'], + ]); + + const result = await service.processProvidedScenarios('test.json', file); + + expect(result).toEqual(expectedScenarios); + }); + + it('should handle string values correctly', async () => { + const csvContent = Buffer.from('Title,Input: Name\nScenario 1,John\nScenario 2,Jane'); + const file: Express.Multer.File = { + buffer: csvContent, + fieldname: '', + originalname: '', + encoding: '', + mimetype: '', + size: 0, + stream: null, + destination: '', + filename: '', + path: '', + }; + + const expectedScenarios: ScenarioData[] = [ + { + _id: expect.any(Types.ObjectId), + title: 'Scenario 1', + ruleID: '', + variables: [{ name: 'Name', value: 'John', type: 'string' }], + goRulesJSONFilename: 'test.json', + }, + { + _id: expect.any(Types.ObjectId), + title: 'Scenario 2', + ruleID: '', + variables: [{ name: 'Name', value: 'Jane', type: 'string' }], + goRulesJSONFilename: 'test.json', + }, + ]; + + jest.spyOn(service, 'parseCSV').mockResolvedValue([ + ['Title', 'Input: Name'], + ['Scenario 1', 'John'], + ['Scenario 2', 'Jane'], + ]); + + const result = await service.processProvidedScenarios('test.json', file); + + expect(result).toEqual(expectedScenarios); + }); + + it('should handle number values correctly', async () => { + const csvContent = Buffer.from('Title,Input: Age\nScenario 1,25\nScenario 2,30'); + const file: Express.Multer.File = { + buffer: csvContent, + fieldname: '', + originalname: '', + encoding: '', + mimetype: '', + size: 0, + stream: null, + destination: '', + filename: '', + path: '', + }; + + const expectedScenarios: ScenarioData[] = [ + { + _id: expect.any(Types.ObjectId), + title: 'Scenario 1', + ruleID: '', + variables: [{ name: 'Age', value: 25, type: 'number' }], + goRulesJSONFilename: 'test.json', + }, + { + _id: expect.any(Types.ObjectId), + title: 'Scenario 2', + ruleID: '', + variables: [{ name: 'Age', value: 30, type: 'number' }], + goRulesJSONFilename: 'test.json', + }, + ]; + + jest.spyOn(service, 'parseCSV').mockResolvedValue([ + ['Title', 'Input: Age'], + ['Scenario 1', '25'], + ['Scenario 2', '30'], + ]); + + const result = await service.processProvidedScenarios('test.json', file); + + expect(result).toEqual(expectedScenarios); + }); + + it('should throw an error if CSV parsing fails', async () => { + const csvContent = Buffer.from('Title,Input: Age\nScenario 1,25\nScenario 2,invalid data'); + const file: Express.Multer.File = { + buffer: csvContent, + fieldname: '', + originalname: '', + encoding: '', + mimetype: '', + size: 0, + stream: null, + destination: '', + filename: '', + path: '', + }; + + jest.spyOn(service, 'parseCSV').mockRejectedValue(new Error('Mocked CSV parsing error')); + + await expect(service.processProvidedScenarios('test.json', file)).rejects.toThrow('Mocked CSV parsing error'); + }); + }); }); From 0d9b2c4c1ae1e8aadaa4b0ddfe072b1b11cfa112 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Thu, 20 Jun 2024 14:01:07 -0700 Subject: [PATCH 22/52] Remove unused imports. --- src/api/scenarioData/scenarioData.service.spec.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/api/scenarioData/scenarioData.service.spec.ts b/src/api/scenarioData/scenarioData.service.spec.ts index 0e6de69..1ce1022 100644 --- a/src/api/scenarioData/scenarioData.service.spec.ts +++ b/src/api/scenarioData/scenarioData.service.spec.ts @@ -2,13 +2,11 @@ import { Test, TestingModule } from '@nestjs/testing'; import { ScenarioDataService } from './scenarioData.service'; import { getModelToken } from '@nestjs/mongoose'; import { Types, Model } from 'mongoose'; -import { ScenarioData, ScenarioDataDocument, Variable } from './scenarioData.schema'; +import { ScenarioData, ScenarioDataDocument } from './scenarioData.schema'; import { DecisionsService } from '../decisions/decisions.service'; import { RuleMappingService } from '../ruleMapping/ruleMapping.service'; import { ConfigService } from '@nestjs/config'; import { DocumentsService } from '../documents/documents.service'; -import * as csvParser from 'csv-parser'; -import { Readable } from 'stream'; describe('ScenarioDataService', () => { let service: ScenarioDataService; From 8587e0fe33efe2dd4388545a6233c33ff38370d1 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Thu, 20 Jun 2024 14:01:44 -0700 Subject: [PATCH 23/52] Update testing for uploadCSV. --- .../scenarioData.controller.spec.ts | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/src/api/scenarioData/scenarioData.controller.spec.ts b/src/api/scenarioData/scenarioData.controller.spec.ts index b43d490..165515b 100644 --- a/src/api/scenarioData/scenarioData.controller.spec.ts +++ b/src/api/scenarioData/scenarioData.controller.spec.ts @@ -4,6 +4,7 @@ import { ScenarioDataService } from './scenarioData.service'; import { ScenarioData } from './scenarioData.schema'; import { HttpException, HttpStatus } from '@nestjs/common'; import { Types } from 'mongoose'; +import { Response } from 'express'; describe('ScenarioDataController', () => { let controller: ScenarioDataController; @@ -20,6 +21,7 @@ describe('ScenarioDataController', () => { updateScenarioData: jest.fn(), deleteScenarioData: jest.fn(), getCSVForRuleRun: jest.fn(), + processProvidedScenarios: jest.fn(), }; beforeEach(async () => { @@ -220,4 +222,100 @@ Scenario 2,couple,3,,200`; } }); }); + describe('uploadCSVAndProcess', () => { + it('should throw an error if no file is uploaded', async () => { + const res: Partial = { + setHeader: jest.fn(), + status: jest.fn().mockReturnThis(), + send: jest.fn(), + }; + + await expect(controller.uploadCSVAndProcess(undefined, res as Response, 'test.json')).rejects.toThrow( + HttpException, + ); + + expect(res.status).not.toHaveBeenCalled(); + expect(res.setHeader).not.toHaveBeenCalled(); + expect(res.send).not.toHaveBeenCalled(); + }); + + it('should process the CSV and return processed data', async () => { + const csvContent = Buffer.from('Title,Input: Age\nScenario 1,25\nScenario 2,30'); + const file: Express.Multer.File = { + buffer: csvContent, + fieldname: '', + originalname: '', + encoding: '', + mimetype: '', + size: 0, + stream: null, + destination: '', + filename: '', + path: '', + }; + + const scenarios = [ + { + _id: new Types.ObjectId(), + title: 'Scenario 1', + ruleID: '', + variables: [{ name: 'Age', value: 25, type: 'number' }], + goRulesJSONFilename: 'test.json', + }, + ]; + + const csvResult = 'Processed CSV Content'; + + mockScenarioDataService.processProvidedScenarios.mockResolvedValue(scenarios); + mockScenarioDataService.getCSVForRuleRun.mockResolvedValue(csvResult); + + const res: Partial = { + setHeader: jest.fn(), + status: jest.fn().mockReturnThis(), + send: jest.fn(), + }; + + await controller.uploadCSVAndProcess(file, res as Response, 'test.json'); + + expect(service.processProvidedScenarios).toHaveBeenCalledWith('test.json', file); + expect(service.getCSVForRuleRun).toHaveBeenCalledWith('test.json', scenarios); + expect(res.setHeader).toHaveBeenCalledWith('Content-Type', 'text/csv'); + expect(res.setHeader).toHaveBeenCalledWith('Content-Disposition', 'attachment; filename=processed_data.csv'); + expect(res.status).toHaveBeenCalledWith(HttpStatus.OK); + expect(res.send).toHaveBeenCalledWith(csvResult); + }); + + it('should handle errors during processing', async () => { + const csvContent = Buffer.from('Title,Input: Age\nScenario 1,25\nScenario 2,30'); + const file: Express.Multer.File = { + buffer: csvContent, + fieldname: '', + originalname: '', + encoding: '', + mimetype: '', + size: 0, + stream: null, + destination: '', + filename: '', + path: '', + }; + + mockScenarioDataService.processProvidedScenarios.mockRejectedValue(new Error('Mocked error')); + + const res: Partial = { + setHeader: jest.fn(), + status: jest.fn().mockReturnThis(), + send: jest.fn(), + }; + + await expect(controller.uploadCSVAndProcess(file, res as Response, 'test.json')).rejects.toThrow( + new HttpException('Error processing CSV file', HttpStatus.INTERNAL_SERVER_ERROR), + ); + + expect(service.processProvidedScenarios).toHaveBeenCalledWith('test.json', file); + expect(res.setHeader).not.toHaveBeenCalled(); + expect(res.status).not.toHaveBeenCalledWith(HttpStatus.OK); + expect(res.send).not.toHaveBeenCalled(); + }); + }); }); From 2b1f842651fce0fbd9fb5abf0c783dfec8a902e6 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Thu, 20 Jun 2024 14:56:03 -0700 Subject: [PATCH 24/52] Update with interface and dto. --- .../scenarioData/dto/create-scenario.dto.ts | 28 +++++++ .../scenarioData.controller.spec.ts | 73 ++++++++++++------- .../scenarioData/scenarioData.controller.ts | 19 ++++- .../scenarioData/scenarioData.interface.ts | 8 ++ src/api/scenarioData/scenarioData.service.ts | 5 +- 5 files changed, 104 insertions(+), 29 deletions(-) create mode 100644 src/api/scenarioData/dto/create-scenario.dto.ts create mode 100644 src/api/scenarioData/scenarioData.interface.ts diff --git a/src/api/scenarioData/dto/create-scenario.dto.ts b/src/api/scenarioData/dto/create-scenario.dto.ts new file mode 100644 index 0000000..9dfbaf5 --- /dev/null +++ b/src/api/scenarioData/dto/create-scenario.dto.ts @@ -0,0 +1,28 @@ +import { IsNotEmpty, IsString, ValidateNested } from 'class-validator'; +import { Type } from 'class-transformer'; +import { Variable } from '../scenarioData.schema'; + +export class VariableClass implements Variable { + name: string; + value: any; + type: string; +} + +export class CreateScenarioDto { + @IsNotEmpty() + @IsString() + title: string; + + @IsNotEmpty() + @IsString() + ruleID: string; + + @IsNotEmpty() + @ValidateNested({ each: true }) + @Type(() => VariableClass) + variables: VariableClass[]; + + @IsNotEmpty() + @IsString() + goRulesJSONFilename: string; +} diff --git a/src/api/scenarioData/scenarioData.controller.spec.ts b/src/api/scenarioData/scenarioData.controller.spec.ts index 165515b..cc64d5d 100644 --- a/src/api/scenarioData/scenarioData.controller.spec.ts +++ b/src/api/scenarioData/scenarioData.controller.spec.ts @@ -5,6 +5,7 @@ import { ScenarioData } from './scenarioData.schema'; import { HttpException, HttpStatus } from '@nestjs/common'; import { Types } from 'mongoose'; import { Response } from 'express'; +import { VariableClass, CreateScenarioDto } from './dto/create-scenario.dto'; describe('ScenarioDataController', () => { let controller: ScenarioDataController; @@ -112,23 +113,37 @@ describe('ScenarioDataController', () => { variables: [], goRulesJSONFilename: 'filename', }; + + // Adjusting variables to use VariableClass instances + const variables: VariableClass[] = [ + { name: 'variable1', value: 'value1', type: 'string' }, + { name: 'variable2', value: 123, type: 'number' }, + ]; + jest.spyOn(service, 'createScenarioData').mockResolvedValue(result); - expect(await controller.createScenarioData(result)).toBe(result); + const dto: CreateScenarioDto = { + title: result.title, + ruleID: result.ruleID, + variables: variables, + goRulesJSONFilename: result.goRulesJSONFilename, + }; + + expect(await controller.createScenarioData(dto)).toBe(result); }); it('should throw an error if service fails', async () => { - jest.spyOn(service, 'createScenarioData').mockRejectedValue(new Error('Service error')); - - await expect( - controller.createScenarioData({ - _id: testObjectId, - title: 'title', - ruleID: 'ruleID', - variables: [], - goRulesJSONFilename: 'filename', - }), - ).rejects.toThrow(HttpException); + const errorMessage = 'Service error'; + jest.spyOn(service, 'createScenarioData').mockRejectedValue(new Error(errorMessage)); + + const dto: CreateScenarioDto = { + title: 'title', + ruleID: 'ruleID', + variables: [], + goRulesJSONFilename: 'filename', + }; + + await expect(controller.createScenarioData(dto)).rejects.toThrow(HttpException); }); }); @@ -137,27 +152,35 @@ describe('ScenarioDataController', () => { const result: ScenarioData = { _id: testObjectId, title: 'title', - ruleID: 'rule1', + ruleID: 'ruleID', variables: [], goRulesJSONFilename: 'filename', }; + jest.spyOn(service, 'updateScenarioData').mockResolvedValue(result); - expect(await controller.updateScenarioData(testObjectId.toHexString(), result)).toBe(result); + const dto: CreateScenarioDto = { + title: result.title, + ruleID: result.ruleID, + variables: [], + goRulesJSONFilename: result.goRulesJSONFilename, + }; + + expect(await controller.updateScenarioData(testObjectId.toHexString(), dto)).toBe(result); }); it('should throw an error if service fails', async () => { - jest.spyOn(service, 'updateScenarioData').mockRejectedValue(new Error('Service error')); - - await expect( - controller.updateScenarioData(testObjectId.toHexString(), { - _id: testObjectId, - title: 'title', - ruleID: 'rule1', - variables: [], - goRulesJSONFilename: 'filename', - }), - ).rejects.toThrow(HttpException); + const errorMessage = 'Service error'; + jest.spyOn(service, 'updateScenarioData').mockRejectedValue(new Error(errorMessage)); + + const dto: CreateScenarioDto = { + title: 'title', + ruleID: 'ruleID', + variables: [], + goRulesJSONFilename: 'filename', + }; + + await expect(controller.updateScenarioData(testObjectId.toHexString(), dto)).rejects.toThrow(HttpException); }); }); diff --git a/src/api/scenarioData/scenarioData.controller.ts b/src/api/scenarioData/scenarioData.controller.ts index 4a5d220..0248895 100644 --- a/src/api/scenarioData/scenarioData.controller.ts +++ b/src/api/scenarioData/scenarioData.controller.ts @@ -16,6 +16,7 @@ import { FileInterceptor } from '@nestjs/platform-express'; import { Response } from 'express'; import { ScenarioDataService } from './scenarioData.service'; import { ScenarioData } from './scenarioData.schema'; +import { CreateScenarioDto } from './dto/create-scenario.dto'; @Controller('api/scenario') export class ScenarioDataController { @@ -58,8 +59,15 @@ export class ScenarioDataController { } @Post() - async createScenarioData(@Body() scenarioData: ScenarioData): Promise { + async createScenarioData(@Body() createScenarioDto: CreateScenarioDto): Promise { try { + const scenarioData: ScenarioData = { + _id: undefined!, + title: createScenarioDto.title, + ruleID: createScenarioDto.ruleID, + variables: createScenarioDto.variables, + goRulesJSONFilename: createScenarioDto.goRulesJSONFilename, + }; return await this.scenarioDataService.createScenarioData(scenarioData); } catch (error) { throw new HttpException('Error creating scenario data', HttpStatus.INTERNAL_SERVER_ERROR); @@ -69,9 +77,16 @@ export class ScenarioDataController { @Put('/:scenarioId') async updateScenarioData( @Param('scenarioId') scenarioId: string, - @Body() scenarioData: ScenarioData, + @Body() updateScenarioDto: CreateScenarioDto, ): Promise { try { + const scenarioData: ScenarioData = { + _id: undefined!, + title: updateScenarioDto.title, + ruleID: updateScenarioDto.ruleID, + variables: updateScenarioDto.variables, + goRulesJSONFilename: updateScenarioDto.goRulesJSONFilename, + }; return await this.scenarioDataService.updateScenarioData(scenarioId, scenarioData); } catch (error) { throw new HttpException('Error updating scenario data', HttpStatus.INTERNAL_SERVER_ERROR); diff --git a/src/api/scenarioData/scenarioData.interface.ts b/src/api/scenarioData/scenarioData.interface.ts new file mode 100644 index 0000000..98e8cb6 --- /dev/null +++ b/src/api/scenarioData/scenarioData.interface.ts @@ -0,0 +1,8 @@ +export interface RuleSchema { + inputs: Array<{ id: string; property: string }>; + finalOutputs: Array<{ id: string; property: string }>; +} + +export interface RuleRunResults { + [scenarioId: string]: any; +} diff --git a/src/api/scenarioData/scenarioData.service.ts b/src/api/scenarioData/scenarioData.service.ts index b1b349c..8b41b49 100644 --- a/src/api/scenarioData/scenarioData.service.ts +++ b/src/api/scenarioData/scenarioData.service.ts @@ -5,6 +5,7 @@ import { ScenarioData, ScenarioDataDocument, Variable } from './scenarioData.sch import { DecisionsService } from '../decisions/decisions.service'; import { RuleMappingService } from '../ruleMapping/ruleMapping.service'; import * as csvParser from 'csv-parser'; +import { RuleSchema, RuleRunResults } from './scenarioData.interface'; @Injectable() export class ScenarioDataService { constructor( @@ -98,7 +99,7 @@ export class ScenarioDataService { newScenarios?: ScenarioData[], ): Promise<{ [scenarioId: string]: any }> { const scenarios = newScenarios || (await this.getScenariosByFilename(goRulesJSONFilename)); - const ruleSchema = await this.ruleMappingService.ruleSchemaFile(goRulesJSONFilename); + const ruleSchema: RuleSchema = await this.ruleMappingService.ruleSchemaFile(goRulesJSONFilename); const results: { [scenarioId: string]: any } = {}; const getPropertyById = (id: string, type: 'input' | 'output') => { @@ -168,7 +169,7 @@ export class ScenarioDataService { * Constructs CSV headers and rows based on input and output keys. */ async getCSVForRuleRun(goRulesJSONFilename: string, newScenarios?: ScenarioData[]): Promise { - const ruleRunResults = await this.runDecisionsForScenarios(goRulesJSONFilename, newScenarios); + const ruleRunResults: RuleRunResults = await this.runDecisionsForScenarios(goRulesJSONFilename, newScenarios); const inputKeys = Array.from( new Set(Object.values(ruleRunResults).flatMap((scenario) => Object.keys(scenario.inputs))), ); From d5b7f72d6a8b564b11fd7f90898080f76958f5ab Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Thu, 20 Jun 2024 15:19:03 -0700 Subject: [PATCH 25/52] Update with expectedResults field to handle testing of scenarios. --- src/api/scenarioData/dto/create-scenario.dto.ts | 4 ++++ .../scenarioData/scenarioData.controller.spec.ts | 9 ++++++++- src/api/scenarioData/scenarioData.controller.ts | 2 ++ src/api/scenarioData/scenarioData.schema.ts | 7 +++++++ src/api/scenarioData/scenarioData.service.spec.ts | 13 +++++++++++++ src/api/scenarioData/scenarioData.service.ts | 1 + 6 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/api/scenarioData/dto/create-scenario.dto.ts b/src/api/scenarioData/dto/create-scenario.dto.ts index 9dfbaf5..89e056c 100644 --- a/src/api/scenarioData/dto/create-scenario.dto.ts +++ b/src/api/scenarioData/dto/create-scenario.dto.ts @@ -22,6 +22,10 @@ export class CreateScenarioDto { @Type(() => VariableClass) variables: VariableClass[]; + @ValidateNested({ each: true }) + @Type(() => VariableClass) + expectedResults: VariableClass[]; + @IsNotEmpty() @IsString() goRulesJSONFilename: string; diff --git a/src/api/scenarioData/scenarioData.controller.spec.ts b/src/api/scenarioData/scenarioData.controller.spec.ts index cc64d5d..b2a61fa 100644 --- a/src/api/scenarioData/scenarioData.controller.spec.ts +++ b/src/api/scenarioData/scenarioData.controller.spec.ts @@ -112,14 +112,16 @@ describe('ScenarioDataController', () => { ruleID: 'ruleID', variables: [], goRulesJSONFilename: 'filename', + expectedResults: [], }; - // Adjusting variables to use VariableClass instances const variables: VariableClass[] = [ { name: 'variable1', value: 'value1', type: 'string' }, { name: 'variable2', value: 123, type: 'number' }, ]; + const expectedResults: VariableClass[] = []; + jest.spyOn(service, 'createScenarioData').mockResolvedValue(result); const dto: CreateScenarioDto = { @@ -127,6 +129,7 @@ describe('ScenarioDataController', () => { ruleID: result.ruleID, variables: variables, goRulesJSONFilename: result.goRulesJSONFilename, + expectedResults: expectedResults, }; expect(await controller.createScenarioData(dto)).toBe(result); @@ -141,6 +144,7 @@ describe('ScenarioDataController', () => { ruleID: 'ruleID', variables: [], goRulesJSONFilename: 'filename', + expectedResults: [], }; await expect(controller.createScenarioData(dto)).rejects.toThrow(HttpException); @@ -155,6 +159,7 @@ describe('ScenarioDataController', () => { ruleID: 'ruleID', variables: [], goRulesJSONFilename: 'filename', + expectedResults: [], }; jest.spyOn(service, 'updateScenarioData').mockResolvedValue(result); @@ -164,6 +169,7 @@ describe('ScenarioDataController', () => { ruleID: result.ruleID, variables: [], goRulesJSONFilename: result.goRulesJSONFilename, + expectedResults: [], }; expect(await controller.updateScenarioData(testObjectId.toHexString(), dto)).toBe(result); @@ -178,6 +184,7 @@ describe('ScenarioDataController', () => { ruleID: 'ruleID', variables: [], goRulesJSONFilename: 'filename', + expectedResults: [], }; await expect(controller.updateScenarioData(testObjectId.toHexString(), dto)).rejects.toThrow(HttpException); diff --git a/src/api/scenarioData/scenarioData.controller.ts b/src/api/scenarioData/scenarioData.controller.ts index 0248895..41a04c3 100644 --- a/src/api/scenarioData/scenarioData.controller.ts +++ b/src/api/scenarioData/scenarioData.controller.ts @@ -67,6 +67,7 @@ export class ScenarioDataController { ruleID: createScenarioDto.ruleID, variables: createScenarioDto.variables, goRulesJSONFilename: createScenarioDto.goRulesJSONFilename, + expectedResults: createScenarioDto.expectedResults, }; return await this.scenarioDataService.createScenarioData(scenarioData); } catch (error) { @@ -86,6 +87,7 @@ export class ScenarioDataController { ruleID: updateScenarioDto.ruleID, variables: updateScenarioDto.variables, goRulesJSONFilename: updateScenarioDto.goRulesJSONFilename, + expectedResults: updateScenarioDto.expectedResults, }; return await this.scenarioDataService.updateScenarioData(scenarioId, scenarioData); } catch (error) { diff --git a/src/api/scenarioData/scenarioData.schema.ts b/src/api/scenarioData/scenarioData.schema.ts index b713006..845edf3 100644 --- a/src/api/scenarioData/scenarioData.schema.ts +++ b/src/api/scenarioData/scenarioData.schema.ts @@ -52,6 +52,13 @@ export class ScenarioData { }) variables: Variable[]; + @Prop({ + required: false, + description: 'The expected result of the scenario', + type: [VariableModelSchema], + }) + expectedResults: Variable[]; + @Prop({ required: true, description: 'The filename of the JSON file containing the rule' }) goRulesJSONFilename: string; } diff --git a/src/api/scenarioData/scenarioData.service.spec.ts b/src/api/scenarioData/scenarioData.service.spec.ts index 1ce1022..de0f937 100644 --- a/src/api/scenarioData/scenarioData.service.spec.ts +++ b/src/api/scenarioData/scenarioData.service.spec.ts @@ -22,6 +22,7 @@ describe('ScenarioDataService', () => { ruleID: 'ruleID', variables: [], goRulesJSONFilename: 'test.json', + expectedResults: [], }; class MockScenarioDataModel { @@ -281,6 +282,7 @@ describe('ScenarioDataService', () => { variables: [{ name: 'familyComposition', value: 'single' }], ruleID: 'ruleID', goRulesJSONFilename: 'test.json', + expectedResults: [], }, { _id: testObjectId, @@ -288,6 +290,7 @@ describe('ScenarioDataService', () => { variables: [{ name: 'numberOfChildren', value: 2 }], ruleID: 'ruleID', goRulesJSONFilename: 'test.json', + expectedResults: [], }, ]; const ruleSchema = { @@ -346,6 +349,7 @@ describe('ScenarioDataService', () => { variables: [{ name: 'familyComposition', value: 'single' }], ruleID: 'ruleID', goRulesJSONFilename: 'test.json', + expectedResults: [], }, ]; const ruleSchema = { @@ -372,6 +376,7 @@ describe('ScenarioDataService', () => { variables: [], ruleID: 'ruleID', goRulesJSONFilename: 'test.json', + expectedResults: [], }, ]; const ruleSchema = { @@ -554,6 +559,7 @@ describe('ScenarioDataService', () => { { name: 'Name', value: 'John', type: 'string' }, ], goRulesJSONFilename: 'test.json', + expectedResults: [], }, { _id: expect.any(Types.ObjectId), @@ -564,6 +570,7 @@ describe('ScenarioDataService', () => { { name: 'Name', value: 'Jane', type: 'string' }, ], goRulesJSONFilename: 'test.json', + expectedResults: [], }, ]; @@ -600,6 +607,7 @@ describe('ScenarioDataService', () => { ruleID: '', variables: [{ name: 'Active', value: true, type: 'boolean' }], goRulesJSONFilename: 'test.json', + expectedResults: [], }, { _id: expect.any(Types.ObjectId), @@ -607,6 +615,7 @@ describe('ScenarioDataService', () => { ruleID: '', variables: [{ name: 'Active', value: false, type: 'boolean' }], goRulesJSONFilename: 'test.json', + expectedResults: [], }, ]; @@ -643,6 +652,7 @@ describe('ScenarioDataService', () => { ruleID: '', variables: [{ name: 'Name', value: 'John', type: 'string' }], goRulesJSONFilename: 'test.json', + expectedResults: [], }, { _id: expect.any(Types.ObjectId), @@ -650,6 +660,7 @@ describe('ScenarioDataService', () => { ruleID: '', variables: [{ name: 'Name', value: 'Jane', type: 'string' }], goRulesJSONFilename: 'test.json', + expectedResults: [], }, ]; @@ -686,6 +697,7 @@ describe('ScenarioDataService', () => { ruleID: '', variables: [{ name: 'Age', value: 25, type: 'number' }], goRulesJSONFilename: 'test.json', + expectedResults: [], }, { _id: expect.any(Types.ObjectId), @@ -693,6 +705,7 @@ describe('ScenarioDataService', () => { ruleID: '', variables: [{ name: 'Age', value: 30, type: 'number' }], goRulesJSONFilename: 'test.json', + expectedResults: [], }, ]; diff --git a/src/api/scenarioData/scenarioData.service.ts b/src/api/scenarioData/scenarioData.service.ts index 8b41b49..6861ec8 100644 --- a/src/api/scenarioData/scenarioData.service.ts +++ b/src/api/scenarioData/scenarioData.service.ts @@ -255,6 +255,7 @@ export class ScenarioDataService { ruleID: '', variables: inputs, goRulesJSONFilename: goRulesJSONFilename, + expectedResults: [], }; scenarios.push(scenario); From bcb2f7e33d509777d349ccf9b9f8dc1d7ad38e25 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Fri, 21 Jun 2024 09:14:15 -0700 Subject: [PATCH 26/52] Update rundecisionsforscenarios to return results and expected results. --- src/api/scenarioData/scenarioData.service.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/api/scenarioData/scenarioData.service.ts b/src/api/scenarioData/scenarioData.service.ts index 6861ec8..0be29e8 100644 --- a/src/api/scenarioData/scenarioData.service.ts +++ b/src/api/scenarioData/scenarioData.service.ts @@ -134,6 +134,11 @@ export class ScenarioDataService { return acc; }, {}); + const expectedResultsObject = scenario?.expectedResults?.reduce((acc: any, obj: any) => { + acc[obj.name] = obj.value; + return acc; + }, {}); + try { const decisionResult = await this.decisionsService.runDecisionByFile( scenario.goRulesJSONFilename, @@ -141,7 +146,7 @@ export class ScenarioDataService { { trace: true }, ); - const scenarioResult = { inputs: {}, outputs: {} }; + const scenarioResult = { inputs: {}, outputs: {}, expectedResults: {}, result: {} }; // Map inputs and outputs based on the trace for (const trace of Object.values(decisionResult.trace)) { @@ -153,6 +158,9 @@ export class ScenarioDataService { } } + scenarioResult.expectedResults = expectedResultsObject; + scenarioResult.result = decisionResult.result; + results[scenario.title.toString()] = scenarioResult; } catch (error) { console.error(`Error running decision for scenario ${scenario._id}: ${error.message}`); From 5d174f23a435d3bc86fdda024f99e01c6a12d911 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Fri, 21 Jun 2024 09:22:07 -0700 Subject: [PATCH 27/52] Update testing for rundecisionsforscenarios to handle new outputs. --- src/api/scenarioData/scenarioData.service.spec.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/api/scenarioData/scenarioData.service.spec.ts b/src/api/scenarioData/scenarioData.service.spec.ts index de0f937..1fa2dd6 100644 --- a/src/api/scenarioData/scenarioData.service.spec.ts +++ b/src/api/scenarioData/scenarioData.service.spec.ts @@ -333,10 +333,18 @@ describe('ScenarioDataService', () => { 'Scenario 1': { inputs: { familyComposition: 'single', numberOfChildren: 2 }, outputs: { baseAmount: 100, isEligible: true }, + result: { + status: 'pass', + }, + expectedResults: {}, }, 'Scenario 2': { inputs: { familyComposition: 'single', numberOfChildren: 2 }, outputs: { baseAmount: 100, isEligible: true }, + result: { + status: 'pass', + }, + expectedResults: {}, }, }); }); @@ -406,6 +414,10 @@ describe('ScenarioDataService', () => { 'Scenario 1': { inputs: {}, outputs: { isEligible: true }, + expectedResults: {}, + result: { + status: 'pass', + }, }, }); }); From 9dbb6efd6a45eeef21b370ad971c4f8c144c461e Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Fri, 21 Jun 2024 10:24:22 -0700 Subject: [PATCH 28/52] Add assessment of result equality. --- .../scenarioData/scenarioData.service.spec.ts | 3 +++ src/api/scenarioData/scenarioData.service.ts | 21 ++++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/api/scenarioData/scenarioData.service.spec.ts b/src/api/scenarioData/scenarioData.service.spec.ts index 1fa2dd6..ffa2f88 100644 --- a/src/api/scenarioData/scenarioData.service.spec.ts +++ b/src/api/scenarioData/scenarioData.service.spec.ts @@ -337,6 +337,7 @@ describe('ScenarioDataService', () => { status: 'pass', }, expectedResults: {}, + resultMatch: true, }, 'Scenario 2': { inputs: { familyComposition: 'single', numberOfChildren: 2 }, @@ -345,6 +346,7 @@ describe('ScenarioDataService', () => { status: 'pass', }, expectedResults: {}, + resultMatch: true, }, }); }); @@ -418,6 +420,7 @@ describe('ScenarioDataService', () => { result: { status: 'pass', }, + resultMatch: true, }, }); }); diff --git a/src/api/scenarioData/scenarioData.service.ts b/src/api/scenarioData/scenarioData.service.ts index 0be29e8..940bd48 100644 --- a/src/api/scenarioData/scenarioData.service.ts +++ b/src/api/scenarioData/scenarioData.service.ts @@ -128,6 +128,23 @@ export class ScenarioDataService { return result; }; + const isEqual = (obj1: any, obj2: any) => { + if (obj1 === obj2) return true; + if (obj1 == null || obj2 == null) return false; + if (typeof obj1 !== 'object' || typeof obj2 !== 'object') return false; + + const keys1 = Object.keys(obj1); + const keys2 = Object.keys(obj2); + + if (keys1.length !== keys2.length) return false; + + for (const key of keys1) { + if (!keys2.includes(key) || !isEqual(obj1[key], obj2[key])) return false; + } + + return true; + }; + for (const scenario of scenarios) { const variablesObject = scenario?.variables?.reduce((acc: any, obj: any) => { acc[obj.name] = obj.value; @@ -146,7 +163,7 @@ export class ScenarioDataService { { trace: true }, ); - const scenarioResult = { inputs: {}, outputs: {}, expectedResults: {}, result: {} }; + const scenarioResult = { inputs: {}, outputs: {}, expectedResults: {}, result: {}, resultMatch: true }; // Map inputs and outputs based on the trace for (const trace of Object.values(decisionResult.trace)) { @@ -160,6 +177,8 @@ export class ScenarioDataService { scenarioResult.expectedResults = expectedResultsObject; scenarioResult.result = decisionResult.result; + scenarioResult.resultMatch = + Object.keys(expectedResultsObject).length > 0 ? isEqual(decisionResult.result, expectedResultsObject) : true; results[scenario.title.toString()] = scenarioResult; } catch (error) { From 7860517d693a9e079d46d0f979a73584688bcb3d Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Fri, 21 Jun 2024 14:29:27 -0700 Subject: [PATCH 29/52] Update csv export to include more details. --- .../scenarioData/scenarioData.service.spec.ts | 15 ++++--- src/api/scenarioData/scenarioData.service.ts | 41 ++++++++++++++----- 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/src/api/scenarioData/scenarioData.service.spec.ts b/src/api/scenarioData/scenarioData.service.spec.ts index ffa2f88..31a8e9a 100644 --- a/src/api/scenarioData/scenarioData.service.spec.ts +++ b/src/api/scenarioData/scenarioData.service.spec.ts @@ -433,10 +433,12 @@ describe('ScenarioDataService', () => { 'Scenario 1': { inputs: { familyComposition: 'single', numberOfChildren: 2 }, outputs: { isEligible: true, baseAmount: 100 }, + expectedResults: {}, }, 'Scenario 2': { inputs: { familyComposition: 'couple', numberOfChildren: 3 }, outputs: { isEligible: false, baseAmount: 200 }, + expectedResults: {}, }, }; @@ -444,7 +446,7 @@ describe('ScenarioDataService', () => { const csvContent = await service.getCSVForRuleRun(goRulesJSONFilename); - const expectedCsvContent = `Scenario,Input: familyComposition,Input: numberOfChildren,Output: isEligible,Output: baseAmount\nScenario 1,single,2,true,100\nScenario 2,couple,3,false,200`; + const expectedCsvContent = `Scenario,Results Match Expected (Pass/Fail),Input: familyComposition,Input: numberOfChildren,Output: isEligible,Output: baseAmount\nScenario 1,Fail,single,2,true,100\nScenario 2,Fail,couple,3,false,200`; expect(csvContent.trim()).toBe(expectedCsvContent.trim()); }); @@ -455,10 +457,12 @@ describe('ScenarioDataService', () => { 'Scenario 1': { inputs: { familyComposition: 'single' }, outputs: { isEligible: true }, + expectedResults: {}, }, 'Scenario 2': { inputs: { familyComposition: 'couple', numberOfChildren: 3 }, outputs: { baseAmount: 200 }, + expectedResults: {}, }, }; @@ -466,7 +470,7 @@ describe('ScenarioDataService', () => { const csvContent = await service.getCSVForRuleRun(goRulesJSONFilename); - const expectedCsvContent = `Scenario,Input: familyComposition,Input: numberOfChildren,Output: isEligible,Output: baseAmount\nScenario 1,single,,true,\nScenario 2,couple,3,,200`; + const expectedCsvContent = `Scenario,Results Match Expected (Pass/Fail),Input: familyComposition,Input: numberOfChildren,Output: isEligible,Output: baseAmount\nScenario 1,Fail,single,,true,\nScenario 2,Fail,couple,3,,200`; expect(csvContent.trim()).toBe(expectedCsvContent.trim()); }); @@ -477,6 +481,7 @@ describe('ScenarioDataService', () => { 'Scenario 1': { inputs: { familyComposition: 'single', numberOfChildren: 2 }, outputs: { isEligible: true, baseAmount: 100 }, + expectedResults: {}, }, }; @@ -484,7 +489,7 @@ describe('ScenarioDataService', () => { const csvContent = await service.getCSVForRuleRun(goRulesJSONFilename); - const expectedCsvContent = `Scenario,Input: familyComposition,Input: numberOfChildren,Output: isEligible,Output: baseAmount\nScenario 1,single,2,true,100`; + const expectedCsvContent = `Scenario,Results Match Expected (Pass/Fail),Input: familyComposition,Input: numberOfChildren,Output: isEligible,Output: baseAmount\nScenario 1,Fail,single,2,true,100`; expect(csvContent.trim()).toBe(expectedCsvContent.trim()); }); @@ -497,7 +502,7 @@ describe('ScenarioDataService', () => { const csvContent = await service.getCSVForRuleRun(goRulesJSONFilename); - const expectedCsvContent = `Scenario`; + const expectedCsvContent = `Scenario,Results Match Expected (Pass/Fail)`; expect(csvContent.trim()).toBe(expectedCsvContent.trim()); }); @@ -515,7 +520,7 @@ describe('ScenarioDataService', () => { const csvContent = await service.getCSVForRuleRun(goRulesJSONFilename); - const expectedCsvContent = `Scenario\nScenario 1`; + const expectedCsvContent = `Scenario,Results Match Expected (Pass/Fail)\nScenario 1,Fail`; expect(csvContent.trim()).toBe(expectedCsvContent.trim()); }); diff --git a/src/api/scenarioData/scenarioData.service.ts b/src/api/scenarioData/scenarioData.service.ts index 940bd48..e840227 100644 --- a/src/api/scenarioData/scenarioData.service.ts +++ b/src/api/scenarioData/scenarioData.service.ts @@ -163,7 +163,16 @@ export class ScenarioDataService { { trace: true }, ); - const scenarioResult = { inputs: {}, outputs: {}, expectedResults: {}, result: {}, resultMatch: true }; + const resultMatches = + Object.keys(expectedResultsObject).length > 0 ? isEqual(decisionResult.result, expectedResultsObject) : true; + + const scenarioResult = { + inputs: {}, + outputs: {}, + expectedResults: expectedResultsObject, + result: decisionResult.result, + resultMatch: resultMatches, + }; // Map inputs and outputs based on the trace for (const trace of Object.values(decisionResult.trace)) { @@ -175,11 +184,6 @@ export class ScenarioDataService { } } - scenarioResult.expectedResults = expectedResultsObject; - scenarioResult.result = decisionResult.result; - scenarioResult.resultMatch = - Object.keys(expectedResultsObject).length > 0 ? isEqual(decisionResult.result, expectedResultsObject) : true; - results[scenario.title.toString()] = scenarioResult; } catch (error) { console.error(`Error running decision for scenario ${scenario._id}: ${error.message}`); @@ -197,24 +201,39 @@ export class ScenarioDataService { */ async getCSVForRuleRun(goRulesJSONFilename: string, newScenarios?: ScenarioData[]): Promise { const ruleRunResults: RuleRunResults = await this.runDecisionsForScenarios(goRulesJSONFilename, newScenarios); + const inputKeys = Array.from( new Set(Object.values(ruleRunResults).flatMap((scenario) => Object.keys(scenario.inputs))), ); const outputKeys = Array.from( new Set(Object.values(ruleRunResults).flatMap((scenario) => Object.keys(scenario.outputs))), ); + const expectedResultsKeys = Array.from( + new Set( + Object.values(ruleRunResults).flatMap((scenario) => { + if (scenario.expectedResults) { + return Object.keys(scenario.expectedResults); + } + return []; + }), + ), + ); const headers = [ 'Scenario', + 'Results Match Expected (Pass/Fail)', ...inputKeys.map((key) => `Input: ${key}`), ...outputKeys.map((key) => `Output: ${key}`), + ...expectedResultsKeys.map((key) => `Expected Result: ${key}`), ]; + const rows = Object.entries(ruleRunResults).map(([scenarioName, scenarioData]) => { - const inputs = inputKeys.map((key) => (scenarioData.inputs[key] !== undefined ? scenarioData.inputs[key] : '')); - const outputs = outputKeys.map((key) => - scenarioData.outputs[key] !== undefined ? scenarioData.outputs[key] : '', - ); - return [scenarioName, ...inputs, ...outputs]; + const resultsMatch = scenarioData.resultMatch ? 'Pass' : 'Fail'; + const inputs = inputKeys.map((key) => scenarioData.inputs[key] ?? ''); + const outputs = outputKeys.map((key) => scenarioData.outputs[key] ?? ''); + const expectedResults = expectedResultsKeys.map((key) => scenarioData.expectedResults[key] ?? ''); + + return [scenarioName, resultsMatch, ...inputs, ...outputs, ...expectedResults]; }); const csvContent = [headers, ...rows].map((row) => row.join(',')).join('\n'); From e9ca1997144073345365513dca49ee2c60b652b7 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Fri, 21 Jun 2024 15:17:40 -0700 Subject: [PATCH 30/52] Update csv testing format. --- src/api/scenarioData/scenarioData.service.ts | 44 ++++++++++++++++---- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/src/api/scenarioData/scenarioData.service.ts b/src/api/scenarioData/scenarioData.service.ts index e840227..e2a7b4b 100644 --- a/src/api/scenarioData/scenarioData.service.ts +++ b/src/api/scenarioData/scenarioData.service.ts @@ -223,8 +223,8 @@ export class ScenarioDataService { 'Scenario', 'Results Match Expected (Pass/Fail)', ...inputKeys.map((key) => `Input: ${key}`), - ...outputKeys.map((key) => `Output: ${key}`), ...expectedResultsKeys.map((key) => `Expected Result: ${key}`), + ...outputKeys.map((key) => `Result: ${key}`), ]; const rows = Object.entries(ruleRunResults).map(([scenarioName, scenarioData]) => { @@ -233,7 +233,7 @@ export class ScenarioDataService { const outputs = outputKeys.map((key) => scenarioData.outputs[key] ?? ''); const expectedResults = expectedResultsKeys.map((key) => scenarioData.expectedResults[key] ?? ''); - return [scenarioName, resultsMatch, ...inputs, ...outputs, ...expectedResults]; + return [scenarioName, resultsMatch, ...inputs, ...expectedResults, ...outputs]; }); const csvContent = [headers, ...rows].map((row) => row.join(',')).join('\n'); @@ -266,10 +266,15 @@ export class ScenarioDataService { ): Promise { const parsedData = await this.parseCSV(csvContent); const headers = parsedData[0]; + const inputKeys = headers .filter((header) => header.startsWith('Input: ')) .map((header) => header.replace('Input: ', '')); + const expectedResultsKeys = headers + .filter((header) => header.startsWith('Expected Result: ')) + .map((header) => header.replace('Expected Result: ', '')); + const scenarios: ScenarioData[] = []; function formatValue(value: string): boolean | number | string { @@ -282,6 +287,9 @@ export class ScenarioDataService { if (!isNaN(numberValue)) { return numberValue; } + if (value === '') { + return null; + } return value; } @@ -289,11 +297,31 @@ export class ScenarioDataService { const row = parsedData[i]; const scenarioTitle = row[0]; - const inputs: Variable[] = inputKeys.map((key, index) => ({ - name: key, - value: formatValue(row[index + 1]), - type: typeof formatValue(row[index + 1]), - })); + const inputs: Variable[] = inputKeys.map((key, index) => { + // Adjusted index to account for scenario title and results match + const value = formatValue(row[index + 2]); + return { + name: key, + value: value, + type: typeof value, + }; + }); + + // Adjusted index to account for scenario title, results match, and inputs in csv layout + const expectedResultsStartIndex = 2 + inputKeys.length; + const expectedResults: Variable[] = expectedResultsKeys + .map((key, index) => { + const value = formatValue(row[expectedResultsStartIndex + index]); + if (value !== null && value !== undefined && value !== '') { + return { + name: key, + value: value, + type: typeof value, + }; + } + return undefined; + }) + .filter((entry) => entry !== undefined); const scenario: ScenarioData = { _id: new Types.ObjectId(), @@ -301,7 +329,7 @@ export class ScenarioDataService { ruleID: '', variables: inputs, goRulesJSONFilename: goRulesJSONFilename, - expectedResults: [], + expectedResults: expectedResults || [], }; scenarios.push(scenario); From 80e39c6f7d7a8d738556bee2716b1d2cd97e687f Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Fri, 21 Jun 2024 15:25:10 -0700 Subject: [PATCH 31/52] Update csv testing format. --- .../scenarioData/scenarioData.service.spec.ts | 30 +++++++++---------- src/api/scenarioData/scenarioData.service.ts | 6 ++-- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/api/scenarioData/scenarioData.service.spec.ts b/src/api/scenarioData/scenarioData.service.spec.ts index 31a8e9a..75ba7a5 100644 --- a/src/api/scenarioData/scenarioData.service.spec.ts +++ b/src/api/scenarioData/scenarioData.service.spec.ts @@ -446,7 +446,7 @@ describe('ScenarioDataService', () => { const csvContent = await service.getCSVForRuleRun(goRulesJSONFilename); - const expectedCsvContent = `Scenario,Results Match Expected (Pass/Fail),Input: familyComposition,Input: numberOfChildren,Output: isEligible,Output: baseAmount\nScenario 1,Fail,single,2,true,100\nScenario 2,Fail,couple,3,false,200`; + const expectedCsvContent = `Scenario,Results Match Expected (Pass/Fail),Input: familyComposition,Input: numberOfChildren,Result: isEligible,Result: baseAmount\nScenario 1,Fail,single,2,true,100\nScenario 2,Fail,couple,3,false,200`; expect(csvContent.trim()).toBe(expectedCsvContent.trim()); }); @@ -470,7 +470,7 @@ describe('ScenarioDataService', () => { const csvContent = await service.getCSVForRuleRun(goRulesJSONFilename); - const expectedCsvContent = `Scenario,Results Match Expected (Pass/Fail),Input: familyComposition,Input: numberOfChildren,Output: isEligible,Output: baseAmount\nScenario 1,Fail,single,,true,\nScenario 2,Fail,couple,3,,200`; + const expectedCsvContent = `Scenario,Results Match Expected (Pass/Fail),Input: familyComposition,Input: numberOfChildren,Result: isEligible,Result: baseAmount\nScenario 1,Fail,single,,true,\nScenario 2,Fail,couple,3,,200`; expect(csvContent.trim()).toBe(expectedCsvContent.trim()); }); @@ -489,7 +489,7 @@ describe('ScenarioDataService', () => { const csvContent = await service.getCSVForRuleRun(goRulesJSONFilename); - const expectedCsvContent = `Scenario,Results Match Expected (Pass/Fail),Input: familyComposition,Input: numberOfChildren,Output: isEligible,Output: baseAmount\nScenario 1,Fail,single,2,true,100`; + const expectedCsvContent = `Scenario,Results Match Expected (Pass/Fail),Input: familyComposition,Input: numberOfChildren,Result: isEligible,Result: baseAmount\nScenario 1,Fail,single,2,true,100`; expect(csvContent.trim()).toBe(expectedCsvContent.trim()); }); @@ -595,9 +595,9 @@ describe('ScenarioDataService', () => { ]; jest.spyOn(service, 'parseCSV').mockResolvedValue([ - ['Title', 'Input: Age', 'Input: Name'], - ['Scenario 1', '25', 'John'], - ['Scenario 2', '30', 'Jane'], + ['Title', 'Results Match Expected (Pass/Fail)', 'Input: Age', 'Input: Name'], + ['Scenario 1', 'Pass', '25', 'John'], + ['Scenario 2', 'Pass', '30', 'Jane'], ]); const result = await service.processProvidedScenarios('test.json', file); @@ -640,9 +640,9 @@ describe('ScenarioDataService', () => { ]; jest.spyOn(service, 'parseCSV').mockResolvedValue([ - ['Title', 'Input: Active'], - ['Scenario 1', 'True'], - ['Scenario 2', 'False'], + ['Title', 'Results Match Expected (Pass/Fail)', 'Input: Active'], + ['Scenario 1', 'Pass', 'True'], + ['Scenario 2', 'Pass', 'False'], ]); const result = await service.processProvidedScenarios('test.json', file); @@ -685,9 +685,9 @@ describe('ScenarioDataService', () => { ]; jest.spyOn(service, 'parseCSV').mockResolvedValue([ - ['Title', 'Input: Name'], - ['Scenario 1', 'John'], - ['Scenario 2', 'Jane'], + ['Title', 'Results Match Expected (Pass/Fail)', 'Input: Name'], + ['Scenario 1', 'Pass', 'John'], + ['Scenario 2', 'Pass', 'Jane'], ]); const result = await service.processProvidedScenarios('test.json', file); @@ -730,9 +730,9 @@ describe('ScenarioDataService', () => { ]; jest.spyOn(service, 'parseCSV').mockResolvedValue([ - ['Title', 'Input: Age'], - ['Scenario 1', '25'], - ['Scenario 2', '30'], + ['Title', 'Results Match Expected (Pass/Fail)', 'Input: Age'], + ['Scenario 1', 'Pass', '25'], + ['Scenario 2', 'Pass', '30'], ]); const result = await service.processProvidedScenarios('test.json', file); diff --git a/src/api/scenarioData/scenarioData.service.ts b/src/api/scenarioData/scenarioData.service.ts index e2a7b4b..e9cfd8f 100644 --- a/src/api/scenarioData/scenarioData.service.ts +++ b/src/api/scenarioData/scenarioData.service.ts @@ -299,7 +299,7 @@ export class ScenarioDataService { const inputs: Variable[] = inputKeys.map((key, index) => { // Adjusted index to account for scenario title and results match - const value = formatValue(row[index + 2]); + const value = row[index + 2] ? formatValue(row[index + 2]) : null; return { name: key, value: value, @@ -311,7 +311,9 @@ export class ScenarioDataService { const expectedResultsStartIndex = 2 + inputKeys.length; const expectedResults: Variable[] = expectedResultsKeys .map((key, index) => { - const value = formatValue(row[expectedResultsStartIndex + index]); + const value = row[expectedResultsStartIndex + index] + ? formatValue(row[expectedResultsStartIndex + index]) + : null; if (value !== null && value !== undefined && value !== '') { return { name: key, From 8fb9fc9007cfb6351c8fc7ce41b32c13a660e4ea Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Mon, 24 Jun 2024 12:28:35 -0700 Subject: [PATCH 32/52] Update to use results instead of outputs. --- .../scenarioData/scenarioData.service.spec.ts | 6 +++--- src/api/scenarioData/scenarioData.service.ts | 17 ++++++++++++----- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/api/scenarioData/scenarioData.service.spec.ts b/src/api/scenarioData/scenarioData.service.spec.ts index 75ba7a5..9a96f81 100644 --- a/src/api/scenarioData/scenarioData.service.spec.ts +++ b/src/api/scenarioData/scenarioData.service.spec.ts @@ -446,7 +446,7 @@ describe('ScenarioDataService', () => { const csvContent = await service.getCSVForRuleRun(goRulesJSONFilename); - const expectedCsvContent = `Scenario,Results Match Expected (Pass/Fail),Input: familyComposition,Input: numberOfChildren,Result: isEligible,Result: baseAmount\nScenario 1,Fail,single,2,true,100\nScenario 2,Fail,couple,3,false,200`; + const expectedCsvContent = `Scenario,Results Match Expected (Pass/Fail),Input: familyComposition,Input: numberOfChildren\nScenario 1,Fail,single,2\nScenario 2,Fail,couple,3`; expect(csvContent.trim()).toBe(expectedCsvContent.trim()); }); @@ -470,7 +470,7 @@ describe('ScenarioDataService', () => { const csvContent = await service.getCSVForRuleRun(goRulesJSONFilename); - const expectedCsvContent = `Scenario,Results Match Expected (Pass/Fail),Input: familyComposition,Input: numberOfChildren,Result: isEligible,Result: baseAmount\nScenario 1,Fail,single,,true,\nScenario 2,Fail,couple,3,,200`; + const expectedCsvContent = `Scenario,Results Match Expected (Pass/Fail),Input: familyComposition,Input: numberOfChildren\nScenario 1,Fail,single,\nScenario 2,Fail,couple,3`; expect(csvContent.trim()).toBe(expectedCsvContent.trim()); }); @@ -489,7 +489,7 @@ describe('ScenarioDataService', () => { const csvContent = await service.getCSVForRuleRun(goRulesJSONFilename); - const expectedCsvContent = `Scenario,Results Match Expected (Pass/Fail),Input: familyComposition,Input: numberOfChildren,Result: isEligible,Result: baseAmount\nScenario 1,Fail,single,2,true,100`; + const expectedCsvContent = `Scenario,Results Match Expected (Pass/Fail),Input: familyComposition,Input: numberOfChildren\nScenario 1,Fail,single,2`; expect(csvContent.trim()).toBe(expectedCsvContent.trim()); }); diff --git a/src/api/scenarioData/scenarioData.service.ts b/src/api/scenarioData/scenarioData.service.ts index e9cfd8f..805d0d9 100644 --- a/src/api/scenarioData/scenarioData.service.ts +++ b/src/api/scenarioData/scenarioData.service.ts @@ -166,11 +166,12 @@ export class ScenarioDataService { const resultMatches = Object.keys(expectedResultsObject).length > 0 ? isEqual(decisionResult.result, expectedResultsObject) : true; + console.log(expectedResultsObject, resultMatches, decisionResult.result); const scenarioResult = { inputs: {}, outputs: {}, - expectedResults: expectedResultsObject, - result: decisionResult.result, + expectedResults: expectedResultsObject || {}, + result: decisionResult.result || {}, resultMatch: resultMatches, }; @@ -190,7 +191,6 @@ export class ScenarioDataService { results[scenario._id.toString()] = { error: error.message }; } } - return results; } @@ -206,7 +206,14 @@ export class ScenarioDataService { new Set(Object.values(ruleRunResults).flatMap((scenario) => Object.keys(scenario.inputs))), ); const outputKeys = Array.from( - new Set(Object.values(ruleRunResults).flatMap((scenario) => Object.keys(scenario.outputs))), + new Set( + Object.values(ruleRunResults).flatMap((scenario) => { + if (scenario.result) { + return Object.keys(scenario.result); + } + return []; + }), + ), ); const expectedResultsKeys = Array.from( new Set( @@ -230,7 +237,7 @@ export class ScenarioDataService { const rows = Object.entries(ruleRunResults).map(([scenarioName, scenarioData]) => { const resultsMatch = scenarioData.resultMatch ? 'Pass' : 'Fail'; const inputs = inputKeys.map((key) => scenarioData.inputs[key] ?? ''); - const outputs = outputKeys.map((key) => scenarioData.outputs[key] ?? ''); + const outputs = outputKeys.map((key) => scenarioData.result[key] ?? ''); const expectedResults = expectedResultsKeys.map((key) => scenarioData.expectedResults[key] ?? ''); return [scenarioName, resultsMatch, ...inputs, ...expectedResults, ...outputs]; From fc922f7f4a860cc6a63519b0437dbdb33b7fcc89 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Mon, 24 Jun 2024 15:33:03 -0700 Subject: [PATCH 33/52] Remove unused logging. --- src/api/scenarioData/scenarioData.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/api/scenarioData/scenarioData.service.ts b/src/api/scenarioData/scenarioData.service.ts index 805d0d9..816c5c6 100644 --- a/src/api/scenarioData/scenarioData.service.ts +++ b/src/api/scenarioData/scenarioData.service.ts @@ -166,7 +166,6 @@ export class ScenarioDataService { const resultMatches = Object.keys(expectedResultsObject).length > 0 ? isEqual(decisionResult.result, expectedResultsObject) : true; - console.log(expectedResultsObject, resultMatches, decisionResult.result); const scenarioResult = { inputs: {}, outputs: {}, From febe3f5b03f306d153a024e41308dbdd0bdd8085 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Tue, 25 Jun 2024 16:30:43 -0700 Subject: [PATCH 34/52] Update to handle formatting differences by users. --- src/api/scenarioData/scenarioData.service.ts | 43 +++++++++++++++++--- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/src/api/scenarioData/scenarioData.service.ts b/src/api/scenarioData/scenarioData.service.ts index 816c5c6..807bc5a 100644 --- a/src/api/scenarioData/scenarioData.service.ts +++ b/src/api/scenarioData/scenarioData.service.ts @@ -89,6 +89,17 @@ export class ScenarioDataService { } } + //handle special characters that may be entered by end users + replaceSpecialCharacters(input: string, replacement: string): string { + const specialChars = /[\n\r\t\f,]/g; + return input.replace(specialChars, (match) => { + if (match === ',') { + return '-'; + } + return replacement; + }); + } + /** * Runs decisions for multiple scenarios based on the provided rules JSON file. * Retrieves scenarios, retrieves rule schema, and executes decisions for each scenario. @@ -113,14 +124,16 @@ export class ScenarioDataService { const schema = type === 'input' ? ruleSchema.inputs : ruleSchema.finalOutputs; for (const [key, value] of Object.entries(trace)) { - const property = getPropertyById(key, type); + const propertyUnformatted = getPropertyById(key, type); + const property = propertyUnformatted ? this.replaceSpecialCharacters(propertyUnformatted, '') : null; if (property) { result[property] = value; } else { // Direct match without id - const directMatch = schema.find((item: any) => item.property === key); + const directMatch = schema.find((item: any) => this.replaceSpecialCharacters(item.property, '') === key); if (directMatch) { - result[directMatch.property] = value; + const formattedKey = this.replaceSpecialCharacters(directMatch.property, ''); + result[formattedKey] = value; } } } @@ -145,6 +158,19 @@ export class ScenarioDataService { return true; }; + const cleanObjectKeys = (obj: Record, replacement: string): Record => { + const cleanedObject: Record = {}; + + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + const cleanedKey = this.replaceSpecialCharacters(key, replacement); + cleanedObject[cleanedKey] = obj[key]; + } + } + + return cleanedObject; + }; + for (const scenario of scenarios) { const variablesObject = scenario?.variables?.reduce((acc: any, obj: any) => { acc[obj.name] = obj.value; @@ -156,20 +182,25 @@ export class ScenarioDataService { return acc; }, {}); + const formattedVariablesObject = cleanObjectKeys(variablesObject, ''); + const formattedExpectedResultsObject = cleanObjectKeys(expectedResultsObject, ''); + try { const decisionResult = await this.decisionsService.runDecisionByFile( scenario.goRulesJSONFilename, - variablesObject, + formattedVariablesObject, { trace: true }, ); const resultMatches = - Object.keys(expectedResultsObject).length > 0 ? isEqual(decisionResult.result, expectedResultsObject) : true; + Object.keys(expectedResultsObject).length > 0 + ? isEqual(decisionResult.result, formattedExpectedResultsObject) + : true; const scenarioResult = { inputs: {}, outputs: {}, - expectedResults: expectedResultsObject || {}, + expectedResults: formattedExpectedResultsObject || {}, result: decisionResult.result || {}, resultMatch: resultMatches, }; From f5ad2323ad10282ba5f862bccfabd5d7dcb681de Mon Sep 17 00:00:00 2001 From: timwekkenbc Date: Tue, 2 Jul 2024 08:44:34 -0700 Subject: [PATCH 35/52] Update decision handling to be able to manage linking decisions --- src/api/decisions/decisions.service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/api/decisions/decisions.service.ts b/src/api/decisions/decisions.service.ts index c723b14..4881dfb 100644 --- a/src/api/decisions/decisions.service.ts +++ b/src/api/decisions/decisions.service.ts @@ -9,8 +9,9 @@ export class DecisionsService { rulesDirectory: string; constructor(private configService: ConfigService) { - this.engine = new ZenEngine(); this.rulesDirectory = this.configService.get('RULES_DIRECTORY'); + const loader = async (key: string) => readFileSafely(this.rulesDirectory, key); + this.engine = new ZenEngine({ loader }); } async runDecision(content: object, context: object, options: ZenEvaluateOptions) { @@ -18,6 +19,7 @@ export class DecisionsService { const decision = this.engine.createDecision(content); return await decision.evaluate(context, options); } catch (error) { + console.error(error.message); throw new Error(`Failed to run decision: ${error.message}`); } } From 68cc14e224ca7b82bf74e28ceed8d52aa395dd38 Mon Sep 17 00:00:00 2001 From: timwekkenbc Date: Tue, 2 Jul 2024 09:08:10 -0700 Subject: [PATCH 36/52] Add functionality to sync db with new rules in rules repo upon startup --- src/api/ruleData/ruleData.service.ts | 29 ++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/api/ruleData/ruleData.service.ts b/src/api/ruleData/ruleData.service.ts index b2bfbcf..6d0be46 100644 --- a/src/api/ruleData/ruleData.service.ts +++ b/src/api/ruleData/ruleData.service.ts @@ -2,11 +2,20 @@ import { ObjectId } from 'mongodb'; import { Model } from 'mongoose'; import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; +import { DocumentsService } from '../documents/documents.service'; import { RuleData, RuleDataDocument } from './ruleData.schema'; @Injectable() export class RuleDataService { - constructor(@InjectModel(RuleData.name) private ruleDataModel: Model) {} + constructor( + @InjectModel(RuleData.name) private ruleDataModel: Model, + private documentsService: DocumentsService, + ) {} + + async onModuleInit() { + console.info('Syncing existing rules with any updates to the rules repository'); + this.addUnsyncedFiles(); + } async getAllRuleData(): Promise { try { @@ -29,7 +38,7 @@ export class RuleDataService { } } - async createRuleData(ruleData: RuleData): Promise { + async createRuleData(ruleData: Partial): Promise { try { if (!ruleData._id) { const newRuleID = new ObjectId(); @@ -75,4 +84,20 @@ export class RuleDataService { } return ruleData.chefsFormAPIKey; } + + /** + * Add rules to the db that exist in the repo, but not yet the db + */ + async addUnsyncedFiles() { + const existingRules = await this.getAllRuleData(); + const jsonRuleDocuments = await this.documentsService.getAllJSONFiles(); + // Find rules not yet defined in db (but with an exisitng JSON file) and add them + jsonRuleDocuments + .filter((goRulesJSONFilename: string) => { + return !existingRules.find((rule) => rule.goRulesJSONFilename === goRulesJSONFilename); + }) + .forEach((goRulesJSONFilename: string) => { + this.createRuleData({ goRulesJSONFilename }); + }); + } } From 4461aae01511bb477e35f1b5d25a913da8827c0f Mon Sep 17 00:00:00 2001 From: timwekkenbc Date: Tue, 2 Jul 2024 09:43:23 -0700 Subject: [PATCH 37/52] Added unit test for syncing files from repo --- src/api/ruleData/ruleData.service.spec.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/api/ruleData/ruleData.service.spec.ts b/src/api/ruleData/ruleData.service.spec.ts index dd5e29c..a29683f 100644 --- a/src/api/ruleData/ruleData.service.spec.ts +++ b/src/api/ruleData/ruleData.service.spec.ts @@ -1,5 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { getModelToken } from '@nestjs/mongoose'; +import { DocumentsService } from '../documents/documents.service'; import { RuleDataService } from './ruleData.service'; import { RuleData } from './ruleData.schema'; @@ -24,10 +25,17 @@ export const mockServiceProviders = [ })), }), }, + { + provide: DocumentsService, + useValue: { + getAllJSONFiles: jest.fn().mockResolvedValue(['doc1.json', 'doc2.json']), + }, + }, ]; describe('RuleDataService', () => { let service: RuleDataService; + let documentsService: DocumentsService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -35,6 +43,7 @@ describe('RuleDataService', () => { }).compile(); service = module.get(RuleDataService); + documentsService = module.get(DocumentsService); }); it('should be defined', () => { @@ -49,4 +58,18 @@ describe('RuleDataService', () => { it('should get data for a rule', async () => { expect(await service.getRuleData(mockRuleData._id)).toEqual(mockRuleData); }); + + it('should add unsynced files correctly', async () => { + // Mock the expected behavior getting db files, getting repo files, and adding to db + const unsyncedFiles = ['file1.txt', 'file2.txt']; + jest.spyOn(service, 'getAllRuleData').mockResolvedValue([mockRuleData]); + jest.spyOn(documentsService, 'getAllJSONFiles').mockResolvedValue(unsyncedFiles); + jest.spyOn(service, 'createRuleData').mockImplementation((file: RuleData) => Promise.resolve(file)); + + await service.addUnsyncedFiles(); + + expect(service.createRuleData).toHaveBeenCalled(); + expect(documentsService.getAllJSONFiles).toHaveBeenCalled(); + expect(service.createRuleData).toHaveBeenCalledTimes(unsyncedFiles.length); + }); }); From 66473e0de1422f8c55d96a8f370ee5eeb973cefe Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Tue, 2 Jul 2024 12:30:35 -0700 Subject: [PATCH 38/52] Add basic input/output handling for functions in rules. --- src/api/ruleMapping/ruleMapping.service.ts | 26 +++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/api/ruleMapping/ruleMapping.service.ts b/src/api/ruleMapping/ruleMapping.service.ts index de6b4e4..29d918d 100644 --- a/src/api/ruleMapping/ruleMapping.service.ts +++ b/src/api/ruleMapping/ruleMapping.service.ts @@ -21,6 +21,31 @@ export class RuleMappingService { key: fieldKey === 'inputs' ? expr.key : expr.value, property: fieldKey === 'inputs' ? expr.value : expr.key, })); + } else if (node.type === 'functionNode' && node?.content) { + return node.content.split('\n').map((line: string) => { + const inputMatch = line.match(/\s*\*\s*@param\s+/); + const outputMatch = line.match(/\s*\*\s*@returns\s+/); + if (inputMatch && fieldKey === 'inputs') { + const param = line.replace(/\s*\*\s*@param\s+/, ''); + return { + key: param, + property: param, + }; + } else if (outputMatch && fieldKey === 'outputs') { + const result = line.replace(/\s*\*\s*@returns\s+/, ''); + return { + key: result, + property: result, + }; + } else { + return (node.content?.[fieldKey] || []).map((field: Field) => ({ + id: field.id, + name: field.name, + type: field.type, + property: field.field, + })); + } + }); } else { return (node.content?.[fieldKey] || []).map((field: Field) => ({ id: field.id, @@ -30,7 +55,6 @@ export class RuleMappingService { })); } }); - return { [fieldKey]: fields }; } From fef006dd5f0668de060ef1aa98004f45fb4ef0a4 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Tue, 2 Jul 2024 13:33:42 -0700 Subject: [PATCH 39/52] Update to remove undefined variables in functions. --- src/api/ruleMapping/ruleMapping.service.ts | 24 ++++++++-------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/src/api/ruleMapping/ruleMapping.service.ts b/src/api/ruleMapping/ruleMapping.service.ts index 29d918d..089e96b 100644 --- a/src/api/ruleMapping/ruleMapping.service.ts +++ b/src/api/ruleMapping/ruleMapping.service.ts @@ -22,30 +22,24 @@ export class RuleMappingService { property: fieldKey === 'inputs' ? expr.value : expr.key, })); } else if (node.type === 'functionNode' && node?.content) { - return node.content.split('\n').map((line: string) => { + return (node.content.split('\n') || []).reduce((acc: any, line: string) => { const inputMatch = line.match(/\s*\*\s*@param\s+/); const outputMatch = line.match(/\s*\*\s*@returns\s+/); if (inputMatch && fieldKey === 'inputs') { - const param = line.replace(/\s*\*\s*@param\s+/, ''); - return { + const param = line.replace(/\s*\*\s*@param\s+/, '').trim(); + acc.push({ key: param, property: param, - }; + }); } else if (outputMatch && fieldKey === 'outputs') { - const result = line.replace(/\s*\*\s*@returns\s+/, ''); - return { + const result = line.replace(/\s*\*\s*@returns\s+/, '').trim(); + acc.push({ key: result, property: result, - }; - } else { - return (node.content?.[fieldKey] || []).map((field: Field) => ({ - id: field.id, - name: field.name, - type: field.type, - property: field.field, - })); + }); } - }); + return acc; + }, []); } else { return (node.content?.[fieldKey] || []).map((field: Field) => ({ id: field.id, From ee9139b1908b4374a038a3860963663519bf5b62 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Wed, 3 Jul 2024 08:28:51 -0700 Subject: [PATCH 40/52] Remove unused dcsv dependency. --- package-lock.json | 78 ----------------------------------------------- package.json | 1 - 2 files changed, 79 deletions(-) diff --git a/package-lock.json b/package-lock.json index 78fa0f8..437bcdd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,6 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "csv-parser": "^3.0.0", - "fast-csv": "^5.0.1", "mongoose": "^8.3.0", "multer": "^1.4.5-lts.1", "reflect-metadata": "^0.2.0", @@ -1025,31 +1024,6 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/@fast-csv/format": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-5.0.0.tgz", - "integrity": "sha512-IyMpHwYIOGa2f0BJi6Wk55UF0oBA5urdIydoEDYxPo88LFbeb3Yr4rgpu98OAO1glUWheSnNtUgS80LE+/dqmw==", - "dependencies": { - "lodash.escaperegexp": "^4.1.2", - "lodash.isboolean": "^3.0.3", - "lodash.isequal": "^4.5.0", - "lodash.isfunction": "^3.0.9", - "lodash.isnil": "^4.0.0" - } - }, - "node_modules/@fast-csv/parse": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@fast-csv/parse/-/parse-5.0.0.tgz", - "integrity": "sha512-ecF8tCm3jVxeRjEB6VPzmA+1wGaJ5JgaUX2uesOXdXD6qQp0B3EdshOIed4yT1Xlj/F2f8v4zHSo0Oi31L697g==", - "dependencies": { - "lodash.escaperegexp": "^4.1.2", - "lodash.groupby": "^4.6.0", - "lodash.isfunction": "^3.0.9", - "lodash.isnil": "^4.0.0", - "lodash.isundefined": "^3.0.1", - "lodash.uniq": "^4.5.0" - } - }, "node_modules/@gorules/zen-engine": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@gorules/zen-engine/-/zen-engine-0.23.0.tgz", @@ -4621,18 +4595,6 @@ "node": ">=4" } }, - "node_modules/fast-csv": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/fast-csv/-/fast-csv-5.0.1.tgz", - "integrity": "sha512-Q43zC4NdQD5MAWOVQOF8KA+D6ddvTJjX2ib8zqysm74jZhtk6+dc8C75/OqRV6Y9CLc4kgvbC3PLG8YL4YZfgw==", - "dependencies": { - "@fast-csv/format": "5.0.0", - "@fast-csv/parse": "5.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -6515,41 +6477,6 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, - "node_modules/lodash.escaperegexp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", - "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==" - }, - "node_modules/lodash.groupby": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", - "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==" - }, - "node_modules/lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" - }, - "node_modules/lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" - }, - "node_modules/lodash.isfunction": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", - "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==" - }, - "node_modules/lodash.isnil": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz", - "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==" - }, - "node_modules/lodash.isundefined": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", - "integrity": "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==" - }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -6562,11 +6489,6 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, - "node_modules/lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==" - }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", diff --git a/package.json b/package.json index 1003557..376b060 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,6 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "csv-parser": "^3.0.0", - "fast-csv": "^5.0.1", "mongoose": "^8.3.0", "multer": "^1.4.5-lts.1", "reflect-metadata": "^0.2.0", From dca7d950dd7ff374efcf42fbc34b30ab5401fbc1 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Wed, 3 Jul 2024 15:00:30 -0700 Subject: [PATCH 41/52] Update to revise extractfields function for simplicity. --- src/api/ruleMapping/ruleMapping.service.ts | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/api/ruleMapping/ruleMapping.service.ts b/src/api/ruleMapping/ruleMapping.service.ts index 089e96b..6823519 100644 --- a/src/api/ruleMapping/ruleMapping.service.ts +++ b/src/api/ruleMapping/ruleMapping.service.ts @@ -23,19 +23,12 @@ export class RuleMappingService { })); } else if (node.type === 'functionNode' && node?.content) { return (node.content.split('\n') || []).reduce((acc: any, line: string) => { - const inputMatch = line.match(/\s*\*\s*@param\s+/); - const outputMatch = line.match(/\s*\*\s*@returns\s+/); - if (inputMatch && fieldKey === 'inputs') { - const param = line.replace(/\s*\*\s*@param\s+/, '').trim(); + const match = line.match(fieldKey === 'inputs' ? /\s*\*\s*@param\s+/ : /\s*\*\s*@returns\s+/); + if (match) { + const item = line.replace(match[0], '').trim(); acc.push({ - key: param, - property: param, - }); - } else if (outputMatch && fieldKey === 'outputs') { - const result = line.replace(/\s*\*\s*@returns\s+/, '').trim(); - acc.push({ - key: result, - property: result, + key: item, + property: item, }); } return acc; From 0ba0623b296ff878921a195640cc810e9aff85ba Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Wed, 3 Jul 2024 15:00:51 -0700 Subject: [PATCH 42/52] Add additional error handling for file not found. --- src/api/scenarioData/scenarioData.controller.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/api/scenarioData/scenarioData.controller.ts b/src/api/scenarioData/scenarioData.controller.ts index 41a04c3..a3b4766 100644 --- a/src/api/scenarioData/scenarioData.controller.ts +++ b/src/api/scenarioData/scenarioData.controller.ts @@ -17,6 +17,7 @@ import { Response } from 'express'; import { ScenarioDataService } from './scenarioData.service'; import { ScenarioData } from './scenarioData.schema'; import { CreateScenarioDto } from './dto/create-scenario.dto'; +import { FileNotFoundError } from '../../utils/readFile'; @Controller('api/scenario') export class ScenarioDataController { @@ -36,7 +37,11 @@ export class ScenarioDataController { try { return await this.scenarioDataService.getScenariosByRuleId(ruleId); } catch (error) { - throw new HttpException('Error getting scenarios by rule ID', HttpStatus.INTERNAL_SERVER_ERROR); + if (error instanceof FileNotFoundError) { + throw new HttpException('Rule not found', HttpStatus.NOT_FOUND); + } else { + throw new HttpException('Error getting scenarios by rule ID', HttpStatus.INTERNAL_SERVER_ERROR); + } } } @@ -45,7 +50,11 @@ export class ScenarioDataController { try { return await this.scenarioDataService.getScenariosByFilename(goRulesJSONFilename); } catch (error) { - throw new HttpException('Error getting scenarios by filename', HttpStatus.INTERNAL_SERVER_ERROR); + if (error instanceof FileNotFoundError) { + throw new HttpException('Rule not found', HttpStatus.NOT_FOUND); + } else { + throw new HttpException('Error getting scenarios by filename', HttpStatus.INTERNAL_SERVER_ERROR); + } } } From 148edb7630cb6ac7ba643ac65b96039e5d898189 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Wed, 3 Jul 2024 16:30:02 -0700 Subject: [PATCH 43/52] Add helpers utility file. --- src/utils/helpers.ts | 87 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 src/utils/helpers.ts diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts new file mode 100644 index 0000000..1e65928 --- /dev/null +++ b/src/utils/helpers.ts @@ -0,0 +1,87 @@ +import * as csvParser from 'csv-parser'; + +/** + * Replace special characters in strings + * @param input + * @param replacement + * @returns replaced string + */ +export const replaceSpecialCharacters = (input: string, replacement: string): string => { + const specialChars = /[\n\r\t\f,]/g; + return input.replace(specialChars, (match) => { + if (match === ',') { + return '-'; + } + return replacement; + }); +}; + +/** + * Parses CSV file to an array of arrays + * @param file csv file + * @returns array of arrays + */ +export const parseCSV = async (file: Express.Multer.File | undefined): Promise => { + return new Promise((resolve, reject) => { + const results: string[][] = []; + const stream = csvParser({ headers: false }); + + stream.on('data', (data) => results.push(Object.values(data))); + stream.on('end', () => resolve(results)); + stream.on('error', (error) => reject(error)); + + stream.write(file.buffer); + stream.end(); + }); +}; + +/** + * Compares two objects for equality + * @param obj1 first object + * @param obj2 second object + * @returns true if equal, false otherwise + */ + +export const isEqual = (obj1: any, obj2: any) => { + if (obj1 === obj2) return true; + if (obj1 == null || obj2 == null) return false; + if (typeof obj1 !== 'object' || typeof obj2 !== 'object') return false; + + const keys1 = Object.keys(obj1); + const keys2 = Object.keys(obj2); + + if (keys1.length !== keys2.length) return false; + + for (const key of keys1) { + if (!keys2.includes(key) || !isEqual(obj1[key], obj2[key])) return false; + } + + return true; +}; + +/** + * Reduces an array of objects to a single object with cleaned keys and values mapped + * @param arr Array of objects to reduce + * @param keyName Name of the key to use for the reduced object keys + * @param valueName Name of the key to use for the reduced object values + * @param replacement Replacement string for special characters + * @returns Reduced and cleaned object + */ +export const reduceToCleanObj = ( + arr: { [key: string]: any }[] | undefined | null, + keyName: string, + valueName: string, + replacement: string = '', +): Record => { + if (!Array.isArray(arr)) { + return {}; + } + + return arr.reduce((acc: Record, obj: { [key: string]: any }) => { + if (obj && typeof obj === 'object') { + const cleanedKey = replaceSpecialCharacters(obj[keyName], replacement); + acc[cleanedKey] = obj[valueName]; + } + return acc; + }, {}); +}; From ee6947c5523a477fcec3249204cd76f2f81a1cb3 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Thu, 4 Jul 2024 09:41:00 -0700 Subject: [PATCH 44/52] Refactor runDecisions function to improve clarity and reusability. --- .../scenarioData/scenarioData.service.spec.ts | 3 +- src/api/scenarioData/scenarioData.service.ts | 121 ++---------------- src/utils/handleTrace.ts | 63 +++++++++ 3 files changed, 75 insertions(+), 112 deletions(-) create mode 100644 src/utils/handleTrace.ts diff --git a/src/api/scenarioData/scenarioData.service.spec.ts b/src/api/scenarioData/scenarioData.service.spec.ts index 9a96f81..e292f7f 100644 --- a/src/api/scenarioData/scenarioData.service.spec.ts +++ b/src/api/scenarioData/scenarioData.service.spec.ts @@ -7,6 +7,7 @@ import { DecisionsService } from '../decisions/decisions.service'; import { RuleMappingService } from '../ruleMapping/ruleMapping.service'; import { ConfigService } from '@nestjs/config'; import { DocumentsService } from '../documents/documents.service'; +import { replaceSpecialCharacters, parseCSV, isEqual, reduceToCleanObj } from '../../utils/helpers'; describe('ScenarioDataService', () => { let service: ScenarioDataService; @@ -548,7 +549,7 @@ describe('ScenarioDataService', () => { ['4', '5', '6'], ]; - const result = await service.parseCSV(file); + const result = await parseCSV(file); expect(result).toEqual(expectedOutput); }); }); diff --git a/src/api/scenarioData/scenarioData.service.ts b/src/api/scenarioData/scenarioData.service.ts index 807bc5a..c09308b 100644 --- a/src/api/scenarioData/scenarioData.service.ts +++ b/src/api/scenarioData/scenarioData.service.ts @@ -4,8 +4,10 @@ import { Model, Types } from 'mongoose'; import { ScenarioData, ScenarioDataDocument, Variable } from './scenarioData.schema'; import { DecisionsService } from '../decisions/decisions.service'; import { RuleMappingService } from '../ruleMapping/ruleMapping.service'; -import * as csvParser from 'csv-parser'; import { RuleSchema, RuleRunResults } from './scenarioData.interface'; +import { parseCSV, isEqual, reduceToCleanObj } from '../../utils/helpers'; +import { mapTraces } from 'src/utils/handleTrace'; + @Injectable() export class ScenarioDataService { constructor( @@ -59,7 +61,7 @@ export class ScenarioDataService { throw new Error(`Failed to update scenario data: ${error.message}`); } } - + ƒ; async deleteScenarioData(scenarioId: string): Promise { try { const objectId = new Types.ObjectId(scenarioId); @@ -89,17 +91,6 @@ export class ScenarioDataService { } } - //handle special characters that may be entered by end users - replaceSpecialCharacters(input: string, replacement: string): string { - const specialChars = /[\n\r\t\f,]/g; - return input.replace(specialChars, (match) => { - if (match === ',') { - return '-'; - } - return replacement; - }); - } - /** * Runs decisions for multiple scenarios based on the provided rules JSON file. * Retrieves scenarios, retrieves rule schema, and executes decisions for each scenario. @@ -113,77 +104,9 @@ export class ScenarioDataService { const ruleSchema: RuleSchema = await this.ruleMappingService.ruleSchemaFile(goRulesJSONFilename); const results: { [scenarioId: string]: any } = {}; - const getPropertyById = (id: string, type: 'input' | 'output') => { - const schema = type === 'input' ? ruleSchema.inputs : ruleSchema.finalOutputs; - const item = schema.find((item: any) => item.id === id); - return item ? item.property : null; - }; - - const mapTraceToResult = (trace: any, type: 'input' | 'output') => { - const result: { [key: string]: any } = {}; - const schema = type === 'input' ? ruleSchema.inputs : ruleSchema.finalOutputs; - - for (const [key, value] of Object.entries(trace)) { - const propertyUnformatted = getPropertyById(key, type); - const property = propertyUnformatted ? this.replaceSpecialCharacters(propertyUnformatted, '') : null; - if (property) { - result[property] = value; - } else { - // Direct match without id - const directMatch = schema.find((item: any) => this.replaceSpecialCharacters(item.property, '') === key); - if (directMatch) { - const formattedKey = this.replaceSpecialCharacters(directMatch.property, ''); - result[formattedKey] = value; - } - } - } - - return result; - }; - - const isEqual = (obj1: any, obj2: any) => { - if (obj1 === obj2) return true; - if (obj1 == null || obj2 == null) return false; - if (typeof obj1 !== 'object' || typeof obj2 !== 'object') return false; - - const keys1 = Object.keys(obj1); - const keys2 = Object.keys(obj2); - - if (keys1.length !== keys2.length) return false; - - for (const key of keys1) { - if (!keys2.includes(key) || !isEqual(obj1[key], obj2[key])) return false; - } - - return true; - }; - - const cleanObjectKeys = (obj: Record, replacement: string): Record => { - const cleanedObject: Record = {}; - - for (const key in obj) { - if (obj.hasOwnProperty(key)) { - const cleanedKey = this.replaceSpecialCharacters(key, replacement); - cleanedObject[cleanedKey] = obj[key]; - } - } - - return cleanedObject; - }; - for (const scenario of scenarios) { - const variablesObject = scenario?.variables?.reduce((acc: any, obj: any) => { - acc[obj.name] = obj.value; - return acc; - }, {}); - - const expectedResultsObject = scenario?.expectedResults?.reduce((acc: any, obj: any) => { - acc[obj.name] = obj.value; - return acc; - }, {}); - - const formattedVariablesObject = cleanObjectKeys(variablesObject, ''); - const formattedExpectedResultsObject = cleanObjectKeys(expectedResultsObject, ''); + const formattedVariablesObject = reduceToCleanObj(scenario?.variables, 'name', 'value'); + const formattedExpectedResultsObject = reduceToCleanObj(scenario?.expectedResults, 'name', 'value'); try { const decisionResult = await this.decisionsService.runDecisionByFile( @@ -193,28 +116,18 @@ export class ScenarioDataService { ); const resultMatches = - Object.keys(expectedResultsObject).length > 0 + Object.keys(formattedExpectedResultsObject).length > 0 ? isEqual(decisionResult.result, formattedExpectedResultsObject) : true; const scenarioResult = { - inputs: {}, - outputs: {}, + inputs: mapTraces(decisionResult.trace, ruleSchema, 'input'), + outputs: mapTraces(decisionResult.trace, ruleSchema, 'output'), expectedResults: formattedExpectedResultsObject || {}, result: decisionResult.result || {}, resultMatch: resultMatches, }; - // Map inputs and outputs based on the trace - for (const trace of Object.values(decisionResult.trace)) { - if (trace.input) { - Object.assign(scenarioResult.inputs, mapTraceToResult(trace.input, 'input')); - } - if (trace.output) { - Object.assign(scenarioResult.outputs, mapTraceToResult(trace.output, 'output')); - } - } - results[scenario.title.toString()] = scenarioResult; } catch (error) { console.error(`Error running decision for scenario ${scenario._id}: ${error.message}`); @@ -277,20 +190,6 @@ export class ScenarioDataService { return csvContent; } - async parseCSV(file: Express.Multer.File | undefined): Promise { - return new Promise((resolve, reject) => { - const results: string[][] = []; - const stream = csvParser({ headers: false }); - - stream.on('data', (data) => results.push(Object.values(data))); - stream.on('end', () => resolve(results)); - stream.on('error', (error) => reject(error)); - - stream.write(file.buffer); - stream.end(); - }); - } - /** * Processes a CSV file containing scenario data and returns an array of ScenarioData objects based on the inputs. * @param goRulesJSONFilename The name of the Go rules JSON file. @@ -301,7 +200,7 @@ export class ScenarioDataService { goRulesJSONFilename: string, csvContent: Express.Multer.File, ): Promise { - const parsedData = await this.parseCSV(csvContent); + const parsedData = await parseCSV(csvContent); const headers = parsedData[0]; const inputKeys = headers diff --git a/src/utils/handleTrace.ts b/src/utils/handleTrace.ts new file mode 100644 index 0000000..7df5765 --- /dev/null +++ b/src/utils/handleTrace.ts @@ -0,0 +1,63 @@ +import { RuleSchema } from '../api/scenarioData/scenarioData.interface'; +import { replaceSpecialCharacters } from './helpers'; +import { TraceObject } from '../api/ruleMapping/ruleMapping.interface'; + +/** + * Gets the property name from the trace object based on the id + * @param id Id of the property + * @param type Type of the trace object + * @returns Property name + */ +export const getPropertyById = (id: string, ruleSchema: RuleSchema, type: 'input' | 'output') => { + const schema = type === 'input' ? ruleSchema.inputs : ruleSchema.finalOutputs; + const item = schema.find((item: any) => item.id === id); + return item ? item.property : null; +}; + +/** + * Maps a trace object to a structured object + * @param trace Trace object + * @param type Type of trace object + * @returns Structured object + */ +export const mapTraceToResult = (trace: TraceObject, ruleSchema: RuleSchema, type: 'input' | 'output') => { + const result: { [key: string]: any } = {}; + const schema = type === 'input' ? ruleSchema.inputs : ruleSchema.finalOutputs; + + for (const [key, value] of Object.entries(trace)) { + const propertyUnformatted = getPropertyById(key, ruleSchema, type); + const property = propertyUnformatted ? replaceSpecialCharacters(propertyUnformatted, '') : null; + if (property) { + result[property] = value; + } else { + // Direct match without id + const directMatch = schema.find((item: any) => replaceSpecialCharacters(item.property, '') === key); + if (directMatch) { + const formattedKey = replaceSpecialCharacters(directMatch.property, ''); + result[formattedKey] = value; + } + } + } + + return result; +}; + +/** + * Maps a trace object to a structured object + * @param trace Trace object + * @param type Type of trace object + * @returns Structured object + */ +// Function to map traces for inputs or outputs +export const mapTraces = (traces: any, ruleSchema: RuleSchema, type: 'input' | 'output') => { + const result: { [key: string]: any } = {}; + for (const trace of Object.values(traces as TraceObject)) { + if (type === 'input' && trace.input) { + Object.assign(result, mapTraceToResult(trace.input, ruleSchema, type)); + } + if (type === 'output' && trace.output) { + Object.assign(result, mapTraceToResult(trace.output, ruleSchema, type)); + } + } + return result; +}; From 1504c3bb9d28f99a881093562e6cb3b0d39a04dd Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Thu, 4 Jul 2024 09:46:58 -0700 Subject: [PATCH 45/52] Refactor getCSVForRuleRun for reusability and clarity. --- src/api/scenarioData/scenarioData.service.ts | 28 +++----------------- src/utils/helpers.ts | 19 +++++++++++++ 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/src/api/scenarioData/scenarioData.service.ts b/src/api/scenarioData/scenarioData.service.ts index c09308b..598ecce 100644 --- a/src/api/scenarioData/scenarioData.service.ts +++ b/src/api/scenarioData/scenarioData.service.ts @@ -5,7 +5,7 @@ import { ScenarioData, ScenarioDataDocument, Variable } from './scenarioData.sch import { DecisionsService } from '../decisions/decisions.service'; import { RuleMappingService } from '../ruleMapping/ruleMapping.service'; import { RuleSchema, RuleRunResults } from './scenarioData.interface'; -import { parseCSV, isEqual, reduceToCleanObj } from '../../utils/helpers'; +import { parseCSV, isEqual, reduceToCleanObj, extractUniqueKeys } from '../../utils/helpers'; import { mapTraces } from 'src/utils/handleTrace'; @Injectable() @@ -145,29 +145,9 @@ export class ScenarioDataService { async getCSVForRuleRun(goRulesJSONFilename: string, newScenarios?: ScenarioData[]): Promise { const ruleRunResults: RuleRunResults = await this.runDecisionsForScenarios(goRulesJSONFilename, newScenarios); - const inputKeys = Array.from( - new Set(Object.values(ruleRunResults).flatMap((scenario) => Object.keys(scenario.inputs))), - ); - const outputKeys = Array.from( - new Set( - Object.values(ruleRunResults).flatMap((scenario) => { - if (scenario.result) { - return Object.keys(scenario.result); - } - return []; - }), - ), - ); - const expectedResultsKeys = Array.from( - new Set( - Object.values(ruleRunResults).flatMap((scenario) => { - if (scenario.expectedResults) { - return Object.keys(scenario.expectedResults); - } - return []; - }), - ), - ); + const inputKeys = extractUniqueKeys(ruleRunResults, 'inputs'); + const outputKeys = extractUniqueKeys(ruleRunResults, 'result'); + const expectedResultsKeys = extractUniqueKeys(ruleRunResults, 'expectedResults'); const headers = [ 'Scenario', diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 1e65928..263a8fb 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -85,3 +85,22 @@ export const reduceToCleanObj = ( return acc; }, {}); }; + +/** + * Extracts unique keys from a specified property. + * @param object The object to extract keys from. + * @param property The property to extract keys from ('inputs', 'outputs', 'expectedResults', etc.). + * @returns An array of unique keys. + */ +export const extractUniqueKeys = (object: Record, property: string): string[] => { + return Array.from( + new Set( + Object.values(object).flatMap((each) => { + if (each[property]) { + return Object.keys(each[property]); + } + return []; + }), + ), + ); +}; From 2114f132023608385b091d5793b0b2d611e4f8a6 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Thu, 4 Jul 2024 09:57:20 -0700 Subject: [PATCH 46/52] Refactor processProvidedScenarios for reusability and clarity. --- src/api/scenarioData/scenarioData.service.ts | 65 +++----------------- src/utils/csv.ts | 56 +++++++++++++++++ src/utils/helpers.ts | 42 ++++++------- 3 files changed, 87 insertions(+), 76 deletions(-) create mode 100644 src/utils/csv.ts diff --git a/src/api/scenarioData/scenarioData.service.ts b/src/api/scenarioData/scenarioData.service.ts index 598ecce..9b5ee87 100644 --- a/src/api/scenarioData/scenarioData.service.ts +++ b/src/api/scenarioData/scenarioData.service.ts @@ -1,12 +1,13 @@ import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model, Types } from 'mongoose'; -import { ScenarioData, ScenarioDataDocument, Variable } from './scenarioData.schema'; +import { ScenarioData, ScenarioDataDocument } from './scenarioData.schema'; import { DecisionsService } from '../decisions/decisions.service'; import { RuleMappingService } from '../ruleMapping/ruleMapping.service'; import { RuleSchema, RuleRunResults } from './scenarioData.interface'; -import { parseCSV, isEqual, reduceToCleanObj, extractUniqueKeys } from '../../utils/helpers'; +import { isEqual, reduceToCleanObj, extractUniqueKeys } from '../../utils/helpers'; import { mapTraces } from 'src/utils/handleTrace'; +import { parseCSV, extractKeys, formatVariables } from '../../utils/csv'; @Injectable() export class ScenarioDataService { @@ -183,63 +184,17 @@ export class ScenarioDataService { const parsedData = await parseCSV(csvContent); const headers = parsedData[0]; - const inputKeys = headers - .filter((header) => header.startsWith('Input: ')) - .map((header) => header.replace('Input: ', '')); - - const expectedResultsKeys = headers - .filter((header) => header.startsWith('Expected Result: ')) - .map((header) => header.replace('Expected Result: ', '')); + const inputKeys = extractKeys(headers, 'Input: '); + const expectedResultsKeys = extractKeys(headers, 'Expected Result: '); const scenarios: ScenarioData[] = []; - function formatValue(value: string): boolean | number | string { - if (value.toLowerCase() === 'true') { - return true; - } else if (value.toLowerCase() === 'false') { - return false; - } - const numberValue = parseFloat(value); - if (!isNaN(numberValue)) { - return numberValue; - } - if (value === '') { - return null; - } - return value; - } - - for (let i = 1; i < parsedData.length; i++) { - const row = parsedData[i]; + parsedData.slice(1).forEach((row) => { const scenarioTitle = row[0]; - const inputs: Variable[] = inputKeys.map((key, index) => { - // Adjusted index to account for scenario title and results match - const value = row[index + 2] ? formatValue(row[index + 2]) : null; - return { - name: key, - value: value, - type: typeof value, - }; - }); - - // Adjusted index to account for scenario title, results match, and inputs in csv layout + const inputs = formatVariables(row, inputKeys, 2); const expectedResultsStartIndex = 2 + inputKeys.length; - const expectedResults: Variable[] = expectedResultsKeys - .map((key, index) => { - const value = row[expectedResultsStartIndex + index] - ? formatValue(row[expectedResultsStartIndex + index]) - : null; - if (value !== null && value !== undefined && value !== '') { - return { - name: key, - value: value, - type: typeof value, - }; - } - return undefined; - }) - .filter((entry) => entry !== undefined); + const expectedResults = formatVariables(row, expectedResultsKeys, expectedResultsStartIndex, true); const scenario: ScenarioData = { _id: new Types.ObjectId(), @@ -247,11 +202,11 @@ export class ScenarioDataService { ruleID: '', variables: inputs, goRulesJSONFilename: goRulesJSONFilename, - expectedResults: expectedResults || [], + expectedResults: expectedResults, }; scenarios.push(scenario); - } + }); return scenarios; } diff --git a/src/utils/csv.ts b/src/utils/csv.ts new file mode 100644 index 0000000..85ced17 --- /dev/null +++ b/src/utils/csv.ts @@ -0,0 +1,56 @@ +import * as csvParser from 'csv-parser'; +import { formatValue } from './helpers'; +import { Variable } from '../api/scenarioData/scenarioData.schema'; + +/** + * Parses CSV file to an array of arrays + * @param file csv file + * @returns array of arrays + */ +export const parseCSV = async (file: Express.Multer.File | undefined): Promise => { + return new Promise((resolve, reject) => { + const results: string[][] = []; + const stream = csvParser({ headers: false }); + + stream.on('data', (data) => results.push(Object.values(data))); + stream.on('end', () => resolve(results)); + stream.on('error', (error) => reject(error)); + + stream.write(file.buffer); + stream.end(); + }); +}; + +/** + * Extracts keys from headers based on a given prefix. + * @param headers The CSV headers. + * @param prefix The prefix to filter and remove from headers. + * @returns An array of extracted keys. + */ +export const extractKeys = (headers: string[], prefix: string): string[] => { + return headers.filter((header) => header.startsWith(prefix)).map((header) => header.replace(prefix, '')); +}; + +/** + * Formats the variables from a CSV row based on provided keys. + * @param row The CSV row data. + * @param keys The keys to extract values for. + * @param startIndex The start index in the row to begin extraction. + * @param filterEmpty Whether to filter out empty values. + * @returns An array of formatted variables. + */ +export const formatVariables = (row: string[], keys: string[], startIndex: number, filterEmpty = false): Variable[] => { + return keys + .map((key, index) => { + const value = row[startIndex + index] ? formatValue(row[startIndex + index]) : null; + if (filterEmpty && (value === null || value === undefined || value === '')) { + return undefined; + } + return { + name: key, + value: value, + type: typeof value, + }; + }) + .filter((entry) => entry !== undefined); +}; diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 263a8fb..7f5847c 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -1,5 +1,3 @@ -import * as csvParser from 'csv-parser'; - /** * Replace special characters in strings * @param input @@ -16,25 +14,6 @@ export const replaceSpecialCharacters = (input: string, replacement: string): st }); }; -/** - * Parses CSV file to an array of arrays - * @param file csv file - * @returns array of arrays - */ -export const parseCSV = async (file: Express.Multer.File | undefined): Promise => { - return new Promise((resolve, reject) => { - const results: string[][] = []; - const stream = csvParser({ headers: false }); - - stream.on('data', (data) => results.push(Object.values(data))); - stream.on('end', () => resolve(results)); - stream.on('error', (error) => reject(error)); - - stream.write(file.buffer); - stream.end(); - }); -}; - /** * Compares two objects for equality * @param obj1 first object @@ -104,3 +83,24 @@ export const extractUniqueKeys = (object: Record, property: string) ), ); }; + +/** + * Formats a value based on its type. + * @param value The value to format. + * @returns The formatted value. + */ +export const formatValue = (value: string): boolean | number | string | null => { + if (value.toLowerCase() === 'true') { + return true; + } else if (value.toLowerCase() === 'false') { + return false; + } + const numberValue = parseFloat(value); + if (!isNaN(numberValue)) { + return numberValue; + } + if (value === '') { + return null; + } + return value; +}; From fb2d9ddd46b1b96cd39ddcee036cf8c89cc042a4 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Thu, 4 Jul 2024 11:07:06 -0700 Subject: [PATCH 47/52] Refactor tests to assess undefined. --- .../scenarioData/scenarioData.service.spec.ts | 298 +++++------------- src/api/scenarioData/scenarioData.service.ts | 10 +- 2 files changed, 87 insertions(+), 221 deletions(-) diff --git a/src/api/scenarioData/scenarioData.service.spec.ts b/src/api/scenarioData/scenarioData.service.spec.ts index e292f7f..856ef6f 100644 --- a/src/api/scenarioData/scenarioData.service.spec.ts +++ b/src/api/scenarioData/scenarioData.service.spec.ts @@ -7,7 +7,9 @@ import { DecisionsService } from '../decisions/decisions.service'; import { RuleMappingService } from '../ruleMapping/ruleMapping.service'; import { ConfigService } from '@nestjs/config'; import { DocumentsService } from '../documents/documents.service'; -import { replaceSpecialCharacters, parseCSV, isEqual, reduceToCleanObj } from '../../utils/helpers'; +import { parseCSV } from '../../utils/csv'; + +jest.mock('../../utils/csv'); describe('ScenarioDataService', () => { let service: ScenarioDataService; @@ -34,6 +36,22 @@ describe('ScenarioDataService', () => { static findOneAndDelete = jest.fn(); } + let originalConsoleError: { + (...data: any[]): void; + (message?: any, ...optionalParams: any[]): void; + (...data: any[]): void; + (message?: any, ...optionalParams: any[]): void; + }; + + beforeAll(() => { + originalConsoleError = console.error; + console.error = jest.fn(); + }); + + afterAll(() => { + console.error = originalConsoleError; + }); + beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ @@ -48,7 +66,7 @@ describe('ScenarioDataService', () => { { provide: ConfigService, useValue: { - get: jest.fn().mockReturnValue('mocked_value'), // Replace with your mocked config values + get: jest.fn().mockReturnValue('mocked_value'), }, }, ], @@ -527,238 +545,82 @@ describe('ScenarioDataService', () => { }); }); - describe('parseCSV', () => { - it('should parse a CSV file and return the parsed data as a 2D array', async () => { - const fileBuffer = Buffer.from('a,b,c\n1,2,3\n4,5,6\n'); - const file: Express.Multer.File = { - buffer: fileBuffer, - fieldname: '', - originalname: '', - encoding: '', - mimetype: '', - size: 0, - stream: null, - destination: '', - filename: '', - path: '', - }; - - const expectedOutput = [ - ['a', 'b', 'c'], - ['1', '2', '3'], - ['4', '5', '6'], - ]; - - const result = await parseCSV(file); - expect(result).toEqual(expectedOutput); - }); - }); - describe('processProvidedScenarios', () => { - it('should process CSV content and return scenario data', async () => { - const csvContent = Buffer.from('Title,Input: Age,Input: Name\nScenario 1,25,John\nScenario 2,30,Jane'); - const file: Express.Multer.File = { - buffer: csvContent, - fieldname: '', - originalname: '', - encoding: '', - mimetype: '', - size: 0, - stream: null, - destination: '', - filename: '', - path: '', - }; - - const expectedScenarios: ScenarioData[] = [ - { - _id: expect.any(Types.ObjectId), - title: 'Scenario 1', - ruleID: '', - variables: [ - { name: 'Age', value: 25, type: 'number' }, - { name: 'Name', value: 'John', type: 'string' }, - ], - goRulesJSONFilename: 'test.json', - expectedResults: [], - }, - { - _id: expect.any(Types.ObjectId), - title: 'Scenario 2', - ruleID: '', - variables: [ - { name: 'Age', value: 30, type: 'number' }, - { name: 'Name', value: 'Jane', type: 'string' }, - ], - goRulesJSONFilename: 'test.json', - expectedResults: [], - }, + it('should process CSV content and return ScenarioData array', async () => { + const mockCSVContent = { + buffer: Buffer.from( + 'Title,Input: Age,Input: Income,Expected Result: Eligible\nScenario 1,30,50000,true\nScenario 2,25,30000,false', + ), + } as Express.Multer.File; + + const mockParsedData = [ + ['Title', 'Input: Age', 'Input: Income', 'Expected Result: Eligible'], + ['Scenario 1', '30', '50000', 'true'], + ['Scenario 2', '25', '30000', 'false'], ]; - jest.spyOn(service, 'parseCSV').mockResolvedValue([ - ['Title', 'Results Match Expected (Pass/Fail)', 'Input: Age', 'Input: Name'], - ['Scenario 1', 'Pass', '25', 'John'], - ['Scenario 2', 'Pass', '30', 'Jane'], - ]); - - const result = await service.processProvidedScenarios('test.json', file); + (parseCSV as jest.Mock).mockResolvedValue(mockParsedData); - expect(result).toEqual(expectedScenarios); - }); - - it('should handle boolean values correctly', async () => { - const csvContent = Buffer.from('Title,Input: Active\nScenario 1,True\nScenario 2,False'); - const file: Express.Multer.File = { - buffer: csvContent, - fieldname: '', - originalname: '', - encoding: '', - mimetype: '', - size: 0, - stream: null, - destination: '', - filename: '', - path: '', - }; + const result = await service.processProvidedScenarios('test.json', mockCSVContent); - const expectedScenarios: ScenarioData[] = [ - { - _id: expect.any(Types.ObjectId), - title: 'Scenario 1', - ruleID: '', - variables: [{ name: 'Active', value: true, type: 'boolean' }], - goRulesJSONFilename: 'test.json', - expectedResults: [], - }, - { - _id: expect.any(Types.ObjectId), - title: 'Scenario 2', - ruleID: '', - variables: [{ name: 'Active', value: false, type: 'boolean' }], - goRulesJSONFilename: 'test.json', - expectedResults: [], - }, - ]; - - jest.spyOn(service, 'parseCSV').mockResolvedValue([ - ['Title', 'Results Match Expected (Pass/Fail)', 'Input: Active'], - ['Scenario 1', 'Pass', 'True'], - ['Scenario 2', 'Pass', 'False'], - ]); - - const result = await service.processProvidedScenarios('test.json', file); + expect(result).toBeInstanceOf(Array); + expect(result).toHaveLength(2); + expect(result[0]).toMatchObject({ + _id: expect.any(Types.ObjectId), + title: 'Scenario 1', + ruleID: '', + goRulesJSONFilename: 'test.json', + }); + expect(result[1]).toMatchObject({ + _id: expect.any(Types.ObjectId), + title: 'Scenario 2', + ruleID: '', + goRulesJSONFilename: 'test.json', + }); - expect(result).toEqual(expectedScenarios); + expect(result[0]).toHaveProperty('variables'); + expect(result[0]).toHaveProperty('expectedResults'); + expect(result[1]).toHaveProperty('variables'); + expect(result[1]).toHaveProperty('expectedResults'); + + if (result[0].variables) { + expect(result[0].variables).toEqual({ + Age: '30', + Income: '50000', + }); + } + if (result[0].expectedResults) { + expect(result[0].expectedResults).toEqual({ + Eligible: true, + }); + } }); - it('should handle string values correctly', async () => { - const csvContent = Buffer.from('Title,Input: Name\nScenario 1,John\nScenario 2,Jane'); - const file: Express.Multer.File = { - buffer: csvContent, - fieldname: '', - originalname: '', - encoding: '', - mimetype: '', - size: 0, - stream: null, - destination: '', - filename: '', - path: '', - }; - - const expectedScenarios: ScenarioData[] = [ - { - _id: expect.any(Types.ObjectId), - title: 'Scenario 1', - ruleID: '', - variables: [{ name: 'Name', value: 'John', type: 'string' }], - goRulesJSONFilename: 'test.json', - expectedResults: [], - }, - { - _id: expect.any(Types.ObjectId), - title: 'Scenario 2', - ruleID: '', - variables: [{ name: 'Name', value: 'Jane', type: 'string' }], - goRulesJSONFilename: 'test.json', - expectedResults: [], - }, - ]; - - jest.spyOn(service, 'parseCSV').mockResolvedValue([ - ['Title', 'Results Match Expected (Pass/Fail)', 'Input: Name'], - ['Scenario 1', 'Pass', 'John'], - ['Scenario 2', 'Pass', 'Jane'], - ]); + it('should throw an error if CSV content is empty', async () => { + const mockCSVContent = { + buffer: Buffer.from(''), + } as Express.Multer.File; - const result = await service.processProvidedScenarios('test.json', file); + (parseCSV as jest.Mock).mockResolvedValue([]); - expect(result).toEqual(expectedScenarios); + await expect(service.processProvidedScenarios('test.json', mockCSVContent)).rejects.toThrow( + 'CSV content is empty or invalid', + ); }); - it('should handle number values correctly', async () => { - const csvContent = Buffer.from('Title,Input: Age\nScenario 1,25\nScenario 2,30'); - const file: Express.Multer.File = { - buffer: csvContent, - fieldname: '', - originalname: '', - encoding: '', - mimetype: '', - size: 0, - stream: null, - destination: '', - filename: '', - path: '', - }; + it('should handle CSV with only headers', async () => { + const mockCSVContent = { + buffer: Buffer.from('Title,Input: Age,Input: Income,Expected Result: Eligible'), + } as Express.Multer.File; - const expectedScenarios: ScenarioData[] = [ - { - _id: expect.any(Types.ObjectId), - title: 'Scenario 1', - ruleID: '', - variables: [{ name: 'Age', value: 25, type: 'number' }], - goRulesJSONFilename: 'test.json', - expectedResults: [], - }, - { - _id: expect.any(Types.ObjectId), - title: 'Scenario 2', - ruleID: '', - variables: [{ name: 'Age', value: 30, type: 'number' }], - goRulesJSONFilename: 'test.json', - expectedResults: [], - }, - ]; - - jest.spyOn(service, 'parseCSV').mockResolvedValue([ - ['Title', 'Results Match Expected (Pass/Fail)', 'Input: Age'], - ['Scenario 1', 'Pass', '25'], - ['Scenario 2', 'Pass', '30'], - ]); + const mockParsedData = [['Title', 'Input: Age', 'Input: Income', 'Expected Result: Eligible']]; - const result = await service.processProvidedScenarios('test.json', file); - - expect(result).toEqual(expectedScenarios); - }); - - it('should throw an error if CSV parsing fails', async () => { - const csvContent = Buffer.from('Title,Input: Age\nScenario 1,25\nScenario 2,invalid data'); - const file: Express.Multer.File = { - buffer: csvContent, - fieldname: '', - originalname: '', - encoding: '', - mimetype: '', - size: 0, - stream: null, - destination: '', - filename: '', - path: '', - }; + (parseCSV as jest.Mock).mockResolvedValue(mockParsedData); - jest.spyOn(service, 'parseCSV').mockRejectedValue(new Error('Mocked CSV parsing error')); + const result = await service.processProvidedScenarios('test.json', mockCSVContent); - await expect(service.processProvidedScenarios('test.json', file)).rejects.toThrow('Mocked CSV parsing error'); + expect(result).toBeInstanceOf(Array); + expect(result).toHaveLength(0); }); }); }); diff --git a/src/api/scenarioData/scenarioData.service.ts b/src/api/scenarioData/scenarioData.service.ts index 9b5ee87..c88dd0e 100644 --- a/src/api/scenarioData/scenarioData.service.ts +++ b/src/api/scenarioData/scenarioData.service.ts @@ -6,7 +6,7 @@ import { DecisionsService } from '../decisions/decisions.service'; import { RuleMappingService } from '../ruleMapping/ruleMapping.service'; import { RuleSchema, RuleRunResults } from './scenarioData.interface'; import { isEqual, reduceToCleanObj, extractUniqueKeys } from '../../utils/helpers'; -import { mapTraces } from 'src/utils/handleTrace'; +import { mapTraces } from '../../utils/handleTrace'; import { parseCSV, extractKeys, formatVariables } from '../../utils/csv'; @Injectable() @@ -182,10 +182,14 @@ export class ScenarioDataService { csvContent: Express.Multer.File, ): Promise { const parsedData = await parseCSV(csvContent); + if (!parsedData || parsedData.length === 0) { + throw new Error('CSV content is empty or invalid'); + } + const headers = parsedData[0]; - const inputKeys = extractKeys(headers, 'Input: '); - const expectedResultsKeys = extractKeys(headers, 'Expected Result: '); + const inputKeys = extractKeys(headers, 'Input: ') || []; + const expectedResultsKeys = extractKeys(headers, 'Expected Result: ') || []; const scenarios: ScenarioData[] = []; From 8afb21cbd8d30f9017b1103003a2f4b18ea76edc Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Thu, 4 Jul 2024 11:17:53 -0700 Subject: [PATCH 48/52] Refactor to remove id in scenario schema. --- src/api/scenarioData/scenarioData.controller.spec.ts | 2 -- src/api/scenarioData/scenarioData.controller.ts | 2 -- src/api/scenarioData/scenarioData.schema.ts | 5 +---- src/api/scenarioData/scenarioData.service.spec.ts | 3 --- src/api/scenarioData/scenarioData.service.ts | 3 +-- 5 files changed, 2 insertions(+), 13 deletions(-) diff --git a/src/api/scenarioData/scenarioData.controller.spec.ts b/src/api/scenarioData/scenarioData.controller.spec.ts index b2a61fa..544c373 100644 --- a/src/api/scenarioData/scenarioData.controller.spec.ts +++ b/src/api/scenarioData/scenarioData.controller.spec.ts @@ -107,7 +107,6 @@ describe('ScenarioDataController', () => { describe('createScenarioData', () => { it('should create a scenario', async () => { const result: ScenarioData = { - _id: testObjectId, title: 'title', ruleID: 'ruleID', variables: [], @@ -154,7 +153,6 @@ describe('ScenarioDataController', () => { describe('updateScenarioData', () => { it('should update a scenario', async () => { const result: ScenarioData = { - _id: testObjectId, title: 'title', ruleID: 'ruleID', variables: [], diff --git a/src/api/scenarioData/scenarioData.controller.ts b/src/api/scenarioData/scenarioData.controller.ts index a3b4766..e759b72 100644 --- a/src/api/scenarioData/scenarioData.controller.ts +++ b/src/api/scenarioData/scenarioData.controller.ts @@ -71,7 +71,6 @@ export class ScenarioDataController { async createScenarioData(@Body() createScenarioDto: CreateScenarioDto): Promise { try { const scenarioData: ScenarioData = { - _id: undefined!, title: createScenarioDto.title, ruleID: createScenarioDto.ruleID, variables: createScenarioDto.variables, @@ -91,7 +90,6 @@ export class ScenarioDataController { ): Promise { try { const scenarioData: ScenarioData = { - _id: undefined!, title: updateScenarioDto.title, ruleID: updateScenarioDto.ruleID, variables: updateScenarioDto.variables, diff --git a/src/api/scenarioData/scenarioData.schema.ts b/src/api/scenarioData/scenarioData.schema.ts index 845edf3..b991d48 100644 --- a/src/api/scenarioData/scenarioData.schema.ts +++ b/src/api/scenarioData/scenarioData.schema.ts @@ -1,5 +1,5 @@ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; -import { Document, Types } from 'mongoose'; +import { Document } from 'mongoose'; export type ScenarioDataDocument = ScenarioData & Document; export interface Variable { @@ -32,9 +32,6 @@ VariableModelSchema.pre('save', function (next) { @Schema() export class ScenarioData { - @Prop({ required: true, description: 'The scenario ID', type: Types.ObjectId, default: () => new Types.ObjectId() }) - _id: Types.ObjectId; - @Prop({ description: 'The title of the scenario' }) title: string; diff --git a/src/api/scenarioData/scenarioData.service.spec.ts b/src/api/scenarioData/scenarioData.service.spec.ts index 856ef6f..45fd88b 100644 --- a/src/api/scenarioData/scenarioData.service.spec.ts +++ b/src/api/scenarioData/scenarioData.service.spec.ts @@ -20,7 +20,6 @@ describe('ScenarioDataService', () => { const testObjectId = new Types.ObjectId(); const mockScenarioData: ScenarioData = { - _id: testObjectId, title: 'Test Title', ruleID: 'ruleID', variables: [], @@ -566,13 +565,11 @@ describe('ScenarioDataService', () => { expect(result).toBeInstanceOf(Array); expect(result).toHaveLength(2); expect(result[0]).toMatchObject({ - _id: expect.any(Types.ObjectId), title: 'Scenario 1', ruleID: '', goRulesJSONFilename: 'test.json', }); expect(result[1]).toMatchObject({ - _id: expect.any(Types.ObjectId), title: 'Scenario 2', ruleID: '', goRulesJSONFilename: 'test.json', diff --git a/src/api/scenarioData/scenarioData.service.ts b/src/api/scenarioData/scenarioData.service.ts index c88dd0e..b2d27ed 100644 --- a/src/api/scenarioData/scenarioData.service.ts +++ b/src/api/scenarioData/scenarioData.service.ts @@ -105,7 +105,7 @@ export class ScenarioDataService { const ruleSchema: RuleSchema = await this.ruleMappingService.ruleSchemaFile(goRulesJSONFilename); const results: { [scenarioId: string]: any } = {}; - for (const scenario of scenarios) { + for (const scenario of scenarios as ScenarioDataDocument[]) { const formattedVariablesObject = reduceToCleanObj(scenario?.variables, 'name', 'value'); const formattedExpectedResultsObject = reduceToCleanObj(scenario?.expectedResults, 'name', 'value'); @@ -201,7 +201,6 @@ export class ScenarioDataService { const expectedResults = formatVariables(row, expectedResultsKeys, expectedResultsStartIndex, true); const scenario: ScenarioData = { - _id: new Types.ObjectId(), title: scenarioTitle, ruleID: '', variables: inputs, From 7e9676fa0f2da9d29b8da155abc05fb818ffdc62 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Thu, 4 Jul 2024 13:24:36 -0700 Subject: [PATCH 49/52] Update naming convention of rulemap finalOutputs to resultOutputs. --- .../ruleMapping.controller.spec.ts | 2 +- .../ruleMapping/ruleMapping.service.spec.ts | 22 +++++++++---------- src/api/ruleMapping/ruleMapping.service.ts | 22 +++++++++---------- .../scenarioData/scenarioData.interface.ts | 2 +- .../scenarioData/scenarioData.service.spec.ts | 6 ++--- src/utils/handleTrace.ts | 4 ++-- 6 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/api/ruleMapping/ruleMapping.controller.spec.ts b/src/api/ruleMapping/ruleMapping.controller.spec.ts index a6ec22f..f1f28b5 100644 --- a/src/api/ruleMapping/ruleMapping.controller.spec.ts +++ b/src/api/ruleMapping/ruleMapping.controller.spec.ts @@ -80,7 +80,7 @@ describe('RuleMappingController', () => { it('should return the evaluated rule map', async () => { const nodes = [{ id: '1', type: 'someType', content: { inputs: [], outputs: [] } }]; const edges = [{ id: '2', type: 'someType', targetId: '1', sourceId: '1' }]; - const result = { inputs: [], outputs: [], finalOutputs: [] }; + const result = { inputs: [], outputs: [], resultOutputs: [] }; jest.spyOn(service, 'ruleSchema').mockReturnValue(result); const dto: EvaluateRuleMappingDto = { nodes, edges }; diff --git a/src/api/ruleMapping/ruleMapping.service.spec.ts b/src/api/ruleMapping/ruleMapping.service.spec.ts index 5ba3f69..46ee873 100644 --- a/src/api/ruleMapping/ruleMapping.service.spec.ts +++ b/src/api/ruleMapping/ruleMapping.service.spec.ts @@ -91,8 +91,8 @@ describe('RuleMappingService', () => { }); }); - describe('extractfinalOutputs', () => { - it('should extract final outputs correctly when there is one output node and corresponding edges', () => { + describe('extractResultOutputs', () => { + it('should extract result outputs correctly when there is one output node and corresponding edges', () => { const nodes: Node[] = [ { id: '1', @@ -129,9 +129,9 @@ describe('RuleMappingService', () => { ], }); - const result = service.extractfinalOutputs(nodes, edges); + const result = service.extractResultOutputs(nodes, edges); expect(result).toEqual({ - finalOutputs: [ + resultOutputs: [ { id: '1', name: 'Output1', type: 'string', property: 'field2' }, { id: '2', name: 'Output2', type: 'number', property: 'field3' }, ], @@ -151,7 +151,7 @@ describe('RuleMappingService', () => { const edges: Edge[] = [{ id: '1', type: 'someType', sourceId: '1', targetId: '2' }]; - expect(() => service.extractfinalOutputs(nodes, edges)).toThrow('No outputNode found in the nodes array'); + expect(() => service.extractResultOutputs(nodes, edges)).toThrow('No outputNode found in the nodes array'); }); it('should return an empty array if no target edges are found for the output node', () => { @@ -178,9 +178,9 @@ describe('RuleMappingService', () => { outputs: [], }); - const result = service.extractfinalOutputs(nodes, edges); + const result = service.extractResultOutputs(nodes, edges); expect(result).toEqual({ - finalOutputs: [], + resultOutputs: [], }); }); @@ -221,9 +221,9 @@ describe('RuleMappingService', () => { ], }); - const result = service.extractfinalOutputs(nodes, edges); + const result = service.extractResultOutputs(nodes, edges); expect(result).toEqual({ - finalOutputs: [ + resultOutputs: [ { id: '1', name: 'Output1', type: 'string', property: 'field2' }, { id: '2', name: 'Output2', type: 'number', property: 'field3' }, ], @@ -367,7 +367,7 @@ describe('RuleMappingService', () => { { id: '1', name: 'Output1', type: 'string', property: 'field2' }, { id: '2', name: 'Output2', type: 'number', property: 'field3' }, ], - finalOutputs: [], + resultOutputs: [], }); }); }); @@ -524,7 +524,7 @@ describe('RuleMappingService', () => { expect(mockGetFileContent).toHaveBeenCalledWith(filePath); expect(result).toEqual({ - finalOutputs: [], + resultOutputs: [], inputs: [{ id: '1', name: 'Input1', type: 'string', property: 'field1' }], outputs: [ { id: '3', name: 'Output1', type: 'string', property: 'field2' }, diff --git a/src/api/ruleMapping/ruleMapping.service.ts b/src/api/ruleMapping/ruleMapping.service.ts index 6823519..f957704 100644 --- a/src/api/ruleMapping/ruleMapping.service.ts +++ b/src/api/ruleMapping/ruleMapping.service.ts @@ -46,11 +46,11 @@ export class RuleMappingService { } // Get the final outputs of a rule from mapping the target output nodes and the edges - extractfinalOutputs( + extractResultOutputs( nodes: Node[], edges: Edge[], ): { - finalOutputs: any[]; + resultOutputs: any[]; } { // Find the output node const outputNode = nodes.find((obj) => obj.type === 'outputNode'); @@ -64,9 +64,9 @@ export class RuleMappingService { // Find the edges that connect the output node to other nodes const targetEdges = edges.filter((edge) => edge.targetId === outputNodeID); const targetOutputNodes = targetEdges.map((edge) => nodes.find((node) => node.id === edge.sourceId)); - const finalOutputs: any[] = this.extractFields(targetOutputNodes, 'outputs').outputs; + const resultOutputs: any[] = this.extractFields(targetOutputNodes, 'outputs').outputs; - return { finalOutputs }; + return { resultOutputs }; } extractInputsAndOutputs(nodes: Node[]): { @@ -109,23 +109,23 @@ export class RuleMappingService { ): { inputs: any[]; outputs: any[]; - finalOutputs: any[]; + resultOutputs: any[]; } { const inputs: any[] = this.extractUniqueInputs(nodes).uniqueInputs; const generalOutputs: any[] = this.extractFields(nodes, 'outputs').outputs; - const finalOutputs: any[] = this.extractfinalOutputs(nodes, edges).finalOutputs; + const resultOutputs: any[] = this.extractResultOutputs(nodes, edges).resultOutputs; //get unique outputs excluding final outputs const outputs: any[] = generalOutputs.filter( (output) => - !finalOutputs.some( - (finalOutput) => - finalOutput.id === output.id || - (finalOutput.key === output.key && finalOutput.property === output.property), + !resultOutputs.some( + (resultOutput) => + resultOutput.id === output.id || + (resultOutput.key === output.key && resultOutput.property === output.property), ), ); - return { inputs, outputs, finalOutputs }; + return { inputs, outputs, resultOutputs }; } // generate a schema for the inputs and outputs of a rule given the trace data of a rule run diff --git a/src/api/scenarioData/scenarioData.interface.ts b/src/api/scenarioData/scenarioData.interface.ts index 98e8cb6..b118873 100644 --- a/src/api/scenarioData/scenarioData.interface.ts +++ b/src/api/scenarioData/scenarioData.interface.ts @@ -1,6 +1,6 @@ export interface RuleSchema { inputs: Array<{ id: string; property: string }>; - finalOutputs: Array<{ id: string; property: string }>; + resultOutputs: Array<{ id: string; property: string }>; } export interface RuleRunResults { diff --git a/src/api/scenarioData/scenarioData.service.spec.ts b/src/api/scenarioData/scenarioData.service.spec.ts index 45fd88b..3988db5 100644 --- a/src/api/scenarioData/scenarioData.service.spec.ts +++ b/src/api/scenarioData/scenarioData.service.spec.ts @@ -316,7 +316,7 @@ describe('ScenarioDataService', () => { { id: 'id1', name: 'Family Composition', property: 'familyComposition' }, { id: 'id2', name: 'Number of Children', property: 'numberOfChildren' }, ], - finalOutputs: [ + resultOutputs: [ { id: 'id3', name: 'Is Eligible', property: 'isEligible' }, { id: 'id4', name: 'Base Amount', property: 'baseAmount' }, ], @@ -382,7 +382,7 @@ describe('ScenarioDataService', () => { ]; const ruleSchema = { inputs: [{ id: 'id1', name: 'Family Composition', property: 'familyComposition' }], - finalOutputs: [{ id: 'id3', name: 'Is Eligible', property: 'isEligible' }], + resultOutputs: [{ id: 'id3', name: 'Is Eligible', property: 'isEligible' }], }; jest.spyOn(service, 'getScenariosByFilename').mockResolvedValue(scenarios); @@ -409,7 +409,7 @@ describe('ScenarioDataService', () => { ]; const ruleSchema = { inputs: [], - finalOutputs: [{ id: 'id3', name: 'Is Eligible', property: 'isEligible' }], + resultOutputs: [{ id: 'id3', name: 'Is Eligible', property: 'isEligible' }], }; const decisionResult = { performance: '0.7', diff --git a/src/utils/handleTrace.ts b/src/utils/handleTrace.ts index 7df5765..3c1e53f 100644 --- a/src/utils/handleTrace.ts +++ b/src/utils/handleTrace.ts @@ -9,7 +9,7 @@ import { TraceObject } from '../api/ruleMapping/ruleMapping.interface'; * @returns Property name */ export const getPropertyById = (id: string, ruleSchema: RuleSchema, type: 'input' | 'output') => { - const schema = type === 'input' ? ruleSchema.inputs : ruleSchema.finalOutputs; + const schema = type === 'input' ? ruleSchema.inputs : ruleSchema.resultOutputs; const item = schema.find((item: any) => item.id === id); return item ? item.property : null; }; @@ -22,7 +22,7 @@ export const getPropertyById = (id: string, ruleSchema: RuleSchema, type: 'input */ export const mapTraceToResult = (trace: TraceObject, ruleSchema: RuleSchema, type: 'input' | 'output') => { const result: { [key: string]: any } = {}; - const schema = type === 'input' ? ruleSchema.inputs : ruleSchema.finalOutputs; + const schema = type === 'input' ? ruleSchema.inputs : ruleSchema.resultOutputs; for (const [key, value] of Object.entries(trace)) { const propertyUnformatted = getPropertyById(key, ruleSchema, type); From 2f45000c3f94d7aa587fcf4b02912cbd8157070a Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Fri, 5 Jul 2024 09:55:59 -0700 Subject: [PATCH 50/52] Update endpoints to post requests to handle nested rules. --- src/api/ruleMapping/ruleMapping.controller.ts | 11 ++++++----- src/api/scenarioData/scenarioData.controller.ts | 12 ++++++------ 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/api/ruleMapping/ruleMapping.controller.ts b/src/api/ruleMapping/ruleMapping.controller.ts index d5b88d2..5887aa5 100644 --- a/src/api/ruleMapping/ruleMapping.controller.ts +++ b/src/api/ruleMapping/ruleMapping.controller.ts @@ -1,19 +1,20 @@ -import { Controller, Get, Param, Res, Post, Body, HttpException, HttpStatus } from '@nestjs/common'; +import { Controller, Query, Res, Post, Body, HttpException, HttpStatus } from '@nestjs/common'; import { RuleMappingService } from './ruleMapping.service'; import { Response } from 'express'; import { EvaluateRuleRunSchemaDto, EvaluateRuleMappingDto } from './dto/evaluate-rulemapping.dto'; + @Controller('api/rulemap') export class RuleMappingController { constructor(private ruleMappingService: RuleMappingService) {} // Map a rule file to its unique inputs, and all outputs - @Get('/:ruleFileName') - async getRuleFile(@Param('ruleFileName') ruleFileName: string, @Res() res: Response) { - const rulemap = await this.ruleMappingService.ruleSchemaFile(ruleFileName); + @Post('/') + async getRuleFile(@Query('goRulesJSONFilename') goRulesJSONFilename: string, @Res() res: Response) { + const rulemap = await this.ruleMappingService.ruleSchemaFile(goRulesJSONFilename); try { res.setHeader('Content-Type', 'application/json'); - res.setHeader('Content-Disposition', `attachment; filename=${ruleFileName}`); + res.setHeader('Content-Disposition', `attachment; filename=${goRulesJSONFilename}`); res.send(rulemap); } catch (error) { throw new HttpException(error.message, HttpStatus.INTERNAL_SERVER_ERROR); diff --git a/src/api/scenarioData/scenarioData.controller.ts b/src/api/scenarioData/scenarioData.controller.ts index e759b72..01be11e 100644 --- a/src/api/scenarioData/scenarioData.controller.ts +++ b/src/api/scenarioData/scenarioData.controller.ts @@ -45,8 +45,8 @@ export class ScenarioDataController { } } - @Get('/by-filename/:goRulesJSONFilename') - async getScenariosByFilename(@Param('goRulesJSONFilename') goRulesJSONFilename: string): Promise { + @Post('/by-filename') + async getScenariosByFilename(@Body('goRulesJSONFilename') goRulesJSONFilename: string): Promise { try { return await this.scenarioDataService.getScenariosByFilename(goRulesJSONFilename); } catch (error) { @@ -123,9 +123,9 @@ export class ScenarioDataController { } } - @Get('/run-decisions/:goRulesJSONFilename') + @Post('/run-decisions') async runDecisionsForScenarios( - @Param('goRulesJSONFilename') goRulesJSONFilename: string, + @Body('goRulesJSONFilename') goRulesJSONFilename: string, ): Promise<{ [scenarioId: string]: any }> { try { return await this.scenarioDataService.runDecisionsForScenarios(goRulesJSONFilename); @@ -134,12 +134,12 @@ export class ScenarioDataController { } } - @Post('/evaluation/upload/:goRulesJSONFilename') + @Post('/evaluation/upload/') @UseInterceptors(FileInterceptor('file')) async uploadCSVAndProcess( @UploadedFile() file: Express.Multer.File | undefined, @Res() res: Response, - @Param('goRulesJSONFilename') goRulesJSONFilename: string, + @Body('goRulesJSONFilename') goRulesJSONFilename: string, ) { if (!file) { throw new HttpException('No file uploaded', HttpStatus.BAD_REQUEST); From 76fb0392b4a0dbc3bbc81a200e85526d17683b03 Mon Sep 17 00:00:00 2001 From: brysonjbest Date: Fri, 5 Jul 2024 13:12:31 -0700 Subject: [PATCH 51/52] Update csv evaulation endpoint. --- src/api/scenarioData/scenarioData.controller.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/api/scenarioData/scenarioData.controller.ts b/src/api/scenarioData/scenarioData.controller.ts index 01be11e..8cd95c3 100644 --- a/src/api/scenarioData/scenarioData.controller.ts +++ b/src/api/scenarioData/scenarioData.controller.ts @@ -111,10 +111,10 @@ export class ScenarioDataController { } } - @Get('/evaluation/:goRulesJSONFilename') - async getCSVForRuleRun(@Param('goRulesJSONFilename') goRulesJSONFilename: string, @Res() res: Response) { - const fileContent = await this.scenarioDataService.getCSVForRuleRun(goRulesJSONFilename); + @Post('/evaluation') + async getCSVForRuleRun(@Body('goRulesJSONFilename') goRulesJSONFilename: string, @Res() res: Response) { try { + const fileContent = await this.scenarioDataService.getCSVForRuleRun(goRulesJSONFilename); res.setHeader('Content-Type', 'text/csv'); res.setHeader('Content-Disposition', `attachment; filename=${goRulesJSONFilename.replace(/\.json$/, '.csv')}`); res.status(HttpStatus.OK).send(fileContent); From afa6e155ebfb1f784d4cf7683e09af6677c311c5 Mon Sep 17 00:00:00 2001 From: brysonjbest <103070659+brysonjbest@users.noreply.github.com> Date: Wed, 10 Jul 2024 12:21:39 -0700 Subject: [PATCH 52/52] Update with db query safety for finding by name. --- src/api/scenarioData/scenarioData.service.spec.ts | 5 +++-- src/api/scenarioData/scenarioData.service.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/api/scenarioData/scenarioData.service.spec.ts b/src/api/scenarioData/scenarioData.service.spec.ts index 3988db5..68c725b 100644 --- a/src/api/scenarioData/scenarioData.service.spec.ts +++ b/src/api/scenarioData/scenarioData.service.spec.ts @@ -267,12 +267,13 @@ describe('ScenarioDataService', () => { it('should return scenarios by filename', async () => { const goRulesJSONFilename = 'test.json'; const scenarioDataList: ScenarioData[] = [mockScenarioData]; + scenarioDataList[0].goRulesJSONFilename = goRulesJSONFilename; MockScenarioDataModel.find = jest.fn().mockReturnValue({ exec: jest.fn().mockResolvedValue(scenarioDataList) }); const result = await service.getScenariosByFilename(goRulesJSONFilename); expect(result).toEqual(scenarioDataList); - expect(MockScenarioDataModel.find).toHaveBeenCalledWith({ goRulesJSONFilename }); + expect(MockScenarioDataModel.find).toHaveBeenCalledWith({ goRulesJSONFilename: { $eq: goRulesJSONFilename } }); }); it('should throw an error if an error occurs while retrieving scenarios by filename', async () => { @@ -287,7 +288,7 @@ describe('ScenarioDataService', () => { await service.getScenariosByFilename(goRulesJSONFilename); }).rejects.toThrowError(`Error getting scenarios by filename: ${errorMessage}`); - expect(MockScenarioDataModel.find).toHaveBeenCalledWith({ goRulesJSONFilename }); + expect(MockScenarioDataModel.find).toHaveBeenCalledWith({ goRulesJSONFilename: { $eq: goRulesJSONFilename } }); }); }); describe('runDecisionsForScenarios', () => { diff --git a/src/api/scenarioData/scenarioData.service.ts b/src/api/scenarioData/scenarioData.service.ts index b2d27ed..5105949 100644 --- a/src/api/scenarioData/scenarioData.service.ts +++ b/src/api/scenarioData/scenarioData.service.ts @@ -86,7 +86,7 @@ export class ScenarioDataService { async getScenariosByFilename(goRulesJSONFilename: string): Promise { try { - return await this.scenarioDataModel.find({ goRulesJSONFilename: goRulesJSONFilename }).exec(); + return await this.scenarioDataModel.find({ goRulesJSONFilename: { $eq: goRulesJSONFilename } }).exec(); } catch (error) { throw new Error(`Error getting scenarios by filename: ${error.message}`); }