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
6 changes: 6 additions & 0 deletions microservices/reporting-service/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY dist ./dist
CMD ["node", "dist/main"]
1 change: 1 addition & 0 deletions microservices/reporting-service/nest-cli.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"$schema":"https://json.schemastore.org/nest-cli","collection":"@nestjs/schematics","sourceRoot":"src"}
51 changes: 51 additions & 0 deletions microservices/reporting-service/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"name": "reporting-service",
"version": "0.0.1",
"description": "Analytics reporting service for custom reports and business intelligence",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"build": "nest build",
"start": "nest start",
"start:dev": "nest start --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,test}/**/*.ts\" --fix",
"type-check": "tsc --noEmit",
"test": "jest",
"test:cov": "jest --coverage"
},
"dependencies": {
"@nestjs/common": "^10.4.4",
"@nestjs/config": "^3.2.3",
"@nestjs/core": "^10.4.4",
"@nestjs/platform-express": "^10.4.4",
"@nestjs/typeorm": "^11.0.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"pg": "^8.13.1",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"typeorm": "^0.3.20"
},
"devDependencies": {
"@nestjs/cli": "^10.4.5",
"@nestjs/schematics": "^10.1.4",
"@nestjs/testing": "^10.4.4",
"@types/jest": "^29.5.12",
"@types/node": "^20.14.15",
"jest": "^29.7.0",
"ts-jest": "^29.2.4",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.5.4"
},
"jest": {
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": { "^.+\\.(t|j)s$": "ts-jest" },
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}
26 changes: 26 additions & 0 deletions microservices/reporting-service/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Report } from './report.entity';
import { ReportService } from './report.service';
import { ReportController } from './report.controller';

@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: (config: ConfigService) => ({
type: 'postgres',
url: config.get('DATABASE_URL'),
entities: [Report],
synchronize: true,
}),
inject: [ConfigService],
}),
TypeOrmModule.forFeature([Report]),
],
controllers: [ReportController],
providers: [ReportService],
})
export class AppModule {}
11 changes: 11 additions & 0 deletions microservices/reporting-service/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableCors();
const port = process.env.PORT || 3013;
await app.listen(port, '0.0.0.0');
console.log(`Reporting Service running on port ${port}`);
}
bootstrap();
31 changes: 31 additions & 0 deletions microservices/reporting-service/src/report.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Controller, Get, Post, Param, Body, Query } from '@nestjs/common';
import { ReportService } from './report.service';
import { Report, ReportFormat } from './report.entity';

@Controller('reports')
export class ReportController {
constructor(private readonly reportService: ReportService) {}

@Post()
create(@Body() body: Partial<Report>): Promise<Report> {
return this.reportService.create(body);
}

@Get()
findAll(): Promise<Report[]> {
return this.reportService.findAll();
}

@Get(':id')
findOne(@Param('id') id: string): Promise<Report | null> {
return this.reportService.findOne(id);
}

@Get(':id/export')
export(
@Param('id') id: string,
@Query('format') format: ReportFormat = ReportFormat.JSON,
) {
return this.reportService.export(id, format);
}
}
31 changes: 31 additions & 0 deletions microservices/reporting-service/src/report.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm';

export enum ReportFormat {
CSV = 'csv',
PDF = 'pdf',
JSON = 'json',
}

@Entity('reports')
export class Report {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column()
name: string;

@Column({ nullable: true })
description: string;

@Column({ type: 'jsonb', default: {} })
query: Record<string, unknown>;

@Column({ type: 'enum', enum: ReportFormat, default: ReportFormat.JSON })
format: ReportFormat;

@Column({ nullable: true })
scheduledCron: string;

@CreateDateColumn()
createdAt: Date;
}
34 changes: 34 additions & 0 deletions microservices/reporting-service/src/report.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Test } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { ReportService } from './report.service';
import { Report, ReportFormat } from './report.entity';

describe('ReportService', () => {
let service: ReportService;
const mockRepo = {
create: jest.fn((d) => d),
save: jest.fn((d) => Promise.resolve({ id: 'uuid', ...d })),
find: jest.fn(() => Promise.resolve([])),
findOneBy: jest.fn(() => Promise.resolve({ id: 'uuid', name: 'test', query: {}, format: ReportFormat.JSON })),
};

beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
ReportService,
{ provide: getRepositoryToken(Report), useValue: mockRepo },
],
}).compile();
service = module.get(ReportService);
});

it('creates a report', async () => {
const result = await service.create({ name: 'Monthly Report' });
expect(result).toBeDefined();
});

it('exports a report as JSON', async () => {
const result = await service.export('uuid', ReportFormat.JSON);
expect(result.format).toBe(ReportFormat.JSON);
});
});
31 changes: 31 additions & 0 deletions microservices/reporting-service/src/report.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Report, ReportFormat } from './report.entity';

@Injectable()
export class ReportService {
constructor(
@InjectRepository(Report)
private readonly reportRepo: Repository<Report>,
) {}

create(data: Partial<Report>): Promise<Report> {
return this.reportRepo.save(this.reportRepo.create(data));
}

findAll(): Promise<Report[]> {
return this.reportRepo.find();
}

findOne(id: string): Promise<Report | null> {
return this.reportRepo.findOneBy({ id });
}

async export(id: string, format: ReportFormat): Promise<{ data: string; format: ReportFormat }> {
const report = await this.reportRepo.findOneBy({ id });
if (!report) throw new Error(`Report ${id} not found`);
// Stub: real implementation would execute report.query and format output
return { data: JSON.stringify({ reportId: id, query: report.query }), format };
}
}
1 change: 1 addition & 0 deletions microservices/reporting-service/tsconfig.build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"extends":"./tsconfig.json","exclude":["node_modules","test","dist","**/*spec.ts"]}
1 change: 1 addition & 0 deletions microservices/reporting-service/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"compilerOptions":{"module":"commonjs","declaration":true,"removeComments":true,"emitDecoratorMetadata":true,"experimentalDecorators":true,"allowSyntheticDefaultImports":true,"target":"ES2021","sourceMap":true,"outDir":"./dist","baseUrl":"./","incremental":true,"skipLibCheck":true,"strictNullChecks":false,"noImplicitAny":false}}