From 56b932f5c9d8888991daa0c071484ba77bfe942f Mon Sep 17 00:00:00 2001 From: Darkmift Date: Thu, 16 May 2024 13:22:41 +0300 Subject: [PATCH 1/4] Initial commit of the core records --- src/app.module.ts | 2 + src/common/mongoose/schemas/core-records.ts | 39 ++++++ .../core-records.controller.spec.ts | 18 +++ src/core-records/core-records.controller.ts | 42 ++++++ src/core-records/core-records.module.ts | 9 ++ src/core-records/core-records.service.spec.ts | 18 +++ src/core-records/core-records.service.ts | 130 ++++++++++++++++++ src/types/core-records.ts | 35 +++++ 8 files changed, 293 insertions(+) create mode 100644 src/common/mongoose/schemas/core-records.ts create mode 100644 src/core-records/core-records.controller.spec.ts create mode 100644 src/core-records/core-records.controller.ts create mode 100644 src/core-records/core-records.module.ts create mode 100644 src/core-records/core-records.service.spec.ts create mode 100644 src/core-records/core-records.service.ts create mode 100644 src/types/core-records.ts diff --git a/src/app.module.ts b/src/app.module.ts index 46f0ffe..4459d28 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -8,6 +8,7 @@ import { MembersModule } from './members/members.module'; import { GithubGqlModule } from './github-gql/github-gql.module'; import { ProjectsModule } from './projects/projects.module'; import { LeaderboardModule } from './leaderboard/leaderboard.module'; +import { CoreRecordsModule } from './core-records/core-records.module'; @Module({ imports: [ @@ -47,6 +48,7 @@ import { LeaderboardModule } from './leaderboard/leaderboard.module'; GithubGqlModule, ProjectsModule, LeaderboardModule, + CoreRecordsModule, ], controllers: [AppController], providers: [AppService], diff --git a/src/common/mongoose/schemas/core-records.ts b/src/common/mongoose/schemas/core-records.ts new file mode 100644 index 0000000..62c8712 --- /dev/null +++ b/src/common/mongoose/schemas/core-records.ts @@ -0,0 +1,39 @@ +import { + CoreRecordTypeName, + ICoreRecord, + RecordType, +} from '@/types/core-records'; +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { type HydratedDocument } from 'mongoose'; + +export type CoreRecordDocument = HydratedDocument; + +@Schema() +export class CoreRecord implements ICoreRecord { + @Prop({ required: true }) + record: RecordType; + + @Prop({ required: true }) + type: CoreRecordTypeName; + + @Prop({ required: true }) + createdAt: Date; + + @Prop({ required: true }) + createdBy: string; + + @Prop({ required: true }) + updatedAt: Date; + + @Prop({ required: true }) + updatedBy: Date | null; + + @Prop({ required: false }) + archivedAt: Date | null; + + @Prop({ required: false }) + archivedBy: string | null; +} + +export type FilterCoreRecords = Partial>; +export const CoreRecordSchema = SchemaFactory.createForClass(CoreRecord); diff --git a/src/core-records/core-records.controller.spec.ts b/src/core-records/core-records.controller.spec.ts new file mode 100644 index 0000000..31fb03d --- /dev/null +++ b/src/core-records/core-records.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { CoreRecordsController } from './core-records.controller'; + +describe('CoreRecordsController', () => { + let controller: CoreRecordsController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [CoreRecordsController], + }).compile(); + + controller = module.get(CoreRecordsController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/core-records/core-records.controller.ts b/src/core-records/core-records.controller.ts new file mode 100644 index 0000000..e613fe7 --- /dev/null +++ b/src/core-records/core-records.controller.ts @@ -0,0 +1,42 @@ +import { Controller } from '@nestjs/common'; +import { CoreRecordsService } from './core-records.service'; +import { FilterCoreRecords } from '@/common/mongoose/schemas/core-records'; +import { CoreRecordTypeName, RecordType } from '@/types/core-records'; + +@Controller('core-records') +export class CoreRecordsController { + constructor(private readonly coreRecordsService: CoreRecordsService) {} + + // Add controller methods here + // get all records paginated and filtered + async getAllRecords({ + page, + limit, + filter, + }: { + page: number; + limit: number; + filter: FilterCoreRecords; + }) { + return this.coreRecordsService.getAllRecords({ page, limit, filter }); + } + + // get a single record by id + async getRecordById(id: string) { + return this.coreRecordsService.getRecordById(id); + } + + // create a new record + async createRecord( + record: RecordType, + recordType: CoreRecordTypeName, + createdBy: string, + ) { + return this.coreRecordsService.createRecord(record, recordType, createdBy); + } + + // update a record + async updateRecord(id: string, record: RecordType, updatedBy: string) { + return this.coreRecordsService.updateRecord(id, record, updatedBy); + } +} diff --git a/src/core-records/core-records.module.ts b/src/core-records/core-records.module.ts new file mode 100644 index 0000000..304c928 --- /dev/null +++ b/src/core-records/core-records.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { CoreRecordsService } from './core-records.service'; +import { CoreRecordsController } from './core-records.controller'; + +@Module({ + providers: [CoreRecordsService], + controllers: [CoreRecordsController], +}) +export class CoreRecordsModule {} diff --git a/src/core-records/core-records.service.spec.ts b/src/core-records/core-records.service.spec.ts new file mode 100644 index 0000000..06e0b3a --- /dev/null +++ b/src/core-records/core-records.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { CoreRecordsService } from './core-records.service'; + +describe('CoreRecordsService', () => { + let service: CoreRecordsService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [CoreRecordsService], + }).compile(); + + service = module.get(CoreRecordsService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/core-records/core-records.service.ts b/src/core-records/core-records.service.ts new file mode 100644 index 0000000..ed2bdda --- /dev/null +++ b/src/core-records/core-records.service.ts @@ -0,0 +1,130 @@ +import { + CoreRecord, + CoreRecordDocument, + FilterCoreRecords, +} from '@/common/mongoose/schemas/core-records'; +import { CoreRecordTypeName, RecordType } from '@/types/core-records'; +import { Injectable, Logger } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model } from 'mongoose'; + +@Injectable() +export class CoreRecordsService { + constructor( + @InjectModel(CoreRecord.name) + private readonly coreRecordModel: Model, + ) {} + // Get all records paginated and filtered + async getAllRecords({ + page, + limit, + filter, + }: { + page: number; + limit: number; + filter: FilterCoreRecords; + }) { + try { + filter.archivedAt = undefined; + const query = this.coreRecordModel.find(filter); + + const total = await this.coreRecordModel.countDocuments(filter).exec(); + const data = await query + .skip((page - 1) * limit) + .limit(limit) + .exec(); + + return { data, total }; + } catch (error) { + Logger.error('Error getting all records', { filter, error }); + return null; + } + } + + // get a single record by id + async getRecordById(id: string) { + try { + const record = await this.coreRecordModel + .find({ + _id: id, + archivedAt: null, + }) + .exec(); + return record; + } catch (error) { + Logger.error('Error getting record by id', { id, error }); + return null; + } + } + + async createRecord( + record: RecordType, + recordType: CoreRecordTypeName, + createdBy: string, + ) { + try { + const newRecord = new this.coreRecordModel({ + record, + type: recordType, + createdAt: new Date(), + createdBy, + updatedAt: new Date(), + updatedBy: createdBy, + }); + await newRecord.save(); + return newRecord; + } catch (error) { + Logger.error('Error saving record', { + record, + createdBy, + recordType, + error, + }); + } + } + + //update a record by id + async updateRecord( + id: string, + record: RecordType, + updatedBy: string, + ): Promise { + try { + const updatedRecord = await this.coreRecordModel + .findByIdAndUpdate( + id, + { + record, + updatedAt: new Date(), + updatedBy, + }, + { new: true }, + ) + .exec(); + return updatedRecord; + } catch (error) { + Logger.error('Error updating record', { id, record, updatedBy, error }); + return null; + } + } + + // delete a record by id + async deleteRecord(id: string, deletedBy: string) { + try { + const deletedRecord = await this.coreRecordModel + .findByIdAndUpdate( + id, + { + archivedAt: new Date(), + archivedBy: deletedBy, + }, + { new: true }, + ) + .exec(); + return deletedRecord; + } catch (error) { + Logger.error('Error deleting record', { id, deletedBy, error }); + return null; + } + } +} diff --git a/src/types/core-records.ts b/src/types/core-records.ts new file mode 100644 index 0000000..f2be36d --- /dev/null +++ b/src/types/core-records.ts @@ -0,0 +1,35 @@ +export type RecordType = MemberRecord | ProjectRecord | MentorRecord; + +export enum CoreRecordTypeName { + Member = 'Member', + Project = 'Project', + Mentor = 'Mentor', +} + +export interface ICoreRecord { + record: RecordType; + type: CoreRecordTypeName; + createdAt: Date; + createdBy: string; + updatedAt: Date; + updatedBy: Date | null; +} + +export interface MemberRecord { + name: string; + discordUser: string; + links: { + github: string; + linkedIn: string; + }; + description: string; +} + +export interface ProjectRecord { + githubLink: string; + discordLink: string; +} + +export interface MentorRecord { + stud: null; +} From 90c9f932080796cb86314559e43ae2d2a8d83233 Mon Sep 17 00:00:00 2001 From: Darkmift Date: Thu, 16 May 2024 14:27:08 +0300 Subject: [PATCH 2/4] Added New schemas and types --- src/common/mongoose/schemas/core-records.ts | 24 +++++--------- .../mongoose/schemas/records/memberRecord.ts | 31 +++++++++++++++++++ .../mongoose/schemas/records/mentorRecord.ts | 13 ++++++++ .../mongoose/schemas/records/projectRecord.ts | 16 ++++++++++ .../mongoose/schemas/type/RecordType.ts | 11 +++++++ src/types/core-records.ts | 14 +++------ 6 files changed, 83 insertions(+), 26 deletions(-) create mode 100644 src/common/mongoose/schemas/records/memberRecord.ts create mode 100644 src/common/mongoose/schemas/records/mentorRecord.ts create mode 100644 src/common/mongoose/schemas/records/projectRecord.ts create mode 100644 src/common/mongoose/schemas/type/RecordType.ts diff --git a/src/common/mongoose/schemas/core-records.ts b/src/common/mongoose/schemas/core-records.ts index 62c8712..6fec266 100644 --- a/src/common/mongoose/schemas/core-records.ts +++ b/src/common/mongoose/schemas/core-records.ts @@ -1,8 +1,4 @@ -import { - CoreRecordTypeName, - ICoreRecord, - RecordType, -} from '@/types/core-records'; +import { CoreRecordTypeName, ICoreRecord } from '@/types/core-records'; import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { type HydratedDocument } from 'mongoose'; @@ -10,10 +6,10 @@ export type CoreRecordDocument = HydratedDocument; @Schema() export class CoreRecord implements ICoreRecord { - @Prop({ required: true }) - record: RecordType; + @Prop({ required: true, refPath: 'type' }) + recordId: string; - @Prop({ required: true }) + @Prop({ required: true, enum: CoreRecordTypeName }) type: CoreRecordTypeName; @Prop({ required: true }) @@ -22,18 +18,12 @@ export class CoreRecord implements ICoreRecord { @Prop({ required: true }) createdBy: string; - @Prop({ required: true }) - updatedAt: Date; - - @Prop({ required: true }) - updatedBy: Date | null; - @Prop({ required: false }) - archivedAt: Date | null; + archivedAt: Date; @Prop({ required: false }) - archivedBy: string | null; + archivedBy: string; } -export type FilterCoreRecords = Partial>; +export type FilterCoreRecords = Partial>; export const CoreRecordSchema = SchemaFactory.createForClass(CoreRecord); diff --git a/src/common/mongoose/schemas/records/memberRecord.ts b/src/common/mongoose/schemas/records/memberRecord.ts new file mode 100644 index 0000000..0d89693 --- /dev/null +++ b/src/common/mongoose/schemas/records/memberRecord.ts @@ -0,0 +1,31 @@ +import { IMemberRecord } from '@/types/core-records'; +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { type HydratedDocument } from 'mongoose'; + +export type MemberRecordDocument = HydratedDocument; + +@Schema() +export class MemberRecord implements IMemberRecord { + @Prop({ required: true }) + name: string; + + @Prop({ required: true }) + discordUser: string; + + @Prop({ + required: true, + type: { + github: String, + linkedIn: String, + }, + }) + links: { + github: string; + linkedIn: string; + }; + + @Prop({ required: true }) + description: string; +} + +export const MemberRecordSchema = SchemaFactory.createForClass(MemberRecord); diff --git a/src/common/mongoose/schemas/records/mentorRecord.ts b/src/common/mongoose/schemas/records/mentorRecord.ts new file mode 100644 index 0000000..8030725 --- /dev/null +++ b/src/common/mongoose/schemas/records/mentorRecord.ts @@ -0,0 +1,13 @@ +import { IMentorRecord } from '@/types/core-records'; +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { type HydratedDocument } from 'mongoose'; + +export type MentorRecordDocument = HydratedDocument; + +@Schema() +export class MentorRecord implements IMentorRecord { + @Prop({ required: true }) + stud: string; +} + +export const MentorRecordSchema = SchemaFactory.createForClass(MentorRecord); diff --git a/src/common/mongoose/schemas/records/projectRecord.ts b/src/common/mongoose/schemas/records/projectRecord.ts new file mode 100644 index 0000000..aa647aa --- /dev/null +++ b/src/common/mongoose/schemas/records/projectRecord.ts @@ -0,0 +1,16 @@ +import { IProjectRecord } from '@/types/core-records'; +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { type HydratedDocument } from 'mongoose'; + +export type ProjectRecordDocument = HydratedDocument; + +@Schema() +export class ProjectRecord implements IProjectRecord { + @Prop({ required: true }) + githubLink: string; + + @Prop({ required: true }) + discordLink: string; +} + +export const ProjectRecordSchema = SchemaFactory.createForClass(ProjectRecord); diff --git a/src/common/mongoose/schemas/type/RecordType.ts b/src/common/mongoose/schemas/type/RecordType.ts new file mode 100644 index 0000000..3d8d4b1 --- /dev/null +++ b/src/common/mongoose/schemas/type/RecordType.ts @@ -0,0 +1,11 @@ +import { MemberRecordSchema } from '@/common/mongoose/schemas/records/memberRecord'; +import { ProjectRecordSchema } from '../records/projectRecord'; +import { MentorRecordSchema } from '../records/mentorRecord'; + +export const AllowedRecordDocumentSchemas = [ + MemberRecordSchema, + ProjectRecordSchema, + MentorRecordSchema, +] as const; + +export type RecordDocumentType = (typeof AllowedRecordDocumentSchemas)[number]; diff --git a/src/types/core-records.ts b/src/types/core-records.ts index f2be36d..7cb0960 100644 --- a/src/types/core-records.ts +++ b/src/types/core-records.ts @@ -1,5 +1,3 @@ -export type RecordType = MemberRecord | ProjectRecord | MentorRecord; - export enum CoreRecordTypeName { Member = 'Member', Project = 'Project', @@ -7,15 +5,13 @@ export enum CoreRecordTypeName { } export interface ICoreRecord { - record: RecordType; + recordId: string; type: CoreRecordTypeName; createdAt: Date; createdBy: string; - updatedAt: Date; - updatedBy: Date | null; } -export interface MemberRecord { +export interface IMemberRecord { name: string; discordUser: string; links: { @@ -25,11 +21,11 @@ export interface MemberRecord { description: string; } -export interface ProjectRecord { +export interface IProjectRecord { githubLink: string; discordLink: string; } -export interface MentorRecord { - stud: null; +export interface IMentorRecord { + stud: string; } From a6620b3cbba2b1f287d63caf494b69556d48fd0c Mon Sep 17 00:00:00 2001 From: Darkmift Date: Thu, 16 May 2024 14:27:27 +0300 Subject: [PATCH 3/4] comment --- src/members/members.service.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/members/members.service.spec.ts b/src/members/members.service.spec.ts index 663550a..87d1605 100644 --- a/src/members/members.service.spec.ts +++ b/src/members/members.service.spec.ts @@ -72,7 +72,7 @@ describe('MembersService', () => { expect(result._id).toBeDefined(); // Ensure _id is defined }); }); - + // TODO re work creates here in future from api request describe.skip('createMany', () => { it('should create multiple members', async () => { const createMemberDtos: CreateMemberDto[] = [ From 67e860014b830bcc047e083d4b578c6ed2231db7 Mon Sep 17 00:00:00 2001 From: Darkmift Date: Thu, 16 May 2024 14:27:58 +0300 Subject: [PATCH 4/4] Stabilized feature --- .../core-records.controller.spec.ts | 29 +++++++++++++++++++ src/core-records/core-records.controller.ts | 15 +++++----- src/core-records/core-records.module.ts | 10 +++++++ src/core-records/core-records.service.spec.ts | 19 +++++++++++- src/core-records/core-records.service.ts | 14 ++++----- 5 files changed, 71 insertions(+), 16 deletions(-) diff --git a/src/core-records/core-records.controller.spec.ts b/src/core-records/core-records.controller.spec.ts index 31fb03d..a1d7e30 100644 --- a/src/core-records/core-records.controller.spec.ts +++ b/src/core-records/core-records.controller.spec.ts @@ -1,12 +1,24 @@ import { Test, TestingModule } from '@nestjs/testing'; import { CoreRecordsController } from './core-records.controller'; +import { CoreRecordsService } from './core-records.service'; describe('CoreRecordsController', () => { let controller: CoreRecordsController; + let mockCoreRecordsService: Partial; beforeEach(async () => { + mockCoreRecordsService = { + getAllRecords: jest.fn(), + }; + const module: TestingModule = await Test.createTestingModule({ controllers: [CoreRecordsController], + providers: [ + { + provide: CoreRecordsService, + useValue: mockCoreRecordsService, + }, + ], }).compile(); controller = module.get(CoreRecordsController); @@ -15,4 +27,21 @@ describe('CoreRecordsController', () => { it('should be defined', () => { expect(controller).toBeDefined(); }); + + it('getAllRecords should call service with correct parameters', async () => { + const filter = { + type: 'type', + }; + await controller.getAllRecords({ + page: 1, + limit: 10, + filter, + }); + + expect(mockCoreRecordsService.getAllRecords).toHaveBeenCalledWith({ + page: 1, + limit: 10, + filter, + }); + }); }); diff --git a/src/core-records/core-records.controller.ts b/src/core-records/core-records.controller.ts index e613fe7..16a1781 100644 --- a/src/core-records/core-records.controller.ts +++ b/src/core-records/core-records.controller.ts @@ -1,7 +1,7 @@ import { Controller } from '@nestjs/common'; import { CoreRecordsService } from './core-records.service'; import { FilterCoreRecords } from '@/common/mongoose/schemas/core-records'; -import { CoreRecordTypeName, RecordType } from '@/types/core-records'; +import { CoreRecordTypeName } from '@/types/core-records'; @Controller('core-records') export class CoreRecordsController { @@ -28,15 +28,14 @@ export class CoreRecordsController { // create a new record async createRecord( - record: RecordType, + recordId: string, recordType: CoreRecordTypeName, createdBy: string, ) { - return this.coreRecordsService.createRecord(record, recordType, createdBy); - } - - // update a record - async updateRecord(id: string, record: RecordType, updatedBy: string) { - return this.coreRecordsService.updateRecord(id, record, updatedBy); + return this.coreRecordsService.createRecord( + recordId, + recordType, + createdBy, + ); } } diff --git a/src/core-records/core-records.module.ts b/src/core-records/core-records.module.ts index 304c928..0b1c319 100644 --- a/src/core-records/core-records.module.ts +++ b/src/core-records/core-records.module.ts @@ -1,9 +1,19 @@ import { Module } from '@nestjs/common'; import { CoreRecordsService } from './core-records.service'; import { CoreRecordsController } from './core-records.controller'; +import { MongooseModule } from '@nestjs/mongoose'; +import { + CoreRecord, + CoreRecordSchema, +} from '@/common/mongoose/schemas/core-records'; @Module({ providers: [CoreRecordsService], controllers: [CoreRecordsController], + imports: [ + MongooseModule.forFeature([ + { name: CoreRecord.name, schema: CoreRecordSchema }, + ]), + ], }) export class CoreRecordsModule {} diff --git a/src/core-records/core-records.service.spec.ts b/src/core-records/core-records.service.spec.ts index 06e0b3a..382906e 100644 --- a/src/core-records/core-records.service.spec.ts +++ b/src/core-records/core-records.service.spec.ts @@ -1,17 +1,34 @@ import { Test, TestingModule } from '@nestjs/testing'; import { CoreRecordsService } from './core-records.service'; +import { + TestDbModule, + closeInMongodConnection, +} from '../../test/mocks/module/mongo-in-memory'; +import { MongooseModule } from '@nestjs/mongoose'; +import { CoreRecordSchema } from '@/common/mongoose/schemas/core-records'; describe('CoreRecordsService', () => { let service: CoreRecordsService; - beforeEach(async () => { + beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [CoreRecordsService], + imports: [ + TestDbModule, + MongooseModule.forFeature([ + { name: 'CoreRecord', schema: CoreRecordSchema }, + ]), + // Include any setup for in-memory MongoDB here + ], }).compile(); service = module.get(CoreRecordsService); }); + afterAll(async () => { + await closeInMongodConnection(); // Close the database connection after all tests + }); + it('should be defined', () => { expect(service).toBeDefined(); }); diff --git a/src/core-records/core-records.service.ts b/src/core-records/core-records.service.ts index ed2bdda..bcebf31 100644 --- a/src/core-records/core-records.service.ts +++ b/src/core-records/core-records.service.ts @@ -3,7 +3,7 @@ import { CoreRecordDocument, FilterCoreRecords, } from '@/common/mongoose/schemas/core-records'; -import { CoreRecordTypeName, RecordType } from '@/types/core-records'; +import { CoreRecordTypeName } from '@/types/core-records'; import { Injectable, Logger } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; @@ -58,13 +58,13 @@ export class CoreRecordsService { } async createRecord( - record: RecordType, + recordId: string, recordType: CoreRecordTypeName, createdBy: string, ) { try { const newRecord = new this.coreRecordModel({ - record, + recordId, type: recordType, createdAt: new Date(), createdBy, @@ -75,7 +75,7 @@ export class CoreRecordsService { return newRecord; } catch (error) { Logger.error('Error saving record', { - record, + recordId, createdBy, recordType, error, @@ -86,7 +86,7 @@ export class CoreRecordsService { //update a record by id async updateRecord( id: string, - record: RecordType, + recordId: string, updatedBy: string, ): Promise { try { @@ -94,7 +94,7 @@ export class CoreRecordsService { .findByIdAndUpdate( id, { - record, + recordId, updatedAt: new Date(), updatedBy, }, @@ -103,7 +103,7 @@ export class CoreRecordsService { .exec(); return updatedRecord; } catch (error) { - Logger.error('Error updating record', { id, record, updatedBy, error }); + Logger.error('Error updating record', { id, recordId, updatedBy, error }); return null; } }