From ab0b96012769f0d9166d7f604a3a2d391df72811 Mon Sep 17 00:00:00 2001 From: timwekkenbc Date: Tue, 30 Jul 2024 11:12:15 -0700 Subject: [PATCH] Added support for drafts - in it's own schema but linked to a rule via ruleData, Added support for storing the branch that's current in review for the rule, Updated README --- README.md | 2 ++ src/api/ruleData/ruleData.controller.spec.ts | 9 +++++++ src/api/ruleData/ruleData.controller.ts | 12 ++++++++- src/api/ruleData/ruleData.module.ts | 19 +++++++++++++ src/api/ruleData/ruleData.schema.ts | 13 ++++++--- src/api/ruleData/ruleData.service.spec.ts | 26 +++++++++++++++++- src/api/ruleData/ruleData.service.ts | 28 +++++++++++++++++++- src/api/ruleData/ruleDraft.schema.ts | 12 +++++++++ src/app.module.ts | 22 +++++---------- 9 files changed, 122 insertions(+), 21 deletions(-) create mode 100644 src/api/ruleData/ruleData.module.ts create mode 100644 src/api/ruleData/ruleDraft.schema.ts diff --git a/README.md b/README.md index c02ded3..a236002 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ Before running your application locally, you'll need some environment variables. - MONGODB_URL: The URL for connecting to the MongoDB instance you created in the previous step. Set it to something like mongodb://localhost/nest. - FRONTEND_URI: The URI for the frontend application. Set it to http://localhost:8080. +- GITHUB_APP_CLIENT_ID +- GITHUB_APP_CLIENT_SECRET ### Including Rules from the Rules Repository diff --git a/src/api/ruleData/ruleData.controller.spec.ts b/src/api/ruleData/ruleData.controller.spec.ts index 177cdb5..a0fdb83 100644 --- a/src/api/ruleData/ruleData.controller.spec.ts +++ b/src/api/ruleData/ruleData.controller.spec.ts @@ -25,6 +25,15 @@ describe('RuleDataController', () => { expect(await controller.getAllRulesData()).toBe(result); }); + it('should return a rule draft for a given ruleId', async () => { + const ruleId = 'testRuleId'; + const mockRuleDraft = { _id: 'sadasd', content: {} }; + jest.spyOn(service, 'getRuleDataWithDraft').mockImplementation(() => Promise.resolve(mockRuleDraft)); + + expect(await controller.getRuleDraft(ruleId)).toBe(mockRuleDraft); + expect(service.getRuleDataWithDraft).toHaveBeenCalledWith(ruleId); + }); + it('should return a rule data', async () => { const ruleId = 'testId'; jest.spyOn(service, 'getRuleData').mockImplementation(() => Promise.resolve(mockRuleData)); diff --git a/src/api/ruleData/ruleData.controller.ts b/src/api/ruleData/ruleData.controller.ts index ab497dc..402fe6b 100644 --- a/src/api/ruleData/ruleData.controller.ts +++ b/src/api/ruleData/ruleData.controller.ts @@ -1,6 +1,7 @@ import { Controller, Get, Param, Post, Body, Put, Delete, HttpException, HttpStatus } from '@nestjs/common'; import { RuleDataService } from './ruleData.service'; -import { RuleData } from './ruleData.schema'; // assuming you have this interface +import { RuleData } from './ruleData.schema'; +import { RuleDraft } from './ruleDraft.schema'; @Controller('api/ruleData') export class RuleDataController { @@ -15,6 +16,15 @@ export class RuleDataController { } } + @Get('/draft/:ruleId') + async getRuleDraft(@Param('ruleId') ruleId: string): Promise { + try { + return await this.ruleDataService.getRuleDataWithDraft(ruleId); + } catch (error) { + throw new HttpException('Error getting draft data', HttpStatus.INTERNAL_SERVER_ERROR); + } + } + @Get('/:ruleId') async getRuleData(@Param('ruleId') ruleId: string): Promise { try { diff --git a/src/api/ruleData/ruleData.module.ts b/src/api/ruleData/ruleData.module.ts new file mode 100644 index 0000000..e73b410 --- /dev/null +++ b/src/api/ruleData/ruleData.module.ts @@ -0,0 +1,19 @@ +import { Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { RuleData, RuleDataSchema } from './ruleData.schema'; +import { RuleDraft, RuleDraftSchema } from './ruleDraft.schema'; +import { RuleDataController } from './ruleData.controller'; +import { RuleDataService } from './ruleData.service'; +import { DocumentsService } from '../documents/documents.service'; + +@Module({ + imports: [ + MongooseModule.forFeature([ + { name: RuleData.name, schema: RuleDataSchema }, + { name: RuleDraft.name, schema: RuleDraftSchema }, + ]), + ], + controllers: [RuleDataController], + providers: [RuleDataService, DocumentsService], +}) +export class RuleDataModule {} diff --git a/src/api/ruleData/ruleData.schema.ts b/src/api/ruleData/ruleData.schema.ts index 652d02d..014376d 100644 --- a/src/api/ruleData/ruleData.schema.ts +++ b/src/api/ruleData/ruleData.schema.ts @@ -1,7 +1,6 @@ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; -import { Document } from 'mongoose'; - -export type RuleDataDocument = RuleData & Document; +import { Document, Types } from 'mongoose'; +import { RuleDraftDocument } from './ruleDraft.schema'; @Schema() export class RuleData { @@ -13,6 +12,14 @@ export class RuleData { @Prop({ required: true, description: 'The filename of the JSON file containing the rule' }) goRulesJSONFilename: string; + + @Prop({ type: Types.ObjectId, description: 'Draft of updated rule content', ref: 'RuleDraft' }) + ruleDraft?: RuleDraftDocument | Types.ObjectId; + + @Prop({ description: 'The name of the branch on github associated with this file' }) + reviewBranch?: string; } +export type RuleDataDocument = RuleData & Document; + export const RuleDataSchema = SchemaFactory.createForClass(RuleData); diff --git a/src/api/ruleData/ruleData.service.spec.ts b/src/api/ruleData/ruleData.service.spec.ts index 2ffa8e4..bf46126 100644 --- a/src/api/ruleData/ruleData.service.spec.ts +++ b/src/api/ruleData/ruleData.service.spec.ts @@ -3,15 +3,22 @@ import { getModelToken } from '@nestjs/mongoose'; import { DocumentsService } from '../documents/documents.service'; import { RuleDataService } from './ruleData.service'; import { RuleData } from './ruleData.schema'; +import { RuleDraft } from './ruleDraft.schema'; -export const mockRuleData = { +export const mockRuleData: RuleData = { _id: 'testId', title: 'Title', goRulesJSONFilename: 'filename.json', }; +const mockRuleDraft = { content: { nodes: [], edges: [] } }; + export const mockServiceProviders = [ RuleDataService, + { + provide: getModelToken(RuleDraft.name), + useFactory: () => ({}), + }, { provide: getModelToken(RuleData.name), useFactory: () => ({ @@ -21,6 +28,11 @@ export const mockServiceProviders = [ findOne: jest.fn().mockImplementation(() => ({ exec: jest.fn().mockResolvedValue(mockRuleData), })), + findById: jest.fn().mockReturnValue({ + populate: jest.fn().mockReturnValue({ + exec: jest.fn().mockResolvedValue({ ...mockRuleData, ruleDraft: mockRuleDraft }), + }), + }), }), }, { @@ -53,10 +65,22 @@ describe('RuleDataService', () => { expect(await service.getAllRuleData()).toEqual(result); }); + it('should return a rule draft for a given ruleId', async () => { + expect(await service.getRuleDataWithDraft(mockRuleData._id)).toEqual(mockRuleDraft); + }); + it('should get data for a rule', async () => { expect(await service.getRuleData(mockRuleData._id)).toEqual(mockRuleData); }); + it('should update rule data', async () => { + // TODO: Implement + }); + + it('should delete rule data', async () => { + // TODO: Implement + }); + 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']; diff --git a/src/api/ruleData/ruleData.service.ts b/src/api/ruleData/ruleData.service.ts index 1646b78..b4cd336 100644 --- a/src/api/ruleData/ruleData.service.ts +++ b/src/api/ruleData/ruleData.service.ts @@ -4,11 +4,13 @@ import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { DocumentsService } from '../documents/documents.service'; import { RuleData, RuleDataDocument } from './ruleData.schema'; +import { RuleDraft, RuleDraftDocument } from './ruleDraft.schema'; @Injectable() export class RuleDataService { constructor( @InjectModel(RuleData.name) private ruleDataModel: Model, + @InjectModel(RuleDraft.name) private ruleDraftModel: Model, private documentsService: DocumentsService, ) {} @@ -26,6 +28,15 @@ export class RuleDataService { } } + async getRuleDataWithDraft(ruleId: string): Promise { + try { + const { ruleDraft } = await this.ruleDataModel.findById(ruleId).populate('ruleDraft').exec(); + return ruleDraft as RuleDraft; + } catch (error) { + throw new Error(`Error getting draft for ${ruleId}: ${error.message}`); + } + } + async getRuleData(ruleId: string): Promise { try { const ruleData = await this.ruleDataModel.findOne({ _id: ruleId }).exec(); @@ -34,8 +45,20 @@ export class RuleDataService { } return ruleData; } catch (error) { - throw new Error(`Error getting all rule data: ${error.message}`); + throw new Error(`Error getting all rule data for ${ruleId}: ${error.message}`); + } + } + + async _addOrUpdateDraft(ruleData: Partial): Promise> { + // If there is a rule draft, update that document specifically + // This is necessary because we don't store the draft on the ruleData object directly + // Instead it is stored elsewhere and linked to the ruleData via its id + if (ruleData?.ruleDraft) { + const newDraft = new this.ruleDraftModel(ruleData.ruleDraft); + const savedDraft = await newDraft.save(); + ruleData.ruleDraft = savedDraft._id; } + return ruleData; } async createRuleData(ruleData: Partial): Promise { @@ -44,6 +67,7 @@ export class RuleDataService { const newRuleID = new ObjectId(); ruleData._id = newRuleID.toHexString(); } + ruleData = await this._addOrUpdateDraft(ruleData); const newRuleData = new this.ruleDataModel(ruleData); const response = await newRuleData.save(); return response; @@ -59,9 +83,11 @@ export class RuleDataService { if (!existingRuleData) { throw new Error('Rule data not found'); } + updatedData = await this._addOrUpdateDraft(updatedData); Object.assign(existingRuleData, updatedData); return await existingRuleData.save(); } catch (error) { + console.error('Error updating rule', error.message); throw new Error(`Failed to update rule data: ${error.message}`); } } diff --git a/src/api/ruleData/ruleDraft.schema.ts b/src/api/ruleData/ruleDraft.schema.ts new file mode 100644 index 0000000..532dd2c --- /dev/null +++ b/src/api/ruleData/ruleDraft.schema.ts @@ -0,0 +1,12 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { Document, Schema as MongooseSchema } from 'mongoose'; + +@Schema() +export class RuleDraft { + @Prop({ type: MongooseSchema.Types.Mixed, description: 'Draft of updated rule content' }) + content: object; +} + +export type RuleDraftDocument = RuleDraft & Document; + +export const RuleDraftSchema = SchemaFactory.createForClass(RuleDraft); diff --git a/src/app.module.ts b/src/app.module.ts index 0e0f0e7..16bee21 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,9 +1,8 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { MongooseModule } from '@nestjs/mongoose'; -import { RuleData, RuleDataSchema } from './api/ruleData/ruleData.schema'; -import { RuleDataController } from './api/ruleData/ruleData.controller'; -import { RuleDataService } from './api/ruleData/ruleData.service'; +import { GithubAuthModule } from './auth/github-auth/github-auth.module'; +import { RuleDataModule } from './api/ruleData/ruleData.module'; import { DecisionsController } from './api/decisions/decisions.controller'; import { DecisionsService } from './api/decisions/decisions.service'; import { DocumentsController } from './api/documents/documents.controller'; @@ -25,18 +24,11 @@ import { ScenarioDataService } from './api/scenarioData/scenarioData.service'; ], }), MongooseModule.forRoot(process.env.MONGODB_URL), - MongooseModule.forFeature([ - { name: RuleData.name, schema: RuleDataSchema }, - { name: ScenarioData.name, schema: ScenarioDataSchema }, - ]), + MongooseModule.forFeature([{ name: ScenarioData.name, schema: ScenarioDataSchema }]), + GithubAuthModule, + RuleDataModule, ], - controllers: [ - RuleDataController, - DecisionsController, - DocumentsController, - RuleMappingController, - ScenarioDataController, - ], - providers: [RuleDataService, DecisionsService, DocumentsService, RuleMappingService, ScenarioDataService], + controllers: [DecisionsController, DocumentsController, RuleMappingController, ScenarioDataController], + providers: [DecisionsService, DocumentsService, RuleMappingService, ScenarioDataService], }) export class AppModule {}