diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..f06c2d8 --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +DATABASE_URL=postgresql://user:password@localhost:5432/dbname +JWT_SECRET=your_jwt_secret +JWT_EXPIRY=1h +PORT=3000 diff --git a/package.json b/package.json index dda7360..3dd518e 100644 --- a/package.json +++ b/package.json @@ -22,10 +22,13 @@ }, "homepage": "https://github.com/MindFlowInteractive/logiquest_api#readme", "dependencies": { + "@nestjs/config": "^3.2.0", + "joi": "^17.12.0", "@nestjs/common": "^11.1.27", "@nestjs/core": "^11.1.27", "@nestjs/platform-express": "^11.1.27", - "reflect-metadata": "^0.2.2" + "reflect-metadata": "^0.2.2", + "@nestjs/event-emitter": "^2.0.0" }, "devDependencies": { "@nestjs/cli": "^11.0.23", diff --git a/src/achievements/achievements.module.ts b/src/achievements/achievements.module.ts new file mode 100644 index 0000000..2f97e4f --- /dev/null +++ b/src/achievements/achievements.module.ts @@ -0,0 +1,17 @@ +import { Module, Injectable } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; +import { EventName } from '../events/events.enum'; +import { AchievementUnlockedPayload } from '../events/event-payloads'; + +@Injectable() +export class AchievementsListener { + @OnEvent(EventName.AchievementUnlocked) + async handleAchievementUnlocked(payload: AchievementUnlockedPayload) { + console.log('AchievementsListener received AchievementUnlocked:', payload); + } +} + +@Module({ + providers: [AchievementsListener], +}) +export class AchievementsModule {} diff --git a/src/app.module.ts b/src/app.module.ts index 01068c5..ca94f4e 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,4 +1,23 @@ import { Module } from '@nestjs/common'; +import { EventEmitterModule } from '@nestjs/event-emitter'; +import { AppConfigModule } from './config/app-config.module'; +import { EventService } from './events/event.service'; +import { ScoringModule } from './scoring/scoring.module'; +import { AchievementsModule } from './achievements/achievements.module'; +import { RewardsModule } from './rewards/rewards.module'; +import { NotificationsModule } from './notifications/notifications.module'; -@Module({}) +@Module({ + imports: [ + AppConfigModule, + EventEmitterModule.forRoot({ + global: true, + }), + ScoringModule, + AchievementsModule, + RewardsModule, + NotificationsModule, + ], + providers: [EventService], +}) export class AppModule {} diff --git a/src/config/app-config.module.spec.ts b/src/config/app-config.module.spec.ts new file mode 100644 index 0000000..6145e06 --- /dev/null +++ b/src/config/app-config.module.spec.ts @@ -0,0 +1,44 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ConfigModule } from '@nestjs/config'; +import * as Joi from 'joi'; + +describe('Config Validation', () => { + const configFactory = () => + ConfigModule.forRoot({ + isGlobal: true, + validationSchema: Joi.object({ + DATABASE_URL: Joi.string().required(), + JWT_SECRET: Joi.string().required(), + JWT_EXPIRY: Joi.string().required(), + PORT: Joi.number().required(), + }), + }); + + it('should fail when required env vars are missing', async () => { + // Clear env vars + delete process.env.DATABASE_URL; + delete process.env.JWT_SECRET; + delete process.env.JWT_EXPIRY; + delete process.env.PORT; + + await expect( + Test.createTestingModule({ + imports: [configFactory()], + }).compile(), + ).rejects.toThrow(); + }); + + it('should succeed when all required env vars are provided', async () => { + process.env.DATABASE_URL = 'postgres://test'; + process.env.JWT_SECRET = 'secret'; + process.env.JWT_EXPIRY = '1h'; + process.env.PORT = '3000'; + + const moduleRef: TestingModule = await Test.createTestingModule({ + imports: [configFactory()], + }).compile(); + + expect(moduleRef).toBeDefined(); + await moduleRef.close(); + }); +}); diff --git a/src/config/app-config.module.ts b/src/config/app-config.module.ts new file mode 100644 index 0000000..55cd370 --- /dev/null +++ b/src/config/app-config.module.ts @@ -0,0 +1,19 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule as NestConfigModule } from '@nestjs/config'; +import * as Joi from 'joi'; + +@Module({ + imports: [ + NestConfigModule.forRoot({ + isGlobal: true, + validationSchema: Joi.object({ + DATABASE_URL: Joi.string().required(), + JWT_SECRET: Joi.string().required(), + JWT_EXPIRY: Joi.string().required(), + PORT: Joi.number().required(), + }), + }), + ], + exports: [NestConfigModule], +}) +export class AppConfigModule {} diff --git a/src/events/event-payloads.ts b/src/events/event-payloads.ts new file mode 100644 index 0000000..c77dfb7 --- /dev/null +++ b/src/events/event-payloads.ts @@ -0,0 +1,20 @@ +export interface SessionCompletedPayload { + sessionId: string; + userId: string; + // any other relevant fields +} + +export interface ScoreUpdatedPayload { + sessionId: string; + newScore: number; +} + +export interface AchievementUnlockedPayload { + userId: string; + achievementId: string; +} + +export interface RewardGrantedPayload { + userId: string; + rewardId: string; +} diff --git a/src/events/event.service.ts b/src/events/event.service.ts new file mode 100644 index 0000000..6c474e7 --- /dev/null +++ b/src/events/event.service.ts @@ -0,0 +1,12 @@ +import { Injectable } from '@nestjs/common'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { EventName } from './events.enum'; + +@Injectable() +export class EventService { + constructor(private readonly eventEmitter: EventEmitter2) {} + + emit(event: EventName, payload: T): void { + this.eventEmitter.emit(event, payload); + } +} diff --git a/src/events/events.enum.ts b/src/events/events.enum.ts new file mode 100644 index 0000000..2d569ef --- /dev/null +++ b/src/events/events.enum.ts @@ -0,0 +1,6 @@ +export enum EventName { + SessionCompleted = 'session.completed', + ScoreUpdated = 'score.updated', + AchievementUnlocked = 'achievement.unlocked', + RewardGranted = 'reward.granted', +} diff --git a/src/main.ts b/src/main.ts index 747b613..c631b19 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,9 +1,11 @@ import { NestFactory } from '@nestjs/core'; import type { NestExpressApplication } from '@nestjs/platform-express'; import { AppModule } from './app.module'; +import { ConfigService } from '@nestjs/config'; async function bootstrap() { - const app = await NestFactory.create(AppModule); - await app.listen(process.env.PORT || 3000); + const configService = app.get(ConfigService); + const port = configService.get('PORT') || 3000; + await app.listen(port); } bootstrap(); diff --git a/src/notifications/notifications.module.ts b/src/notifications/notifications.module.ts new file mode 100644 index 0000000..9dbaa84 --- /dev/null +++ b/src/notifications/notifications.module.ts @@ -0,0 +1,17 @@ +import { Module, Injectable } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; +import { EventName } from '../events/events.enum'; +import { SessionCompletedPayload } from '../events/event-payloads'; + +@Injectable() +export class NotificationsListener { + @OnEvent(EventName.SessionCompleted) + async handleSessionCompleted(payload: SessionCompletedPayload) { + console.log('NotificationsListener received SessionCompleted:', payload); + } +} + +@Module({ + providers: [NotificationsListener], +}) +export class NotificationsModule {} diff --git a/src/rewards/rewards.module.ts b/src/rewards/rewards.module.ts new file mode 100644 index 0000000..5715df9 --- /dev/null +++ b/src/rewards/rewards.module.ts @@ -0,0 +1,17 @@ +import { Module, Injectable } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; +import { EventName } from '../events/events.enum'; +import { RewardGrantedPayload } from '../events/event-payloads'; + +@Injectable() +export class RewardsListener { + @OnEvent(EventName.RewardGranted) + async handleRewardGranted(payload: RewardGrantedPayload) { + console.log('RewardsListener received RewardGranted:', payload); + } +} + +@Module({ + providers: [RewardsListener], +}) +export class RewardsModule {} diff --git a/src/scoring/scoring.module.ts b/src/scoring/scoring.module.ts new file mode 100644 index 0000000..0f6c9e3 --- /dev/null +++ b/src/scoring/scoring.module.ts @@ -0,0 +1,18 @@ +import { Module, Injectable } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; +import { EventName } from '../events/events.enum'; +import { ScoreUpdatedPayload } from '../events/event-payloads'; + +@Injectable() +export class ScoringListener { + @OnEvent(EventName.ScoreUpdated) + async handleScoreUpdated(payload: ScoreUpdatedPayload) { + // Placeholder implementation + console.log('ScoringListener received ScoreUpdated:', payload); + } +} + +@Module({ + providers: [ScoringListener], +}) +export class ScoringModule {}