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..6fec266 --- /dev/null +++ b/src/common/mongoose/schemas/core-records.ts @@ -0,0 +1,29 @@ +import { CoreRecordTypeName, ICoreRecord } 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, refPath: 'type' }) + recordId: string; + + @Prop({ required: true, enum: CoreRecordTypeName }) + type: CoreRecordTypeName; + + @Prop({ required: true }) + createdAt: Date; + + @Prop({ required: true }) + createdBy: string; + + @Prop({ required: false }) + archivedAt: Date; + + @Prop({ required: false }) + archivedBy: string; +} + +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/core-records/core-records.controller.spec.ts b/src/core-records/core-records.controller.spec.ts new file mode 100644 index 0000000..a1d7e30 --- /dev/null +++ b/src/core-records/core-records.controller.spec.ts @@ -0,0 +1,47 @@ +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); + }); + + 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 new file mode 100644 index 0000000..16a1781 --- /dev/null +++ b/src/core-records/core-records.controller.ts @@ -0,0 +1,41 @@ +import { Controller } from '@nestjs/common'; +import { CoreRecordsService } from './core-records.service'; +import { FilterCoreRecords } from '@/common/mongoose/schemas/core-records'; +import { CoreRecordTypeName } 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( + recordId: string, + recordType: CoreRecordTypeName, + createdBy: string, + ) { + 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 new file mode 100644 index 0000000..0b1c319 --- /dev/null +++ b/src/core-records/core-records.module.ts @@ -0,0 +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 new file mode 100644 index 0000000..382906e --- /dev/null +++ b/src/core-records/core-records.service.spec.ts @@ -0,0 +1,35 @@ +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; + + 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 new file mode 100644 index 0000000..bcebf31 --- /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 } 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( + recordId: string, + recordType: CoreRecordTypeName, + createdBy: string, + ) { + try { + const newRecord = new this.coreRecordModel({ + recordId, + type: recordType, + createdAt: new Date(), + createdBy, + updatedAt: new Date(), + updatedBy: createdBy, + }); + await newRecord.save(); + return newRecord; + } catch (error) { + Logger.error('Error saving record', { + recordId, + createdBy, + recordType, + error, + }); + } + } + + //update a record by id + async updateRecord( + id: string, + recordId: string, + updatedBy: string, + ): Promise { + try { + const updatedRecord = await this.coreRecordModel + .findByIdAndUpdate( + id, + { + recordId, + updatedAt: new Date(), + updatedBy, + }, + { new: true }, + ) + .exec(); + return updatedRecord; + } catch (error) { + Logger.error('Error updating record', { id, recordId, 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/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[] = [ diff --git a/src/types/core-records.ts b/src/types/core-records.ts new file mode 100644 index 0000000..7cb0960 --- /dev/null +++ b/src/types/core-records.ts @@ -0,0 +1,31 @@ +export enum CoreRecordTypeName { + Member = 'Member', + Project = 'Project', + Mentor = 'Mentor', +} + +export interface ICoreRecord { + recordId: string; + type: CoreRecordTypeName; + createdAt: Date; + createdBy: string; +} + +export interface IMemberRecord { + name: string; + discordUser: string; + links: { + github: string; + linkedIn: string; + }; + description: string; +} + +export interface IProjectRecord { + githubLink: string; + discordLink: string; +} + +export interface IMentorRecord { + stud: string; +}