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
12 changes: 12 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Queue Dashboard - Revised (Custom REST API Dashboard, no new deps needed)

## Plan Steps:

1. [x] Add Redis config ✅
2. [x] Create src/queue/queue.module.ts & queue.controller.ts ✅
3. [x] Update src/events/events.module.ts & events.service.ts with processor ✅
4. [x] Update src/app.module.ts: import QueueModule ✅
5. [x] Add example test-job endpoint to health.controller.ts ✅
6. [ ] Test: docker-compose up redis db backend, POST /health/test-job, GET /queue/dashboard

Next: Test & complete.
28 changes: 28 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
module.exports = {
moduleFileExtensions: ['js', 'json', 'ts'],
rootDir: 'src',
testRegex: '.*\\.spec\\.ts$',
transform: {
'^.+\\.(t|j)s$': ['ts-jest', {
tsconfig: {
module: 'commonjs',
moduleResolution: 'node',
esModuleInterop: true,
allowSyntheticDefaultImports: true,
resolvePackageJsonExports: false,
emitDecoratorMetadata: true,
experimentalDecorators: true,
}
}]
},
collectCoverageFrom: ['**/*.(t|j)s'],
coverageDirectory: '../coverage',
testEnvironment: 'node',
moduleNameMapper: {
'^src/(.*)$': '<rootDir>/$1',
},
testTimeout: 30000,
maxWorkers: 1,
};


17 changes: 0 additions & 17 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,22 +84,5 @@
},
"prisma": {
"seed": "npx ts-node --esm prisma/seed.ts"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}
2 changes: 2 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { AdoptionModule } from './adoption/adoption.module';
import { CustodyModule } from './custody/custody.module';
import { EscrowModule } from './escrow/escrow.module';
import { EventsModule } from './events/events.module';
import { QueueModule } from './queue/queue.module';
import { StellarModule } from './stellar/stellar.module';
import { AuthModule } from './auth/auth.module';
import { HealthModule } from './health/health.module';
Expand All @@ -22,6 +23,7 @@ import { CloudinaryModule } from './cloudinary/cloudinary.module';
CustodyModule,
EscrowModule,
EventsModule,
QueueModule,
StellarModule,
AuthModule,
HealthModule,
Expand Down
8 changes: 8 additions & 0 deletions src/config/configuration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { config } from 'dotenv';

config();

export default () => ({
port: parseInt(process.env.PORT || '3000', 10),
redis: process.env.REDIS_URL || 'redis://localhost:6379',
});
5 changes: 3 additions & 2 deletions src/events/events.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { Global, Module } from '@nestjs/common';
import { EventsService } from './events.service';
import { PrismaModule } from '../prisma/prisma.module';

@Global() // Makes EventsService available application-wide without needing to import EventsModule everywhere
@Global()
@Module({
imports: [PrismaModule],
providers: [EventsService],
exports: [EventsService], // Export it so other modules can use it
exports: [EventsService],
})
export class EventsModule {}

36 changes: 27 additions & 9 deletions src/health/health.controller.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
import { Controller, Get } from '@nestjs/common';
import { Controller, Get, Post, Body } from '@nestjs/common';
import {
HealthCheckService,
HealthCheck,
PrismaHealthIndicator,
} from '@nestjs/terminus';
import { PrismaService } from '../prisma/prisma.service';
import { PrismaClient } from '@prisma/client';
import { InjectQueue } from '@nestjs/bullmq';
import { Queue } from 'bullmq';
import { EventEntityType, EventType } from '@prisma/client';
import { EventsService } from '../events/events.service';

@Controller('health')
export class HealthController {
constructor(
private health: HealthCheckService,
private db: PrismaHealthIndicator,
private prisma: PrismaService,
private eventsService: EventsService,
@InjectQueue('events') private eventsQueue: Queue,
) {}

@Get()
Expand All @@ -30,12 +36,24 @@ export class HealthController {
() => this.db.pingCheck('database', this.prisma),
]);
}
// @Get()
// @HealthCheck()
// check() {
// return this.health.check([
// // This checks if the database is reachable via Prisma
// () => this.db.pingCheck('database', this.prisma as PrismaClient),
// ]);
// }

@Post('test-job')
async addTestJob(
@Body()
body: {
entityType: EventEntityType;
entityId: string;
eventType: EventType;
payload?: any;
},
) {
const jobData = {
entityType: body.entityType || 'PET',
entityId: body.entityId || 'test-pet-1',
eventType: body.eventType || 'PET_LISTED',
payload: body.payload || { test: true },
};
const job = await this.eventsQueue.add('log-event', jobData);
return { jobId: job.id, message: 'Test job added to events queue' };
}
}
87 changes: 87 additions & 0 deletions src/queue/queue.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import {
Controller,
Get,
Post,
Param,
ParseUUIDPipe,
Body,
} from '@nestjs/common';
import { InjectQueue } from '@nestjs/bullmq';
import { Queue, Job } from 'bullmq';
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';

@ApiTags('Queue Dashboard')
@Controller('queue')
export class QueueController {
constructor(
@InjectQueue('events') private readonly eventsQueue: Queue,
@InjectQueue('notifications') private readonly notificationsQueue: Queue,
) {}

@Get('dashboard')
@ApiOperation({
summary: 'Get queue dashboard overview (counts and recent jobs)',
})
@ApiResponse({ status: 200, description: 'Dashboard data' })
async getDashboard() {
const states = ['active', 'waiting', 'completed', 'failed'] as const;
const queues = ['events', 'notifications'] as const;
const data: Record<string, any> = {};

for (const queueName of queues) {
const queue =
queueName === 'events' ? this.eventsQueue : this.notificationsQueue;
data[queueName] = {};
for (const state of states) {
const jobs = await queue.getJobs([state]);
data[queueName][state] = {
count: jobs.length,
jobs: jobs.slice(0, 5), // recent 5
};
}
}

return {
timestamp: new Date().toISOString(),
queues: data,
};
}

@Get(':name/:state')
@ApiOperation({ summary: 'List jobs in specific state' })
async getJobs(@Param('name') name: string, @Param('state') state: string) {
const queue =
name === 'events' ? this.eventsQueue : this.notificationsQueue;
const jobs = await queue.getJobs([state as any]);
return jobs.map((job: Job) => job.toJSON());
}

@Get(':name/job/:id')
@ApiOperation({ summary: 'Inspect single job' })
async getJob(@Param('name') name: string, @Param('id') id: string) {
const queue =
name === 'events' ? this.eventsQueue : this.notificationsQueue;
const job = await queue.getJob(id);
return job ? job.toJSON() : null;
}

@Post(':name/job/:id/retry')
@ApiOperation({ summary: 'Retry a failed job' })
async retryJob(@Param('name') name: string, @Param('id') id: string) {
const queue =
name === 'events' ? this.eventsQueue : this.notificationsQueue;
const job = await queue.getJob(id);
if (!job) return { error: 'Job not found' };
await job.retry();
return { success: true, message: 'Job retried' };
}

@Post('add/:name')
@ApiOperation({ summary: 'Add test job to queue' })
async addJob(@Param('name') name: string, @Body() data: any) {
const queue =
name === 'events' ? this.eventsQueue : this.notificationsQueue;
const job = await queue.add('test', data);
return { jobId: job.id };
}
}
29 changes: 29 additions & 0 deletions src/queue/queue.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Module } from '@nestjs/common';
import { BullModule } from '@nestjs/bullmq';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { QueueController } from './queue.controller';

@Module({
imports: [
BullModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => {
const redisUrl = configService.get<string>('redis') || 'redis://localhost:6379';
return {
connection: { url: redisUrl },
};
},
inject: [ConfigService],
}),
BullModule.registerQueue({
name: 'events',
}),
BullModule.registerQueue({
name: 'notifications',
}),
],
controllers: [QueueController],
})
export class QueueModule {}


File renamed without changes.
15 changes: 13 additions & 2 deletions test/jest-e2e.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
"^.+\\.(t|j)s$": ["ts-jest", {
"tsconfig": {
"module": "commonjs",
"moduleResolution": "node",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"resolvePackageJsonExports": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true
}
}]
},
"testTimeout": 30000
}
Loading