Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
JWT_SECRET=your_jwt_secret
JWT_EXPIRY=1h
PORT=3000
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
17 changes: 17 additions & 0 deletions src/achievements/achievements.module.ts
Original file line number Diff line number Diff line change
@@ -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 {}
21 changes: 20 additions & 1 deletion src/app.module.ts
Original file line number Diff line number Diff line change
@@ -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 {}
44 changes: 44 additions & 0 deletions src/config/app-config.module.spec.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
19 changes: 19 additions & 0 deletions src/config/app-config.module.ts
Original file line number Diff line number Diff line change
@@ -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 {}
20 changes: 20 additions & 0 deletions src/events/event-payloads.ts
Original file line number Diff line number Diff line change
@@ -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;
}
12 changes: 12 additions & 0 deletions src/events/event.service.ts
Original file line number Diff line number Diff line change
@@ -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<T>(event: EventName, payload: T): void {
this.eventEmitter.emit(event, payload);
}
}
6 changes: 6 additions & 0 deletions src/events/events.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export enum EventName {
SessionCompleted = 'session.completed',
ScoreUpdated = 'score.updated',
AchievementUnlocked = 'achievement.unlocked',
RewardGranted = 'reward.granted',
}
6 changes: 4 additions & 2 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -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<NestExpressApplication>(AppModule);
await app.listen(process.env.PORT || 3000);
const configService = app.get(ConfigService);
const port = configService.get<number>('PORT') || 3000;
await app.listen(port);
}
bootstrap();
17 changes: 17 additions & 0 deletions src/notifications/notifications.module.ts
Original file line number Diff line number Diff line change
@@ -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 {}
17 changes: 17 additions & 0 deletions src/rewards/rewards.module.ts
Original file line number Diff line number Diff line change
@@ -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 {}
18 changes: 18 additions & 0 deletions src/scoring/scoring.module.ts
Original file line number Diff line number Diff line change
@@ -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 {}