Skip to content

Commit

Permalink
Merge pull request #11 from bcgov/dev
Browse files Browse the repository at this point in the history
Update to be an API that supports our own implementation of Zen Engine, ability to fetch json files, and rule mapping
  • Loading branch information
timwekkenbc authored Jun 18, 2024
2 parents 7881cf6 + 89cc63b commit 8012a2d
Show file tree
Hide file tree
Showing 25 changed files with 1,689 additions and 6 deletions.
9 changes: 9 additions & 0 deletions .github/workflows/build-and-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ on:
- main
- dev
- pipeline
workflow_dispatch: # Allows us to trigger this workflow from elsewhere (like the rules repo)

env:
REGISTRY: ghcr.io
Expand All @@ -31,13 +32,21 @@ jobs:
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

- name: Extract current branch name
id: extract_branch
run: echo "BRANCH_NAME=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV

- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
# This is the branch name we want to use of the rules repo branch
# We want it to be the same as the branch we are building so they stay in sync
build-args: |
RULES_REPO_BRANCH=${{ env.BRANCH_NAME }}
outputs:
image_tag: ${{ steps.meta.outputs.tags }}
Expand Down
7 changes: 7 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Install the app dependencies in a full Node docker image
FROM registry.access.redhat.com/ubi9/nodejs-20:latest

# Set the environment variables
ARG RULES_REPO_BRANCH
ENV RULES_REPO_BRANCH=${RULES_REPO_BRANCH}

# Set the working directory
WORKDIR /opt/app-root/src

Expand All @@ -14,5 +18,8 @@ RUN npm ci
# Copy the application code
COPY . ./

# Clone the rules repository
RUN git clone -b ${RULES_REPO_BRANCH} https://github.com/bcgov/brms-rules.git brms-rules

# Start the application
CMD ["npm", "start"]
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ Before running your application locally, you'll need some environment variables.
- FRONTEND_URI: The URI for the frontend application. Set it to http://localhost:8080.
- CHEFS_API_URL: The URL for the Chefs API. Set it to https://submit.digital.gov.bc.ca/app/api/v1.

### Including Rules from the Rules Repository

To get access to rules locally on your machine simply clone the repo at https://github.com/bcgov/brms-rules into your project. This project is set to grab rules from `brms-rules/rules`, which is the default location of rules if that project is cloned into this one.

### Running the Application

Install dependencies:
Expand Down
3 changes: 3 additions & 0 deletions helm/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ spec:
metadata:
labels:
app.kubernetes.io/name: brms-api
annotations:
helm.sh/release: "{{ .Release.Name }}"
helm.sh/revision: "{{ .Release.Revision }}"
spec:
containers:
- name: brms-api
Expand Down
126 changes: 126 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,15 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@gorules/zen-engine": "^0.23.0",
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.2.0",
"@nestjs/core": "^10.0.0",
"@nestjs/mongoose": "^10.0.5",
"@nestjs/platform-express": "^10.3.7",
"axios": "^1.6.8",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"mongoose": "^8.3.0",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1"
Expand Down
29 changes: 29 additions & 0 deletions src/api/decisions/decisions.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Controller, Post, Query, Body, HttpException, HttpStatus } from '@nestjs/common';
import { DecisionsService } from './decisions.service';
import { EvaluateDecisionDto, EvaluateDecisionWithContentDto } from './dto/evaluate-decision.dto';

@Controller('api/decisions')
export class DecisionsController {
constructor(private readonly decisionsService: DecisionsService) {}

@Post('/evaluate')
async evaluateDecisionByContent(@Body() { content, context, trace }: EvaluateDecisionWithContentDto) {
try {
return await this.decisionsService.runDecision(content, context, { trace });
} catch (error) {
throw new HttpException(error.message, HttpStatus.INTERNAL_SERVER_ERROR);
}
}

@Post('/evaluateByFile')
async evaluateDecisionByFile(
@Query('ruleFileName') ruleFileName: string,
@Body() { context, trace }: EvaluateDecisionDto,
) {
try {
return await this.decisionsService.runDecisionByFile(ruleFileName, context, { trace });
} catch (error) {
throw new HttpException(error.message, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
75 changes: 75 additions & 0 deletions src/api/decisions/decisions.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { ConfigService } from '@nestjs/config';
import { Test, TestingModule } from '@nestjs/testing';
import { ZenEngine, ZenDecision, ZenEvaluateOptions } from '@gorules/zen-engine';
import { DecisionsService } from './decisions.service';
import { readFileSafely } from '../../utils/readFile';

jest.mock('../../utils/readFile', () => ({
readFileSafely: jest.fn(),
FileNotFoundError: jest.fn(),
}));

describe('DecisionsService', () => {
let service: DecisionsService;
let mockEngine: Partial<ZenEngine>;
let mockDecision: Partial<ZenDecision>;

beforeEach(async () => {
mockDecision = {
evaluate: jest.fn(),
};
mockEngine = {
createDecision: jest.fn().mockReturnValue(mockDecision),
};

const module: TestingModule = await Test.createTestingModule({
providers: [ConfigService, DecisionsService, { provide: ZenEngine, useValue: mockEngine }],
}).compile();

service = module.get<DecisionsService>(DecisionsService);
service.engine = mockEngine as ZenEngine;
});

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

it('should throw an error if the decision fails', async () => {
const content = {};
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');
});
});

describe('runDecisionByFile', () => {
it('should read a file and run a decision', async () => {
const ruleFileName = 'rule';
const context = {};
const options: ZenEvaluateOptions = { trace: false };
const content = JSON.stringify({ rule: 'rule' });
(readFileSafely as jest.Mock).mockResolvedValue(content);
await service.runDecisionByFile(ruleFileName, context, options);
expect(readFileSafely).toHaveBeenCalledWith(service.rulesDirectory, ruleFileName);
expect(mockEngine.createDecision).toHaveBeenCalledWith(content);
expect(mockDecision.evaluate).toHaveBeenCalledWith(context, options);
});

it('should throw an error if reading the file fails', async () => {
const ruleFileName = 'rule';
const context = {};
const options: ZenEvaluateOptions = { trace: false };
(readFileSafely as jest.Mock).mockRejectedValue(new Error('Error'));
await expect(service.runDecisionByFile(ruleFileName, context, options)).rejects.toThrow(
'Failed to run decision: Error',
);
});
});
});
Loading

0 comments on commit 8012a2d

Please sign in to comment.