Skip to content

Commit

Permalink
Merge branch 'dev' into feature/nested-inputs
Browse files Browse the repository at this point in the history
  • Loading branch information
brysonjbest committed Jul 18, 2024
2 parents 8fd860e + a480438 commit d441465
Show file tree
Hide file tree
Showing 15 changed files with 195 additions and 184 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe('DecisionsController', () => {
{
provide: DecisionsService,
useValue: {
runDecision: jest.fn(),
runDecisionByContent: jest.fn(),
runDecisionByFile: jest.fn(),
},
},
Expand All @@ -26,23 +26,23 @@ describe('DecisionsController', () => {
service = module.get<DecisionsService>(DecisionsService);
});

it('should call runDecision with correct parameters', async () => {
it('should call runDecisionByContent with correct parameters', async () => {
const dto: EvaluateDecisionWithContentDto = {
content: { value: 'content' },
ruleContent: { nodes: [], edges: [] },
context: { value: 'context' },
trace: false,
};
await controller.evaluateDecisionByContent(dto);
expect(service.runDecision).toHaveBeenCalledWith(dto.content, dto.context, { trace: dto.trace });
expect(service.runDecisionByContent).toHaveBeenCalledWith(dto.ruleContent, dto.context, { trace: dto.trace });
});

it('should throw an error when runDecision fails', async () => {
const dto: EvaluateDecisionWithContentDto = {
content: { value: 'content' },
ruleContent: { nodes: [], edges: [] },
context: { value: 'context' },
trace: false,
};
(service.runDecision as jest.Mock).mockRejectedValue(new Error('Error'));
(service.runDecisionByContent as jest.Mock).mockRejectedValue(new Error('Error'));
await expect(controller.evaluateDecisionByContent(dto)).rejects.toThrow(HttpException);
});

Expand Down
4 changes: 2 additions & 2 deletions src/api/decisions/decisions.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ export class DecisionsController {
constructor(private readonly decisionsService: DecisionsService) {}

@Post('/evaluate')
async evaluateDecisionByContent(@Body() { content, context, trace }: EvaluateDecisionWithContentDto) {
async evaluateDecisionByContent(@Body() { ruleContent, context, trace }: EvaluateDecisionWithContentDto) {
try {
return await this.decisionsService.runDecision(content, context, { trace });
return await this.decisionsService.runDecisionByContent(ruleContent, context, { trace });
} catch (error) {
throw new HttpException(error.message, HttpStatus.INTERNAL_SERVER_ERROR);
}
Expand Down
39 changes: 32 additions & 7 deletions src/api/decisions/decisions.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,45 @@ describe('DecisionsService', () => {

describe('runDecision', () => {
it('should run a decision', async () => {
const content = {};
const ruleFileName = 'rule';
const ruleContent = { nodes: [], edges: [] };
const context = {};
const options: ZenEvaluateOptions = { trace: false };
await service.runDecision(content, context, options);
expect(mockEngine.createDecision).toHaveBeenCalledWith(content);
await service.runDecision(ruleContent, ruleFileName, context, options);
expect(mockEngine.createDecision).toHaveBeenCalledWith(ruleContent);
expect(mockDecision.evaluate).toHaveBeenCalledWith(context, options);
});

it('should throw an error if the decision fails', async () => {
const content = {};
const ruleFileName = 'rule';
const ruleContent = { nodes: [], edges: [] };
const context = {};
const options: ZenEvaluateOptions = { trace: false };
(mockDecision.evaluate as jest.Mock).mockRejectedValue(new Error('Error'));
await expect(service.runDecision(content, context, options)).rejects.toThrow('Failed to run decision: Error');
await expect(service.runDecision(ruleContent, ruleFileName, context, options)).rejects.toThrow(
'Failed to run decision: Error',
);
});
});

describe('runDecisionByContent', () => {
it('should run a decision by content', async () => {
const ruleContent = { nodes: [], edges: [] };
const context = {};
const options: ZenEvaluateOptions = { trace: false };
await service.runDecisionByContent(ruleContent, context, options);
expect(mockEngine.createDecision).toHaveBeenCalledWith(ruleContent);
expect(mockDecision.evaluate).toHaveBeenCalledWith(context, options);
});

it('should throw an error if the decision fails', async () => {
const ruleContent = { nodes: [], edges: [] };
const context = {};
const options: ZenEvaluateOptions = { trace: false };
(mockDecision.evaluate as jest.Mock).mockRejectedValue(new Error('Error'));
await expect(service.runDecisionByContent(ruleContent, context, options)).rejects.toThrow(
'Failed to run decision: Error',
);
});
});

Expand All @@ -54,8 +79,8 @@ describe('DecisionsService', () => {
const ruleFileName = 'rule';
const context = {};
const options: ZenEvaluateOptions = { trace: false };
const content = JSON.stringify({ rule: 'rule' });
(readFileSafely as jest.Mock).mockResolvedValue(content);
const content = { rule: 'rule' };
(readFileSafely as jest.Mock).mockResolvedValue(Buffer.from(JSON.stringify(content)));
await service.runDecisionByFile(ruleFileName, context, options);
expect(readFileSafely).toHaveBeenCalledWith(service.rulesDirectory, ruleFileName);
expect(mockEngine.createDecision).toHaveBeenCalledWith(content);
Expand Down
19 changes: 15 additions & 4 deletions src/api/decisions/decisions.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
import { ZenEngine, ZenEvaluateOptions } from '@gorules/zen-engine';
import { ConfigService } from '@nestjs/config';
import { RuleContent } from '../ruleMapping/ruleMapping.interface';
import { readFileSafely, FileNotFoundError } from '../../utils/readFile';

@Injectable()
Expand All @@ -14,9 +15,9 @@ export class DecisionsService {
this.engine = new ZenEngine({ loader });
}

async runDecision(content: object, context: object, options: ZenEvaluateOptions) {
async runDecisionByContent(ruleContent: RuleContent, context: object, options: ZenEvaluateOptions) {
try {
const decision = this.engine.createDecision(content);
const decision = this.engine.createDecision(ruleContent);
return await decision.evaluate(context, options);
} catch (error) {
console.error(error.message);
Expand All @@ -26,8 +27,9 @@ export class DecisionsService {

async runDecisionByFile(ruleFileName: string, context: object, options: ZenEvaluateOptions) {
try {
const content = await readFileSafely(this.rulesDirectory, ruleFileName);
return this.runDecision(content, context, options);
const decisionFile = await readFileSafely(this.rulesDirectory, ruleFileName);
const content: RuleContent = JSON.parse(decisionFile.toString()); // Convert file buffer to rulecontent
return this.runDecisionByContent(content, context, options);
} catch (error) {
if (error instanceof FileNotFoundError) {
throw new HttpException('Rule not found', HttpStatus.NOT_FOUND);
Expand All @@ -36,4 +38,13 @@ export class DecisionsService {
}
}
}

/** Run the decision by content if it exists, otherwise run by filename */
async runDecision(ruleContent: RuleContent, ruleFileName: string, context: object, options: ZenEvaluateOptions) {
if (ruleContent) {
return await this.runDecisionByContent(ruleContent, context, options);
} else {
return await this.runDecisionByFile(ruleFileName, context, options);
}
}
}
3 changes: 2 additions & 1 deletion src/api/decisions/dto/evaluate-decision.dto.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { IsBoolean, IsObject } from 'class-validator';
import { RuleContent } from 'src/api/ruleMapping/ruleMapping.interface';

export class EvaluateDecisionDto {
@IsObject()
Expand All @@ -10,5 +11,5 @@ export class EvaluateDecisionDto {

export class EvaluateDecisionWithContentDto extends EvaluateDecisionDto {
@IsObject()
content: object;
ruleContent: RuleContent;
}
40 changes: 8 additions & 32 deletions src/api/ruleMapping/ruleMapping.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ describe('RuleMappingController', () => {
{
provide: RuleMappingService,
useValue: {
ruleSchemaFile: jest.fn(),
ruleSchema: jest.fn(),
evaluateRuleSchema: jest.fn(),
},
Expand All @@ -37,43 +36,28 @@ describe('RuleMappingController', () => {
service = module.get<RuleMappingService>(RuleMappingService);
});

describe('getRuleFile', () => {
it('should return the rule file with the correct headers', async () => {
describe('getRuleSchema', () => {
it('should return the rule schema with the correct headers', async () => {
const ruleFileName = 'test-rule.json';
const filePath = `${ruleFileName}`;
const rulemap = { inputs: [], outputs: [] };
jest.spyOn(service, 'ruleSchemaFile').mockResolvedValue(rulemap);
const ruleContent = { nodes: [], edges: [] };
const rulemap = { inputs: [], outputs: [], resultOutputs: [] };
jest.spyOn(service, 'ruleSchema').mockReturnValue(rulemap);

const mockResponse = {
setHeader: jest.fn(),
send: jest.fn(),
} as unknown as Response;

await controller.getRuleFile(ruleFileName, mockResponse);
await controller.getRuleSchema(ruleFileName, ruleContent, mockResponse);

expect(service.ruleSchemaFile).toHaveBeenCalledWith(filePath);
expect(service.ruleSchema).toHaveBeenCalledWith(ruleContent);
expect(mockResponse.setHeader).toHaveBeenCalledWith('Content-Type', 'application/json');
expect(mockResponse.setHeader).toHaveBeenCalledWith(
'Content-Disposition',
`attachment; filename=${ruleFileName}`,
);
expect(mockResponse.send).toHaveBeenCalledWith(rulemap);
});

it('should handle errors properly', async () => {
const ruleFileName = 'test-rule.json';
const error = new Error('File not found');
jest.spyOn(service, 'ruleSchemaFile').mockRejectedValue(error);

const mockResponse = {
setHeader: jest.fn(),
send: jest.fn(),
} as unknown as Response;

await expect(controller.getRuleFile(ruleFileName, mockResponse)).rejects.toThrow(
new HttpException(error.message, HttpStatus.INTERNAL_SERVER_ERROR),
);
});
});

describe('evaluateRuleMap', () => {
Expand All @@ -86,18 +70,10 @@ describe('RuleMappingController', () => {
const dto: EvaluateRuleMappingDto = { nodes, edges };
const response = await controller.evaluateRuleMap(dto);

expect(service.ruleSchema).toHaveBeenCalledWith(nodes, edges);
expect(service.ruleSchema).toHaveBeenCalledWith({ nodes, edges });
expect(response).toEqual({ result });
});

it('should handle invalid request data', async () => {
const dto = { nodes: 'invalid' } as unknown as EvaluateRuleMappingDto;

await expect(controller.evaluateRuleMap(dto)).rejects.toThrow(
new HttpException('Invalid request data', HttpStatus.BAD_REQUEST),
);
});

it('should handle errors properly', async () => {
const nodes = [{ id: '1', type: 'someType', content: { inputs: [], outputs: [] } }];
const edges = [{ id: '2', type: 'someType', targetId: '1', sourceId: '1' }];
Expand Down
29 changes: 17 additions & 12 deletions src/api/ruleMapping/ruleMapping.controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Controller, Query, Res, Post, Body, HttpException, HttpStatus } from '@nestjs/common';
import { RuleMappingService } from './ruleMapping.service';
import { Controller, Res, Post, Body, HttpException, HttpStatus } from '@nestjs/common';
import { RuleMappingService, InvalidRuleContent } from './ruleMapping.service';
import { Response } from 'express';
import { EvaluateRuleRunSchemaDto, EvaluateRuleMappingDto } from './dto/evaluate-rulemapping.dto';

Expand All @@ -9,30 +9,35 @@ export class RuleMappingController {

// Map a rule file to its unique inputs, and all outputs
@Post('/')
async getRuleFile(@Query('goRulesJSONFilename') goRulesJSONFilename: string, @Res() res: Response) {
const rulemap = await this.ruleMappingService.ruleSchemaFile(goRulesJSONFilename);
async getRuleSchema(
@Body('goRulesJSONFilename') goRulesJSONFilename: string,
@Body('ruleContent') ruleContent: EvaluateRuleMappingDto,
@Res() res: Response,
) {
const rulemap = this.ruleMappingService.ruleSchema(ruleContent);

try {
res.setHeader('Content-Type', 'application/json');
res.setHeader('Content-Disposition', `attachment; filename=${goRulesJSONFilename}`);
res.send(rulemap);
} catch (error) {
throw new HttpException(error.message, HttpStatus.INTERNAL_SERVER_ERROR);
if (error instanceof InvalidRuleContent) {
throw new HttpException('Invalid rule content', HttpStatus.BAD_REQUEST);
} else {
throw new HttpException('Internal server error', HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}

// Map a rule to its unique inputs, and all outputs
@Post('/evaluate')
async evaluateRuleMap(@Body() { nodes, edges }: EvaluateRuleMappingDto) {
async evaluateRuleMap(@Body() ruleContent: EvaluateRuleMappingDto) {
try {
if (!nodes || !Array.isArray(nodes)) {
throw new HttpException('Invalid request data', HttpStatus.BAD_REQUEST);
}
const result = this.ruleMappingService.ruleSchema(nodes, edges);
const result = this.ruleMappingService.ruleSchema(ruleContent);
return { result };
} catch (error) {
if (error instanceof HttpException && error.getStatus() === HttpStatus.BAD_REQUEST) {
throw error;
if (error instanceof InvalidRuleContent) {
throw new HttpException('Invalid rule content', HttpStatus.BAD_REQUEST);
} else {
throw new HttpException('Internal server error', HttpStatus.INTERNAL_SERVER_ERROR);
}
Expand Down
5 changes: 5 additions & 0 deletions src/api/ruleMapping/ruleMapping.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ export interface Edge {
targetHandle?: string;
}

export interface RuleContent {
nodes: Node[];
edges: Edge[];
}

export interface TraceObjectEntry extends ZenEngineTrace {
id: string;
name: string;
Expand Down
Loading

0 comments on commit d441465

Please sign in to comment.