260322:1648 Correct Coresspondence / Doing RFA / Correct CI
This commit is contained in:
@@ -67,9 +67,10 @@ Comprehensive best practices and architecture guide for NestJS applications, des
|
||||
- 9.2 [Use Message and Event Patterns Correctly](#92-use-message-and-event-patterns-correctly)
|
||||
- 9.3 [Use Message Queues for Background Jobs](#93-use-message-queues-for-background-jobs)
|
||||
10. [DevOps & Deployment](#10-devops-deployment) — **LOW-MEDIUM**
|
||||
- 10.1 [Implement Graceful Shutdown](#101-implement-graceful-shutdown)
|
||||
- 10.2 [Use ConfigModule for Environment Configuration](#102-use-configmodule-for-environment-configuration)
|
||||
- 10.3 [Use Structured Logging](#103-use-structured-logging)
|
||||
|
||||
- 10.1 [Implement Graceful Shutdown](#101-implement-graceful-shutdown)
|
||||
- 10.2 [Use ConfigModule for Environment Configuration](#102-use-configmodule-for-environment-configuration)
|
||||
- 10.3 [Use Structured Logging](#103-use-structured-logging)
|
||||
|
||||
---
|
||||
|
||||
@@ -390,7 +391,7 @@ export class UserAndOrderService {
|
||||
private userRepo: UserRepository,
|
||||
private orderRepo: OrderRepository,
|
||||
private mailer: MailService,
|
||||
private payment: PaymentService,
|
||||
private payment: PaymentService
|
||||
) {}
|
||||
|
||||
async createUser(dto: CreateUserDto) {
|
||||
@@ -461,7 +462,7 @@ export class OrdersController {
|
||||
constructor(
|
||||
private orders: OrdersService,
|
||||
private payment: PaymentService,
|
||||
private notifications: NotificationService,
|
||||
private notifications: NotificationService
|
||||
) {}
|
||||
|
||||
@Post()
|
||||
@@ -495,7 +496,7 @@ export class OrdersService {
|
||||
private emailService: EmailService,
|
||||
private analyticsService: AnalyticsService,
|
||||
private notificationService: NotificationService,
|
||||
private loyaltyService: LoyaltyService,
|
||||
private loyaltyService: LoyaltyService
|
||||
) {}
|
||||
|
||||
async createOrder(dto: CreateOrderDto): Promise<Order> {
|
||||
@@ -526,7 +527,7 @@ export class OrderCreatedEvent {
|
||||
public readonly orderId: string,
|
||||
public readonly userId: string,
|
||||
public readonly items: OrderItem[],
|
||||
public readonly total: number,
|
||||
public readonly total: number
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -535,17 +536,14 @@ export class OrderCreatedEvent {
|
||||
export class OrdersService {
|
||||
constructor(
|
||||
private eventEmitter: EventEmitter2,
|
||||
private repo: Repository<Order>,
|
||||
private repo: Repository<Order>
|
||||
) {}
|
||||
|
||||
async createOrder(dto: CreateOrderDto): Promise<Order> {
|
||||
const order = await this.repo.save(dto);
|
||||
|
||||
// Emit event - no knowledge of consumers
|
||||
this.eventEmitter.emit(
|
||||
'order.created',
|
||||
new OrderCreatedEvent(order.id, order.userId, order.items, order.total),
|
||||
);
|
||||
this.eventEmitter.emit('order.created', new OrderCreatedEvent(order.id, order.userId, order.items, order.total));
|
||||
|
||||
return order;
|
||||
}
|
||||
@@ -596,9 +594,7 @@ Create custom repositories to encapsulate complex queries and database logic. Th
|
||||
// Complex queries in services
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(
|
||||
@InjectRepository(User) private repo: Repository<User>,
|
||||
) {}
|
||||
constructor(@InjectRepository(User) private repo: Repository<User>) {}
|
||||
|
||||
async findActiveWithOrders(minOrders: number): Promise<User[]> {
|
||||
// Complex query logic mixed with business logic
|
||||
@@ -623,9 +619,7 @@ export class UsersService {
|
||||
// Custom repository with encapsulated queries
|
||||
@Injectable()
|
||||
export class UsersRepository {
|
||||
constructor(
|
||||
@InjectRepository(User) private repo: Repository<User>,
|
||||
) {}
|
||||
constructor(@InjectRepository(User) private repo: Repository<User>) {}
|
||||
|
||||
async findById(id: string): Promise<User | null> {
|
||||
return this.repo.findOne({ where: { id } });
|
||||
@@ -735,7 +729,7 @@ export class OrdersService {
|
||||
constructor(
|
||||
private usersService: UsersService,
|
||||
private inventoryService: InventoryService,
|
||||
private paymentService: PaymentService,
|
||||
private paymentService: PaymentService
|
||||
) {}
|
||||
|
||||
async createOrder(dto: CreateOrderDto): Promise<Order> {
|
||||
@@ -810,14 +804,14 @@ interface NotificationService {
|
||||
@Injectable()
|
||||
export class OrdersService {
|
||||
constructor(
|
||||
private notifications: NotificationService, // Depends on 8 methods, uses 1
|
||||
private notifications: NotificationService // Depends on 8 methods, uses 1
|
||||
) {}
|
||||
|
||||
async confirmOrder(order: Order): Promise<void> {
|
||||
await this.notifications.sendEmail(
|
||||
order.customer.email,
|
||||
'Order Confirmed',
|
||||
`Your order ${order.id} has been confirmed.`,
|
||||
`Your order ${order.id} has been confirmed.`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -825,12 +819,12 @@ export class OrdersService {
|
||||
// Testing is painful - must mock unused methods
|
||||
const mockNotificationService = {
|
||||
sendEmail: jest.fn(),
|
||||
sendSms: jest.fn(), // Never used, but required
|
||||
sendPush: jest.fn(), // Never used, but required
|
||||
sendSlack: jest.fn(), // Never used, but required
|
||||
logNotification: jest.fn(), // Never used, but required
|
||||
sendSms: jest.fn(), // Never used, but required
|
||||
sendPush: jest.fn(), // Never used, but required
|
||||
sendSlack: jest.fn(), // Never used, but required
|
||||
logNotification: jest.fn(), // Never used, but required
|
||||
getDeliveryStatus: jest.fn(), // Never used, but required
|
||||
retryFailed: jest.fn(), // Never used, but required
|
||||
retryFailed: jest.fn(), // Never used, but required
|
||||
scheduleNotification: jest.fn(), // Never used, but required
|
||||
};
|
||||
```
|
||||
@@ -887,14 +881,14 @@ export class SendGridEmailService implements EmailSender {
|
||||
@Injectable()
|
||||
export class OrdersService {
|
||||
constructor(
|
||||
@Inject(EMAIL_SENDER) private emailSender: EmailSender, // Minimal dependency
|
||||
@Inject(EMAIL_SENDER) private emailSender: EmailSender // Minimal dependency
|
||||
) {}
|
||||
|
||||
async confirmOrder(order: Order): Promise<void> {
|
||||
await this.emailSender.sendEmail(
|
||||
order.customer.email,
|
||||
'Order Confirmed',
|
||||
`Your order ${order.id} has been confirmed.`,
|
||||
`Your order ${order.id} has been confirmed.`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -932,7 +926,7 @@ type MultiChannelSender = EmailSender & SmsSender & PushSender;
|
||||
export class AlertService {
|
||||
constructor(
|
||||
@Inject(MULTI_CHANNEL_SENDER)
|
||||
private sender: EmailSender & SmsSender,
|
||||
private sender: EmailSender & SmsSender
|
||||
) {}
|
||||
|
||||
async sendCriticalAlert(user: User, message: string): Promise<void> {
|
||||
@@ -1123,9 +1117,7 @@ export class OrdersService {
|
||||
|
||||
```typescript
|
||||
// Shared test suite that any implementation must pass
|
||||
function testPaymentGatewayContract(
|
||||
createGateway: () => PaymentGateway,
|
||||
) {
|
||||
function testPaymentGatewayContract(createGateway: () => PaymentGateway) {
|
||||
describe('PaymentGateway contract', () => {
|
||||
let gateway: PaymentGateway;
|
||||
|
||||
@@ -1142,13 +1134,11 @@ function testPaymentGatewayContract(
|
||||
});
|
||||
|
||||
it('throws InvalidCurrencyException for unsupported currency', async () => {
|
||||
await expect(gateway.charge(1000, 'INVALID'))
|
||||
.rejects.toThrow(InvalidCurrencyException);
|
||||
await expect(gateway.charge(1000, 'INVALID')).rejects.toThrow(InvalidCurrencyException);
|
||||
});
|
||||
|
||||
it('throws TransactionNotFoundException for invalid refund', async () => {
|
||||
await expect(gateway.refund('nonexistent'))
|
||||
.rejects.toThrow(TransactionNotFoundException);
|
||||
await expect(gateway.refund('nonexistent')).rejects.toThrow(TransactionNotFoundException);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1204,7 +1194,7 @@ export class UsersService {
|
||||
export class UsersService {
|
||||
constructor(
|
||||
private readonly userRepo: UserRepository,
|
||||
@Inject('CONFIG') private readonly config: ConfigType,
|
||||
@Inject('CONFIG') private readonly config: ConfigType
|
||||
) {}
|
||||
|
||||
async findAll(): Promise<User[]> {
|
||||
@@ -1359,7 +1349,9 @@ interface PaymentGateway {
|
||||
|
||||
@Injectable()
|
||||
export class StripeService implements PaymentGateway {
|
||||
charge(amount: number) { /* ... */ }
|
||||
charge(amount: number) {
|
||||
/* ... */
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@@ -1398,9 +1390,7 @@ export class MockPaymentService implements PaymentGateway {
|
||||
providers: [
|
||||
{
|
||||
provide: PAYMENT_GATEWAY,
|
||||
useClass: process.env.NODE_ENV === 'test'
|
||||
? MockPaymentService
|
||||
: StripeService,
|
||||
useClass: process.env.NODE_ENV === 'test' ? MockPaymentService : StripeService,
|
||||
},
|
||||
],
|
||||
exports: [PAYMENT_GATEWAY],
|
||||
@@ -1410,9 +1400,7 @@ export class PaymentModule {}
|
||||
// Injection
|
||||
@Injectable()
|
||||
export class OrdersService {
|
||||
constructor(
|
||||
@Inject(PAYMENT_GATEWAY) private payment: PaymentGateway,
|
||||
) {}
|
||||
constructor(@Inject(PAYMENT_GATEWAY) private payment: PaymentGateway) {}
|
||||
|
||||
async createOrder(dto: CreateOrderDto) {
|
||||
await this.payment.charge(dto.amount);
|
||||
@@ -1654,7 +1642,7 @@ export class UsersController {
|
||||
export class EntityNotFoundException extends Error {
|
||||
constructor(
|
||||
public readonly entity: string,
|
||||
public readonly id: string,
|
||||
public readonly id: string
|
||||
) {
|
||||
super(`${entity} with ID "${id}" not found`);
|
||||
}
|
||||
@@ -1773,20 +1761,11 @@ export class AllExceptionsFilter implements ExceptionFilter {
|
||||
const response = ctx.getResponse<Response>();
|
||||
const request = ctx.getRequest<Request>();
|
||||
|
||||
const status =
|
||||
exception instanceof HttpException
|
||||
? exception.getStatus()
|
||||
: HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
|
||||
const message =
|
||||
exception instanceof HttpException
|
||||
? exception.message
|
||||
: 'Internal server error';
|
||||
const message = exception instanceof HttpException ? exception.message : 'Internal server error';
|
||||
|
||||
this.logger.error(
|
||||
`${request.method} ${request.url}`,
|
||||
exception instanceof Error ? exception.stack : exception,
|
||||
);
|
||||
this.logger.error(`${request.method} ${request.url}`, exception instanceof Error ? exception.stack : exception);
|
||||
|
||||
response.status(status).json({
|
||||
statusCode: status,
|
||||
@@ -1798,10 +1777,7 @@ export class AllExceptionsFilter implements ExceptionFilter {
|
||||
}
|
||||
|
||||
// Register globally in main.ts
|
||||
app.useGlobalFilters(
|
||||
new AllExceptionsFilter(app.get(Logger)),
|
||||
new DomainExceptionFilter(),
|
||||
);
|
||||
app.useGlobalFilters(new AllExceptionsFilter(app.get(Logger)), new DomainExceptionFilter());
|
||||
|
||||
// Or via module
|
||||
@Module({
|
||||
@@ -1931,7 +1907,7 @@ export class AuthService {
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(
|
||||
private config: ConfigService,
|
||||
private usersService: UsersService,
|
||||
private usersService: UsersService
|
||||
) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
@@ -2271,15 +2247,12 @@ export class AdminController {
|
||||
export class JwtAuthGuard implements CanActivate {
|
||||
constructor(
|
||||
private jwtService: JwtService,
|
||||
private reflector: Reflector,
|
||||
private reflector: Reflector
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
// Check for @Public() decorator
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [context.getHandler(), context.getClass()]);
|
||||
if (isPublic) return true;
|
||||
|
||||
const request = context.switchToHttp().getRequest();
|
||||
@@ -2309,10 +2282,7 @@ export class RolesGuard implements CanActivate {
|
||||
constructor(private reflector: Reflector) {}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const requiredRoles = this.reflector.getAllAndOverride<Role[]>('roles', [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
const requiredRoles = this.reflector.getAllAndOverride<Role[]>('roles', [context.getHandler(), context.getClass()]);
|
||||
|
||||
if (!requiredRoles) return true;
|
||||
|
||||
@@ -2387,9 +2357,9 @@ export class UsersController {
|
||||
|
||||
// DTOs without validation decorators
|
||||
export class CreateUserDto {
|
||||
name: string; // No validation
|
||||
email: string; // Could be "not-an-email"
|
||||
age: number; // Could be "abc" or -999
|
||||
name: string; // No validation
|
||||
email: string; // Could be "not-an-email"
|
||||
age: number; // Could be "abc" or -999
|
||||
}
|
||||
```
|
||||
|
||||
@@ -2402,13 +2372,13 @@ async function bootstrap() {
|
||||
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
whitelist: true, // Strip unknown properties
|
||||
forbidNonWhitelisted: true, // Throw on unknown properties
|
||||
transform: true, // Auto-transform to DTO types
|
||||
whitelist: true, // Strip unknown properties
|
||||
forbidNonWhitelisted: true, // Throw on unknown properties
|
||||
transform: true, // Auto-transform to DTO types
|
||||
transformOptions: {
|
||||
enableImplicitConversion: true,
|
||||
},
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
await app.listen(3000);
|
||||
@@ -2573,7 +2543,7 @@ export class DatabaseService implements OnModuleInit {
|
||||
export class CacheWarmerService implements OnApplicationBootstrap {
|
||||
constructor(
|
||||
private cache: CacheService,
|
||||
private products: ProductsService,
|
||||
private products: ProductsService
|
||||
) {}
|
||||
|
||||
async onApplicationBootstrap(): Promise<void> {
|
||||
@@ -2697,10 +2667,7 @@ export class ModuleLoaderService {
|
||||
|
||||
constructor(private lazyModuleLoader: LazyModuleLoader) {}
|
||||
|
||||
async load<T>(
|
||||
key: string,
|
||||
importFn: () => Promise<{ default: Type<T> } | Type<T>>,
|
||||
): Promise<ModuleRef> {
|
||||
async load<T>(key: string, importFn: () => Promise<{ default: Type<T> } | Type<T>>): Promise<ModuleRef> {
|
||||
if (!this.loadedModules.has(key)) {
|
||||
const module = await importFn();
|
||||
const moduleType = 'default' in module ? module.default : module;
|
||||
@@ -2915,9 +2882,7 @@ export class UsersService {
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: (config: ConfigService) => ({
|
||||
stores: [
|
||||
new KeyvRedis(config.get('REDIS_URL')),
|
||||
],
|
||||
stores: [new KeyvRedis(config.get('REDIS_URL'))],
|
||||
ttl: 60 * 1000, // Default 60s
|
||||
}),
|
||||
}),
|
||||
@@ -2930,7 +2895,7 @@ export class AppModule {}
|
||||
export class ProductsService {
|
||||
constructor(
|
||||
@Inject(CACHE_MANAGER) private cache: Cache,
|
||||
private productsRepo: ProductRepository,
|
||||
private productsRepo: ProductRepository
|
||||
) {}
|
||||
|
||||
async getPopular(): Promise<Product[]> {
|
||||
@@ -2981,10 +2946,7 @@ export class CacheInvalidationService {
|
||||
@OnEvent('product.updated')
|
||||
@OnEvent('product.deleted')
|
||||
async invalidateProductCaches(event: ProductEvent) {
|
||||
await Promise.all([
|
||||
this.cache.del('products:popular'),
|
||||
this.cache.del(`product:${event.productId}`),
|
||||
]);
|
||||
await Promise.all([this.cache.del('products:popular'), this.cache.del(`product:${event.productId}`)]);
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -3055,7 +3017,7 @@ describe('UsersController (e2e)', () => {
|
||||
whitelist: true,
|
||||
transform: true,
|
||||
forbidNonWhitelisted: true,
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
await app.init();
|
||||
@@ -3091,9 +3053,7 @@ describe('UsersController (e2e)', () => {
|
||||
|
||||
describe('/users/:id (GET)', () => {
|
||||
it('should return 404 for non-existent user', () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/users/non-existent-id')
|
||||
.expect(404);
|
||||
return request(app.getHttpServer()).get('/users/non-existent-id').expect(404);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3121,9 +3081,7 @@ describe('Protected Routes (e2e)', () => {
|
||||
});
|
||||
|
||||
it('should return 401 without token', () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/users/me')
|
||||
.expect(401);
|
||||
return request(app.getHttpServer()).get('/users/me').expect(401);
|
||||
});
|
||||
|
||||
it('should return user profile with valid token', () => {
|
||||
@@ -3254,9 +3212,7 @@ describe('WeatherService', () => {
|
||||
});
|
||||
|
||||
it('should handle API timeout', async () => {
|
||||
httpService.get.mockReturnValue(
|
||||
throwError(() => new Error('ETIMEDOUT')),
|
||||
);
|
||||
httpService.get.mockReturnValue(throwError(() => new Error('ETIMEDOUT')));
|
||||
|
||||
await expect(service.getWeather('NYC')).rejects.toThrow('Weather service unavailable');
|
||||
});
|
||||
@@ -3265,7 +3221,7 @@ describe('WeatherService', () => {
|
||||
httpService.get.mockReturnValue(
|
||||
throwError(() => ({
|
||||
response: { status: 429, data: { message: 'Rate limited' } },
|
||||
})),
|
||||
}))
|
||||
);
|
||||
|
||||
await expect(service.getWeather('NYC')).rejects.toThrow(TooManyRequestsException);
|
||||
@@ -3287,10 +3243,7 @@ describe('UsersService', () => {
|
||||
};
|
||||
|
||||
const module = await Test.createTestingModule({
|
||||
providers: [
|
||||
UsersService,
|
||||
{ provide: getRepositoryToken(User), useValue: mockRepo },
|
||||
],
|
||||
providers: [UsersService, { provide: getRepositoryToken(User), useValue: mockRepo }],
|
||||
}).compile();
|
||||
|
||||
service = module.get(UsersService);
|
||||
@@ -3433,9 +3386,7 @@ describe('UsersService', () => {
|
||||
it('should throw on duplicate email', async () => {
|
||||
repo.findOne.mockResolvedValue({ id: '1', email: 'test@test.com' });
|
||||
|
||||
await expect(
|
||||
service.create({ name: 'Test', email: 'test@test.com' }),
|
||||
).rejects.toThrow(ConflictException);
|
||||
await expect(service.create({ name: 'Test', email: 'test@test.com' })).rejects.toThrow(ConflictException);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3813,12 +3764,7 @@ export class OrdersService {
|
||||
|
||||
for (const item of items) {
|
||||
await manager.save(OrderItem, { orderId: order.id, ...item });
|
||||
await manager.decrement(
|
||||
Inventory,
|
||||
{ productId: item.productId },
|
||||
'stock',
|
||||
item.quantity,
|
||||
);
|
||||
await manager.decrement(Inventory, { productId: item.productId }, 'stock', item.quantity);
|
||||
}
|
||||
|
||||
// If this throws, everything rolls back
|
||||
@@ -3841,12 +3787,7 @@ export class TransferService {
|
||||
|
||||
try {
|
||||
// Debit source account
|
||||
await queryRunner.manager.decrement(
|
||||
Account,
|
||||
{ id: fromId },
|
||||
'balance',
|
||||
amount,
|
||||
);
|
||||
await queryRunner.manager.decrement(Account, { id: fromId }, 'balance', amount);
|
||||
|
||||
// Verify sufficient funds
|
||||
const source = await queryRunner.manager.findOne(Account, {
|
||||
@@ -3857,12 +3798,7 @@ export class TransferService {
|
||||
}
|
||||
|
||||
// Credit destination account
|
||||
await queryRunner.manager.increment(
|
||||
Account,
|
||||
{ id: toId },
|
||||
'balance',
|
||||
amount,
|
||||
);
|
||||
await queryRunner.manager.increment(Account, { id: toId }, 'balance', amount);
|
||||
|
||||
// Log the transaction
|
||||
await queryRunner.manager.save(TransactionLog, {
|
||||
@@ -3887,13 +3823,10 @@ export class TransferService {
|
||||
export class UsersRepository {
|
||||
constructor(
|
||||
@InjectRepository(User) private repo: Repository<User>,
|
||||
private dataSource: DataSource,
|
||||
private dataSource: DataSource
|
||||
) {}
|
||||
|
||||
async createWithProfile(
|
||||
userData: CreateUserDto,
|
||||
profileData: CreateProfileDto,
|
||||
): Promise<User> {
|
||||
async createWithProfile(userData: CreateUserDto, profileData: CreateProfileDto): Promise<User> {
|
||||
return this.dataSource.transaction(async (manager) => {
|
||||
const user = await manager.save(User, userData);
|
||||
await manager.save(Profile, { ...profileData, userId: user.id });
|
||||
@@ -4034,7 +3967,7 @@ export class UsersController {
|
||||
@SerializeOptions({ type: UserResponseDto })
|
||||
async findAll(): Promise<UserResponseDto[]> {
|
||||
const users = await this.usersService.findAll();
|
||||
return users.map(u => plainToInstance(UserResponseDto, u));
|
||||
return users.map((u) => plainToInstance(UserResponseDto, u));
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@@ -4650,10 +4583,7 @@ export class UsersService {
|
||||
@Controller('users')
|
||||
export class UsersController {
|
||||
@Get(':id')
|
||||
async findOne(
|
||||
@Param('id') id: string,
|
||||
@Headers('X-API-Version') version: string = '1',
|
||||
): Promise<any> {
|
||||
async findOne(@Param('id') id: string, @Headers('X-API-Version') version: string = '1'): Promise<any> {
|
||||
return this.usersService.findOne(id, version);
|
||||
}
|
||||
}
|
||||
@@ -5137,11 +5067,7 @@ import { BullModule } from '@nestjs/bullmq';
|
||||
},
|
||||
},
|
||||
}),
|
||||
BullModule.registerQueue(
|
||||
{ name: 'email' },
|
||||
{ name: 'reports' },
|
||||
{ name: 'notifications' },
|
||||
),
|
||||
BullModule.registerQueue({ name: 'email' }, { name: 'reports' }, { name: 'notifications' }),
|
||||
],
|
||||
})
|
||||
export class QueueModule {}
|
||||
@@ -5149,9 +5075,7 @@ export class QueueModule {}
|
||||
// Producer: Add jobs to queue
|
||||
@Injectable()
|
||||
export class ReportsService {
|
||||
constructor(
|
||||
@InjectQueue('reports') private reportsQueue: Queue,
|
||||
) {}
|
||||
constructor(@InjectQueue('reports') private reportsQueue: Queue) {}
|
||||
|
||||
async requestReport(dto: GenerateReportDto): Promise<{ jobId: string }> {
|
||||
// Return immediately, process in background
|
||||
@@ -5249,7 +5173,7 @@ export class NotificationService {
|
||||
{
|
||||
attempts: 5,
|
||||
backoff: { type: 'exponential', delay: 5000 },
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5267,7 +5191,7 @@ export class ScheduledJobsService implements OnModuleInit {
|
||||
{
|
||||
repeat: { cron: '0 0 * * *' },
|
||||
jobId: 'daily-cleanup', // Prevent duplicates
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// Send digest every hour
|
||||
@@ -5277,7 +5201,7 @@ export class ScheduledJobsService implements OnModuleInit {
|
||||
{
|
||||
repeat: { every: 60 * 60 * 1000 },
|
||||
jobId: 'hourly-digest',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5406,9 +5330,7 @@ export class DatabaseService implements OnApplicationShutdown {
|
||||
console.log(`Database service shutting down on ${signal}`);
|
||||
|
||||
// Close all connections gracefully
|
||||
await Promise.all(
|
||||
this.connections.map((conn) => conn.close()),
|
||||
);
|
||||
await Promise.all(this.connections.map((conn) => conn.close()));
|
||||
|
||||
console.log('All database connections closed');
|
||||
}
|
||||
@@ -5477,9 +5399,7 @@ export class HealthController {
|
||||
throw new ServiceUnavailableException('Shutting down');
|
||||
}
|
||||
|
||||
return this.health.check([
|
||||
() => this.db.pingCheck('database'),
|
||||
]);
|
||||
return this.health.check([() => this.db.pingCheck('database')]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5535,10 +5455,7 @@ export class RequestTracker implements NestMiddleware, OnApplicationShutdown {
|
||||
});
|
||||
|
||||
// Wait with timeout
|
||||
await Promise.race([
|
||||
this.shutdownPromise,
|
||||
new Promise((resolve) => setTimeout(resolve, 30000)),
|
||||
]);
|
||||
await Promise.race([this.shutdownPromise, new Promise((resolve) => setTimeout(resolve, 30000))]);
|
||||
}
|
||||
|
||||
console.log('All requests completed');
|
||||
@@ -5608,9 +5525,7 @@ export const appConfig = registerAs('app', () => ({
|
||||
|
||||
// config/validation.schema.ts
|
||||
export const validationSchema = Joi.object({
|
||||
NODE_ENV: Joi.string()
|
||||
.valid('development', 'production', 'test')
|
||||
.default('development'),
|
||||
NODE_ENV: Joi.string().valid('development', 'production', 'test').default('development'),
|
||||
PORT: Joi.number().default(3000),
|
||||
DB_HOST: Joi.string().required(),
|
||||
DB_PORT: Joi.number().default(5432),
|
||||
@@ -5684,7 +5599,7 @@ export class AppService {
|
||||
export class DatabaseService {
|
||||
constructor(
|
||||
@Inject(databaseConfig.KEY)
|
||||
private dbConfig: ConfigType<typeof databaseConfig>,
|
||||
private dbConfig: ConfigType<typeof databaseConfig>
|
||||
) {
|
||||
// Full type inference!
|
||||
const host = this.dbConfig.host; // string
|
||||
@@ -5694,12 +5609,7 @@ export class DatabaseService {
|
||||
|
||||
// Environment files support
|
||||
ConfigModule.forRoot({
|
||||
envFilePath: [
|
||||
`.env.${process.env.NODE_ENV}.local`,
|
||||
`.env.${process.env.NODE_ENV}`,
|
||||
'.env.local',
|
||||
'.env',
|
||||
],
|
||||
envFilePath: [`.env.${process.env.NODE_ENV}.local`, `.env.${process.env.NODE_ENV}`, '.env.local', '.env'],
|
||||
});
|
||||
|
||||
// .env.development
|
||||
@@ -5757,9 +5667,7 @@ logger.log('User ' + userId + ' created at ' + new Date());
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule, {
|
||||
logger:
|
||||
process.env.NODE_ENV === 'production'
|
||||
? ['error', 'warn', 'log']
|
||||
: ['error', 'warn', 'log', 'debug', 'verbose'],
|
||||
process.env.NODE_ENV === 'production' ? ['error', 'warn', 'log'] : ['error', 'warn', 'log', 'debug', 'verbose'],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5794,7 +5702,7 @@ export class JsonLogger implements LoggerService {
|
||||
timestamp: new Date().toISOString(),
|
||||
message,
|
||||
...context,
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5806,7 +5714,7 @@ export class JsonLogger implements LoggerService {
|
||||
message,
|
||||
trace,
|
||||
...context,
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5817,7 +5725,7 @@ export class JsonLogger implements LoggerService {
|
||||
timestamp: new Date().toISOString(),
|
||||
message,
|
||||
...context,
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5828,7 +5736,7 @@ export class JsonLogger implements LoggerService {
|
||||
timestamp: new Date().toISOString(),
|
||||
message,
|
||||
...context,
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5878,7 +5786,7 @@ export class ContextLogger {
|
||||
userId: this.cls.get('userId'),
|
||||
message,
|
||||
...data,
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5893,7 +5801,7 @@ export class ContextLogger {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
...data,
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5906,10 +5814,7 @@ import { LoggerModule } from 'nestjs-pino';
|
||||
LoggerModule.forRoot({
|
||||
pinoHttp: {
|
||||
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
|
||||
transport:
|
||||
process.env.NODE_ENV !== 'production'
|
||||
? { target: 'pino-pretty' }
|
||||
: undefined,
|
||||
transport: process.env.NODE_ENV !== 'production' ? { target: 'pino-pretty' } : undefined,
|
||||
redact: ['req.headers.authorization', 'req.body.password'],
|
||||
serializers: {
|
||||
req: (req) => ({
|
||||
@@ -5955,4 +5860,4 @@ Reference: [NestJS Logger](https://docs.nestjs.com/techniques/logger)
|
||||
|
||||
---
|
||||
|
||||
*Generated by build-agents.ts on 2026-01-16*
|
||||
_Generated by build-agents.ts on 2026-01-16_
|
||||
|
||||
@@ -36,11 +36,12 @@ npx skills add Kadajett/agent-nestjs-skills -a claude-code -a cursor
|
||||
- `area-description.md` - Individual rule files
|
||||
- `scripts/` - Build scripts and utilities
|
||||
- `metadata.json` - Document metadata (version, organization, abstract)
|
||||
- __`AGENTS.md`__ - Compiled output (generated)
|
||||
- **`AGENTS.md`** - Compiled output (generated)
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Install dependencies:
|
||||
|
||||
```bash
|
||||
cd scripts && npm install
|
||||
```
|
||||
@@ -74,7 +75,7 @@ npx skills add Kadajett/agent-nestjs-skills -a claude-code -a cursor
|
||||
|
||||
Each rule file should follow this structure:
|
||||
|
||||
```markdown
|
||||
````markdown
|
||||
---
|
||||
title: Rule Title Here
|
||||
impact: MEDIUM
|
||||
@@ -91,6 +92,7 @@ Brief explanation of the rule and why it matters.
|
||||
```typescript
|
||||
// Bad code example
|
||||
```
|
||||
````
|
||||
|
||||
**Correct (description of what's right):**
|
||||
|
||||
@@ -102,7 +104,6 @@ Optional explanatory text after examples.
|
||||
|
||||
Reference: [NestJS Documentation](https://docs.nestjs.com)
|
||||
|
||||
|
||||
## File Naming Convention
|
||||
|
||||
- Files starting with `_` are special (excluded from build)
|
||||
@@ -113,13 +114,13 @@ Reference: [NestJS Documentation](https://docs.nestjs.com)
|
||||
|
||||
## Impact Levels
|
||||
|
||||
| Level | Description |
|
||||
|-------|-------------|
|
||||
| CRITICAL | Violations cause runtime errors, security vulnerabilities, or architectural breakdown |
|
||||
| HIGH | Significant impact on reliability, security, or maintainability |
|
||||
| MEDIUM-HIGH | Notable impact on quality and developer experience |
|
||||
| MEDIUM | Moderate impact on code quality and best practices |
|
||||
| LOW-MEDIUM | Minor improvements for consistency and maintainability |
|
||||
| Level | Description |
|
||||
| ----------- | ------------------------------------------------------------------------------------- |
|
||||
| CRITICAL | Violations cause runtime errors, security vulnerabilities, or architectural breakdown |
|
||||
| HIGH | Significant impact on reliability, security, or maintainability |
|
||||
| MEDIUM-HIGH | Notable impact on quality and developer experience |
|
||||
| MEDIUM | Moderate impact on code quality and best practices |
|
||||
| LOW-MEDIUM | Minor improvements for consistency and maintainability |
|
||||
|
||||
## Scripts
|
||||
|
||||
@@ -160,4 +161,3 @@ These NestJS skills work with:
|
||||
|
||||
- [Claude Code](https://claude.ai/code) - Anthropic's official CLI
|
||||
- [AdaL](https://sylph.ai/adal) - Self-evolving AI coding agent with MCP support
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ description: NestJS best practices and architecture patterns for building produc
|
||||
license: MIT
|
||||
metadata:
|
||||
author: Kadajett
|
||||
version: "1.1.0"
|
||||
version: '1.1.0'
|
||||
---
|
||||
|
||||
# NestJS Best Practices
|
||||
@@ -24,18 +24,18 @@ Reference these guidelines when:
|
||||
|
||||
## Rule Categories by Priority
|
||||
|
||||
| Priority | Category | Impact | Prefix |
|
||||
|----------|----------|--------|--------|
|
||||
| 1 | Architecture | CRITICAL | `arch-` |
|
||||
| 2 | Dependency Injection | CRITICAL | `di-` |
|
||||
| 3 | Error Handling | HIGH | `error-` |
|
||||
| 4 | Security | HIGH | `security-` |
|
||||
| 5 | Performance | HIGH | `perf-` |
|
||||
| 6 | Testing | MEDIUM-HIGH | `test-` |
|
||||
| 7 | Database & ORM | MEDIUM-HIGH | `db-` |
|
||||
| 8 | API Design | MEDIUM | `api-` |
|
||||
| 9 | Microservices | MEDIUM | `micro-` |
|
||||
| 10 | DevOps & Deployment | LOW-MEDIUM | `devops-` |
|
||||
| Priority | Category | Impact | Prefix |
|
||||
| -------- | -------------------- | ----------- | ----------- |
|
||||
| 1 | Architecture | CRITICAL | `arch-` |
|
||||
| 2 | Dependency Injection | CRITICAL | `di-` |
|
||||
| 3 | Error Handling | HIGH | `error-` |
|
||||
| 4 | Security | HIGH | `security-` |
|
||||
| 5 | Performance | HIGH | `perf-` |
|
||||
| 6 | Testing | MEDIUM-HIGH | `test-` |
|
||||
| 7 | Database & ORM | MEDIUM-HIGH | `db-` |
|
||||
| 8 | API Design | MEDIUM | `api-` |
|
||||
| 9 | Microservices | MEDIUM | `micro-` |
|
||||
| 10 | DevOps & Deployment | LOW-MEDIUM | `devops-` |
|
||||
|
||||
## Quick Reference
|
||||
|
||||
@@ -120,6 +120,7 @@ rules/_sections.md
|
||||
```
|
||||
|
||||
Each rule file contains:
|
||||
|
||||
- Brief explanation of why it matters
|
||||
- Incorrect code example with explanation
|
||||
- Correct code example with explanation
|
||||
|
||||
@@ -126,7 +126,7 @@ export class UsersController {
|
||||
@SerializeOptions({ type: UserResponseDto })
|
||||
async findAll(): Promise<UserResponseDto[]> {
|
||||
const users = await this.usersService.findAll();
|
||||
return users.map(u => plainToInstance(UserResponseDto, u));
|
||||
return users.map((u) => plainToInstance(UserResponseDto, u));
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
|
||||
@@ -159,10 +159,7 @@ export class UsersService {
|
||||
@Controller('users')
|
||||
export class UsersController {
|
||||
@Get(':id')
|
||||
async findOne(
|
||||
@Param('id') id: string,
|
||||
@Headers('X-API-Version') version: string = '1',
|
||||
): Promise<any> {
|
||||
async findOne(@Param('id') id: string, @Headers('X-API-Version') version: string = '1'): Promise<any> {
|
||||
return this.usersService.findOne(id, version);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Avoid Circular Dependencies
|
||||
impact: CRITICAL
|
||||
impactDescription: "#1 cause of runtime crashes"
|
||||
impactDescription: '#1 cause of runtime crashes'
|
||||
tags: architecture, modules, dependencies
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Organize by Feature Modules
|
||||
impact: CRITICAL
|
||||
impactDescription: "3-5x faster onboarding and development"
|
||||
impactDescription: '3-5x faster onboarding and development'
|
||||
tags: architecture, modules, organization
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Single Responsibility for Services
|
||||
impact: CRITICAL
|
||||
impactDescription: "40%+ improvement in testability"
|
||||
impactDescription: '40%+ improvement in testability'
|
||||
tags: architecture, services, single-responsibility
|
||||
---
|
||||
|
||||
@@ -19,7 +19,7 @@ export class UserAndOrderService {
|
||||
private userRepo: UserRepository,
|
||||
private orderRepo: OrderRepository,
|
||||
private mailer: MailService,
|
||||
private payment: PaymentService,
|
||||
private payment: PaymentService
|
||||
) {}
|
||||
|
||||
async createUser(dto: CreateUserDto) {
|
||||
@@ -90,7 +90,7 @@ export class OrdersController {
|
||||
constructor(
|
||||
private orders: OrdersService,
|
||||
private payment: PaymentService,
|
||||
private notifications: NotificationService,
|
||||
private notifications: NotificationService
|
||||
) {}
|
||||
|
||||
@Post()
|
||||
|
||||
@@ -20,7 +20,7 @@ export class OrdersService {
|
||||
private emailService: EmailService,
|
||||
private analyticsService: AnalyticsService,
|
||||
private notificationService: NotificationService,
|
||||
private loyaltyService: LoyaltyService,
|
||||
private loyaltyService: LoyaltyService
|
||||
) {}
|
||||
|
||||
async createOrder(dto: CreateOrderDto): Promise<Order> {
|
||||
@@ -51,7 +51,7 @@ export class OrderCreatedEvent {
|
||||
public readonly orderId: string,
|
||||
public readonly userId: string,
|
||||
public readonly items: OrderItem[],
|
||||
public readonly total: number,
|
||||
public readonly total: number
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -60,17 +60,14 @@ export class OrderCreatedEvent {
|
||||
export class OrdersService {
|
||||
constructor(
|
||||
private eventEmitter: EventEmitter2,
|
||||
private repo: Repository<Order>,
|
||||
private repo: Repository<Order>
|
||||
) {}
|
||||
|
||||
async createOrder(dto: CreateOrderDto): Promise<Order> {
|
||||
const order = await this.repo.save(dto);
|
||||
|
||||
// Emit event - no knowledge of consumers
|
||||
this.eventEmitter.emit(
|
||||
'order.created',
|
||||
new OrderCreatedEvent(order.id, order.userId, order.items, order.total),
|
||||
);
|
||||
this.eventEmitter.emit('order.created', new OrderCreatedEvent(order.id, order.userId, order.items, order.total));
|
||||
|
||||
return order;
|
||||
}
|
||||
|
||||
@@ -15,9 +15,7 @@ Create custom repositories to encapsulate complex queries and database logic. Th
|
||||
// Complex queries in services
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(
|
||||
@InjectRepository(User) private repo: Repository<User>,
|
||||
) {}
|
||||
constructor(@InjectRepository(User) private repo: Repository<User>) {}
|
||||
|
||||
async findActiveWithOrders(minOrders: number): Promise<User[]> {
|
||||
// Complex query logic mixed with business logic
|
||||
@@ -42,9 +40,7 @@ export class UsersService {
|
||||
// Custom repository with encapsulated queries
|
||||
@Injectable()
|
||||
export class UsersRepository {
|
||||
constructor(
|
||||
@InjectRepository(User) private repo: Repository<User>,
|
||||
) {}
|
||||
constructor(@InjectRepository(User) private repo: Repository<User>) {}
|
||||
|
||||
async findById(id: string): Promise<User | null> {
|
||||
return this.repo.findOne({ where: { id } });
|
||||
|
||||
@@ -47,12 +47,7 @@ export class OrdersService {
|
||||
|
||||
for (const item of items) {
|
||||
await manager.save(OrderItem, { orderId: order.id, ...item });
|
||||
await manager.decrement(
|
||||
Inventory,
|
||||
{ productId: item.productId },
|
||||
'stock',
|
||||
item.quantity,
|
||||
);
|
||||
await manager.decrement(Inventory, { productId: item.productId }, 'stock', item.quantity);
|
||||
}
|
||||
|
||||
// If this throws, everything rolls back
|
||||
@@ -75,12 +70,7 @@ export class TransferService {
|
||||
|
||||
try {
|
||||
// Debit source account
|
||||
await queryRunner.manager.decrement(
|
||||
Account,
|
||||
{ id: fromId },
|
||||
'balance',
|
||||
amount,
|
||||
);
|
||||
await queryRunner.manager.decrement(Account, { id: fromId }, 'balance', amount);
|
||||
|
||||
// Verify sufficient funds
|
||||
const source = await queryRunner.manager.findOne(Account, {
|
||||
@@ -91,12 +81,7 @@ export class TransferService {
|
||||
}
|
||||
|
||||
// Credit destination account
|
||||
await queryRunner.manager.increment(
|
||||
Account,
|
||||
{ id: toId },
|
||||
'balance',
|
||||
amount,
|
||||
);
|
||||
await queryRunner.manager.increment(Account, { id: toId }, 'balance', amount);
|
||||
|
||||
// Log the transaction
|
||||
await queryRunner.manager.save(TransactionLog, {
|
||||
@@ -121,13 +106,10 @@ export class TransferService {
|
||||
export class UsersRepository {
|
||||
constructor(
|
||||
@InjectRepository(User) private repo: Repository<User>,
|
||||
private dataSource: DataSource,
|
||||
private dataSource: DataSource
|
||||
) {}
|
||||
|
||||
async createWithProfile(
|
||||
userData: CreateUserDto,
|
||||
profileData: CreateProfileDto,
|
||||
): Promise<User> {
|
||||
async createWithProfile(userData: CreateUserDto, profileData: CreateProfileDto): Promise<User> {
|
||||
return this.dataSource.transaction(async (manager) => {
|
||||
const user = await manager.save(User, userData);
|
||||
await manager.save(Profile, { ...profileData, userId: user.id });
|
||||
|
||||
@@ -79,9 +79,7 @@ export class DatabaseService implements OnApplicationShutdown {
|
||||
console.log(`Database service shutting down on ${signal}`);
|
||||
|
||||
// Close all connections gracefully
|
||||
await Promise.all(
|
||||
this.connections.map((conn) => conn.close()),
|
||||
);
|
||||
await Promise.all(this.connections.map((conn) => conn.close()));
|
||||
|
||||
console.log('All database connections closed');
|
||||
}
|
||||
@@ -150,9 +148,7 @@ export class HealthController {
|
||||
throw new ServiceUnavailableException('Shutting down');
|
||||
}
|
||||
|
||||
return this.health.check([
|
||||
() => this.db.pingCheck('database'),
|
||||
]);
|
||||
return this.health.check([() => this.db.pingCheck('database')]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,10 +204,7 @@ export class RequestTracker implements NestMiddleware, OnApplicationShutdown {
|
||||
});
|
||||
|
||||
// Wait with timeout
|
||||
await Promise.race([
|
||||
this.shutdownPromise,
|
||||
new Promise((resolve) => setTimeout(resolve, 30000)),
|
||||
]);
|
||||
await Promise.race([this.shutdownPromise, new Promise((resolve) => setTimeout(resolve, 30000))]);
|
||||
}
|
||||
|
||||
console.log('All requests completed');
|
||||
|
||||
@@ -61,9 +61,7 @@ export const appConfig = registerAs('app', () => ({
|
||||
|
||||
// config/validation.schema.ts
|
||||
export const validationSchema = Joi.object({
|
||||
NODE_ENV: Joi.string()
|
||||
.valid('development', 'production', 'test')
|
||||
.default('development'),
|
||||
NODE_ENV: Joi.string().valid('development', 'production', 'test').default('development'),
|
||||
PORT: Joi.number().default(3000),
|
||||
DB_HOST: Joi.string().required(),
|
||||
DB_PORT: Joi.number().default(5432),
|
||||
@@ -137,7 +135,7 @@ export class AppService {
|
||||
export class DatabaseService {
|
||||
constructor(
|
||||
@Inject(databaseConfig.KEY)
|
||||
private dbConfig: ConfigType<typeof databaseConfig>,
|
||||
private dbConfig: ConfigType<typeof databaseConfig>
|
||||
) {
|
||||
// Full type inference!
|
||||
const host = this.dbConfig.host; // string
|
||||
@@ -147,12 +145,7 @@ export class DatabaseService {
|
||||
|
||||
// Environment files support
|
||||
ConfigModule.forRoot({
|
||||
envFilePath: [
|
||||
`.env.${process.env.NODE_ENV}.local`,
|
||||
`.env.${process.env.NODE_ENV}`,
|
||||
'.env.local',
|
||||
'.env',
|
||||
],
|
||||
envFilePath: [`.env.${process.env.NODE_ENV}.local`, `.env.${process.env.NODE_ENV}`, '.env.local', '.env'],
|
||||
});
|
||||
|
||||
// .env.development
|
||||
|
||||
@@ -45,9 +45,7 @@ logger.log('User ' + userId + ' created at ' + new Date());
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule, {
|
||||
logger:
|
||||
process.env.NODE_ENV === 'production'
|
||||
? ['error', 'warn', 'log']
|
||||
: ['error', 'warn', 'log', 'debug', 'verbose'],
|
||||
process.env.NODE_ENV === 'production' ? ['error', 'warn', 'log'] : ['error', 'warn', 'log', 'debug', 'verbose'],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -82,7 +80,7 @@ export class JsonLogger implements LoggerService {
|
||||
timestamp: new Date().toISOString(),
|
||||
message,
|
||||
...context,
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -94,7 +92,7 @@ export class JsonLogger implements LoggerService {
|
||||
message,
|
||||
trace,
|
||||
...context,
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -105,7 +103,7 @@ export class JsonLogger implements LoggerService {
|
||||
timestamp: new Date().toISOString(),
|
||||
message,
|
||||
...context,
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -116,7 +114,7 @@ export class JsonLogger implements LoggerService {
|
||||
timestamp: new Date().toISOString(),
|
||||
message,
|
||||
...context,
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -166,7 +164,7 @@ export class ContextLogger {
|
||||
userId: this.cls.get('userId'),
|
||||
message,
|
||||
...data,
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -181,7 +179,7 @@ export class ContextLogger {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
...data,
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -194,10 +192,7 @@ import { LoggerModule } from 'nestjs-pino';
|
||||
LoggerModule.forRoot({
|
||||
pinoHttp: {
|
||||
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
|
||||
transport:
|
||||
process.env.NODE_ENV !== 'production'
|
||||
? { target: 'pino-pretty' }
|
||||
: undefined,
|
||||
transport: process.env.NODE_ENV !== 'production' ? { target: 'pino-pretty' } : undefined,
|
||||
redact: ['req.headers.authorization', 'req.body.password'],
|
||||
serializers: {
|
||||
req: (req) => ({
|
||||
|
||||
@@ -55,7 +55,7 @@ export class OrdersService {
|
||||
constructor(
|
||||
private usersService: UsersService,
|
||||
private inventoryService: InventoryService,
|
||||
private paymentService: PaymentService,
|
||||
private paymentService: PaymentService
|
||||
) {}
|
||||
|
||||
async createOrder(dto: CreateOrderDto): Promise<Order> {
|
||||
|
||||
@@ -28,14 +28,14 @@ interface NotificationService {
|
||||
@Injectable()
|
||||
export class OrdersService {
|
||||
constructor(
|
||||
private notifications: NotificationService, // Depends on 8 methods, uses 1
|
||||
private notifications: NotificationService // Depends on 8 methods, uses 1
|
||||
) {}
|
||||
|
||||
async confirmOrder(order: Order): Promise<void> {
|
||||
await this.notifications.sendEmail(
|
||||
order.customer.email,
|
||||
'Order Confirmed',
|
||||
`Your order ${order.id} has been confirmed.`,
|
||||
`Your order ${order.id} has been confirmed.`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -43,12 +43,12 @@ export class OrdersService {
|
||||
// Testing is painful - must mock unused methods
|
||||
const mockNotificationService = {
|
||||
sendEmail: jest.fn(),
|
||||
sendSms: jest.fn(), // Never used, but required
|
||||
sendPush: jest.fn(), // Never used, but required
|
||||
sendSlack: jest.fn(), // Never used, but required
|
||||
logNotification: jest.fn(), // Never used, but required
|
||||
sendSms: jest.fn(), // Never used, but required
|
||||
sendPush: jest.fn(), // Never used, but required
|
||||
sendSlack: jest.fn(), // Never used, but required
|
||||
logNotification: jest.fn(), // Never used, but required
|
||||
getDeliveryStatus: jest.fn(), // Never used, but required
|
||||
retryFailed: jest.fn(), // Never used, but required
|
||||
retryFailed: jest.fn(), // Never used, but required
|
||||
scheduleNotification: jest.fn(), // Never used, but required
|
||||
};
|
||||
```
|
||||
@@ -105,14 +105,14 @@ export class SendGridEmailService implements EmailSender {
|
||||
@Injectable()
|
||||
export class OrdersService {
|
||||
constructor(
|
||||
@Inject(EMAIL_SENDER) private emailSender: EmailSender, // Minimal dependency
|
||||
@Inject(EMAIL_SENDER) private emailSender: EmailSender // Minimal dependency
|
||||
) {}
|
||||
|
||||
async confirmOrder(order: Order): Promise<void> {
|
||||
await this.emailSender.sendEmail(
|
||||
order.customer.email,
|
||||
'Order Confirmed',
|
||||
`Your order ${order.id} has been confirmed.`,
|
||||
`Your order ${order.id} has been confirmed.`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -150,7 +150,7 @@ type MultiChannelSender = EmailSender & SmsSender & PushSender;
|
||||
export class AlertService {
|
||||
constructor(
|
||||
@Inject(MULTI_CHANNEL_SENDER)
|
||||
private sender: EmailSender & SmsSender,
|
||||
private sender: EmailSender & SmsSender
|
||||
) {}
|
||||
|
||||
async sendCriticalAlert(user: User, message: string): Promise<void> {
|
||||
|
||||
@@ -178,9 +178,7 @@ export class OrdersService {
|
||||
|
||||
```typescript
|
||||
// Shared test suite that any implementation must pass
|
||||
function testPaymentGatewayContract(
|
||||
createGateway: () => PaymentGateway,
|
||||
) {
|
||||
function testPaymentGatewayContract(createGateway: () => PaymentGateway) {
|
||||
describe('PaymentGateway contract', () => {
|
||||
let gateway: PaymentGateway;
|
||||
|
||||
@@ -197,13 +195,11 @@ function testPaymentGatewayContract(
|
||||
});
|
||||
|
||||
it('throws InvalidCurrencyException for unsupported currency', async () => {
|
||||
await expect(gateway.charge(1000, 'INVALID'))
|
||||
.rejects.toThrow(InvalidCurrencyException);
|
||||
await expect(gateway.charge(1000, 'INVALID')).rejects.toThrow(InvalidCurrencyException);
|
||||
});
|
||||
|
||||
it('throws TransactionNotFoundException for invalid refund', async () => {
|
||||
await expect(gateway.refund('nonexistent'))
|
||||
.rejects.toThrow(TransactionNotFoundException);
|
||||
await expect(gateway.refund('nonexistent')).rejects.toThrow(TransactionNotFoundException);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ export class UsersService {
|
||||
export class UsersService {
|
||||
constructor(
|
||||
private readonly userRepo: UserRepository,
|
||||
@Inject('CONFIG') private readonly config: ConfigType,
|
||||
@Inject('CONFIG') private readonly config: ConfigType
|
||||
) {}
|
||||
|
||||
async findAll(): Promise<User[]> {
|
||||
|
||||
@@ -19,7 +19,9 @@ interface PaymentGateway {
|
||||
|
||||
@Injectable()
|
||||
export class StripeService implements PaymentGateway {
|
||||
charge(amount: number) { /* ... */ }
|
||||
charge(amount: number) {
|
||||
/* ... */
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@@ -58,9 +60,7 @@ export class MockPaymentService implements PaymentGateway {
|
||||
providers: [
|
||||
{
|
||||
provide: PAYMENT_GATEWAY,
|
||||
useClass: process.env.NODE_ENV === 'test'
|
||||
? MockPaymentService
|
||||
: StripeService,
|
||||
useClass: process.env.NODE_ENV === 'test' ? MockPaymentService : StripeService,
|
||||
},
|
||||
],
|
||||
exports: [PAYMENT_GATEWAY],
|
||||
@@ -70,9 +70,7 @@ export class PaymentModule {}
|
||||
// Injection
|
||||
@Injectable()
|
||||
export class OrdersService {
|
||||
constructor(
|
||||
@Inject(PAYMENT_GATEWAY) private payment: PaymentGateway,
|
||||
) {}
|
||||
constructor(@Inject(PAYMENT_GATEWAY) private payment: PaymentGateway) {}
|
||||
|
||||
async createOrder(dto: CreateOrderDto) {
|
||||
await this.payment.charge(dto.amount);
|
||||
|
||||
@@ -88,7 +88,7 @@ export class UsersController {
|
||||
export class EntityNotFoundException extends Error {
|
||||
constructor(
|
||||
public readonly entity: string,
|
||||
public readonly id: string,
|
||||
public readonly id: string
|
||||
) {
|
||||
super(`${entity} with ID "${id}" not found`);
|
||||
}
|
||||
|
||||
@@ -95,20 +95,11 @@ export class AllExceptionsFilter implements ExceptionFilter {
|
||||
const response = ctx.getResponse<Response>();
|
||||
const request = ctx.getRequest<Request>();
|
||||
|
||||
const status =
|
||||
exception instanceof HttpException
|
||||
? exception.getStatus()
|
||||
: HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
|
||||
const message =
|
||||
exception instanceof HttpException
|
||||
? exception.message
|
||||
: 'Internal server error';
|
||||
const message = exception instanceof HttpException ? exception.message : 'Internal server error';
|
||||
|
||||
this.logger.error(
|
||||
`${request.method} ${request.url}`,
|
||||
exception instanceof Error ? exception.stack : exception,
|
||||
);
|
||||
this.logger.error(`${request.method} ${request.url}`, exception instanceof Error ? exception.stack : exception);
|
||||
|
||||
response.status(status).json({
|
||||
statusCode: status,
|
||||
@@ -120,10 +111,7 @@ export class AllExceptionsFilter implements ExceptionFilter {
|
||||
}
|
||||
|
||||
// Register globally in main.ts
|
||||
app.useGlobalFilters(
|
||||
new AllExceptionsFilter(app.get(Logger)),
|
||||
new DomainExceptionFilter(),
|
||||
);
|
||||
app.useGlobalFilters(new AllExceptionsFilter(app.get(Logger)), new DomainExceptionFilter());
|
||||
|
||||
// Or via module
|
||||
@Module({
|
||||
|
||||
@@ -64,11 +64,7 @@ import { BullModule } from '@nestjs/bullmq';
|
||||
},
|
||||
},
|
||||
}),
|
||||
BullModule.registerQueue(
|
||||
{ name: 'email' },
|
||||
{ name: 'reports' },
|
||||
{ name: 'notifications' },
|
||||
),
|
||||
BullModule.registerQueue({ name: 'email' }, { name: 'reports' }, { name: 'notifications' }),
|
||||
],
|
||||
})
|
||||
export class QueueModule {}
|
||||
@@ -76,9 +72,7 @@ export class QueueModule {}
|
||||
// Producer: Add jobs to queue
|
||||
@Injectable()
|
||||
export class ReportsService {
|
||||
constructor(
|
||||
@InjectQueue('reports') private reportsQueue: Queue,
|
||||
) {}
|
||||
constructor(@InjectQueue('reports') private reportsQueue: Queue) {}
|
||||
|
||||
async requestReport(dto: GenerateReportDto): Promise<{ jobId: string }> {
|
||||
// Return immediately, process in background
|
||||
@@ -176,7 +170,7 @@ export class NotificationService {
|
||||
{
|
||||
attempts: 5,
|
||||
backoff: { type: 'exponential', delay: 5000 },
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -194,7 +188,7 @@ export class ScheduledJobsService implements OnModuleInit {
|
||||
{
|
||||
repeat: { cron: '0 0 * * *' },
|
||||
jobId: 'daily-cleanup', // Prevent duplicates
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// Send digest every hour
|
||||
@@ -204,7 +198,7 @@ export class ScheduledJobsService implements OnModuleInit {
|
||||
{
|
||||
repeat: { every: 60 * 60 * 1000 },
|
||||
jobId: 'hourly-digest',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ export class DatabaseService implements OnModuleInit {
|
||||
export class CacheWarmerService implements OnApplicationBootstrap {
|
||||
constructor(
|
||||
private cache: CacheService,
|
||||
private products: ProductsService,
|
||||
private products: ProductsService
|
||||
) {}
|
||||
|
||||
async onApplicationBootstrap(): Promise<void> {
|
||||
|
||||
@@ -81,10 +81,7 @@ export class ModuleLoaderService {
|
||||
|
||||
constructor(private lazyModuleLoader: LazyModuleLoader) {}
|
||||
|
||||
async load<T>(
|
||||
key: string,
|
||||
importFn: () => Promise<{ default: Type<T> } | Type<T>>,
|
||||
): Promise<ModuleRef> {
|
||||
async load<T>(key: string, importFn: () => Promise<{ default: Type<T> } | Type<T>>): Promise<ModuleRef> {
|
||||
if (!this.loadedModules.has(key)) {
|
||||
const module = await importFn();
|
||||
const moduleType = 'default' in module ? module.default : module;
|
||||
|
||||
@@ -51,9 +51,7 @@ export class UsersService {
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: (config: ConfigService) => ({
|
||||
stores: [
|
||||
new KeyvRedis(config.get('REDIS_URL')),
|
||||
],
|
||||
stores: [new KeyvRedis(config.get('REDIS_URL'))],
|
||||
ttl: 60 * 1000, // Default 60s
|
||||
}),
|
||||
}),
|
||||
@@ -66,7 +64,7 @@ export class AppModule {}
|
||||
export class ProductsService {
|
||||
constructor(
|
||||
@Inject(CACHE_MANAGER) private cache: Cache,
|
||||
private productsRepo: ProductRepository,
|
||||
private productsRepo: ProductRepository
|
||||
) {}
|
||||
|
||||
async getPopular(): Promise<Product[]> {
|
||||
@@ -117,10 +115,7 @@ export class CacheInvalidationService {
|
||||
@OnEvent('product.updated')
|
||||
@OnEvent('product.deleted')
|
||||
async invalidateProductCaches(event: ProductEvent) {
|
||||
await Promise.all([
|
||||
this.cache.del('products:popular'),
|
||||
this.cache.del(`product:${event.productId}`),
|
||||
]);
|
||||
await Promise.all([this.cache.del('products:popular'), this.cache.del(`product:${event.productId}`)]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -111,7 +111,7 @@ export class AuthService {
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(
|
||||
private config: ConfigService,
|
||||
private usersService: UsersService,
|
||||
private usersService: UsersService
|
||||
) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
|
||||
@@ -47,15 +47,12 @@ export class AdminController {
|
||||
export class JwtAuthGuard implements CanActivate {
|
||||
constructor(
|
||||
private jwtService: JwtService,
|
||||
private reflector: Reflector,
|
||||
private reflector: Reflector
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
// Check for @Public() decorator
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [context.getHandler(), context.getClass()]);
|
||||
if (isPublic) return true;
|
||||
|
||||
const request = context.switchToHttp().getRequest();
|
||||
@@ -85,10 +82,7 @@ export class RolesGuard implements CanActivate {
|
||||
constructor(private reflector: Reflector) {}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const requiredRoles = this.reflector.getAllAndOverride<Role[]>('roles', [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
const requiredRoles = this.reflector.getAllAndOverride<Role[]>('roles', [context.getHandler(), context.getClass()]);
|
||||
|
||||
if (!requiredRoles) return true;
|
||||
|
||||
|
||||
@@ -30,9 +30,9 @@ export class UsersController {
|
||||
|
||||
// DTOs without validation decorators
|
||||
export class CreateUserDto {
|
||||
name: string; // No validation
|
||||
email: string; // Could be "not-an-email"
|
||||
age: number; // Could be "abc" or -999
|
||||
name: string; // No validation
|
||||
email: string; // Could be "not-an-email"
|
||||
age: number; // Could be "abc" or -999
|
||||
}
|
||||
```
|
||||
|
||||
@@ -45,13 +45,13 @@ async function bootstrap() {
|
||||
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
whitelist: true, // Strip unknown properties
|
||||
forbidNonWhitelisted: true, // Throw on unknown properties
|
||||
transform: true, // Auto-transform to DTO types
|
||||
whitelist: true, // Strip unknown properties
|
||||
forbidNonWhitelisted: true, // Throw on unknown properties
|
||||
transform: true, // Auto-transform to DTO types
|
||||
transformOptions: {
|
||||
enableImplicitConversion: true,
|
||||
},
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
await app.listen(3000);
|
||||
|
||||
@@ -61,7 +61,7 @@ describe('UsersController (e2e)', () => {
|
||||
whitelist: true,
|
||||
transform: true,
|
||||
forbidNonWhitelisted: true,
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
await app.init();
|
||||
@@ -97,9 +97,7 @@ describe('UsersController (e2e)', () => {
|
||||
|
||||
describe('/users/:id (GET)', () => {
|
||||
it('should return 404 for non-existent user', () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/users/non-existent-id')
|
||||
.expect(404);
|
||||
return request(app.getHttpServer()).get('/users/non-existent-id').expect(404);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -127,9 +125,7 @@ describe('Protected Routes (e2e)', () => {
|
||||
});
|
||||
|
||||
it('should return 401 without token', () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/users/me')
|
||||
.expect(401);
|
||||
return request(app.getHttpServer()).get('/users/me').expect(401);
|
||||
});
|
||||
|
||||
it('should return user profile with valid token', () => {
|
||||
|
||||
@@ -84,9 +84,7 @@ describe('WeatherService', () => {
|
||||
});
|
||||
|
||||
it('should handle API timeout', async () => {
|
||||
httpService.get.mockReturnValue(
|
||||
throwError(() => new Error('ETIMEDOUT')),
|
||||
);
|
||||
httpService.get.mockReturnValue(throwError(() => new Error('ETIMEDOUT')));
|
||||
|
||||
await expect(service.getWeather('NYC')).rejects.toThrow('Weather service unavailable');
|
||||
});
|
||||
@@ -95,7 +93,7 @@ describe('WeatherService', () => {
|
||||
httpService.get.mockReturnValue(
|
||||
throwError(() => ({
|
||||
response: { status: 429, data: { message: 'Rate limited' } },
|
||||
})),
|
||||
}))
|
||||
);
|
||||
|
||||
await expect(service.getWeather('NYC')).rejects.toThrow(TooManyRequestsException);
|
||||
@@ -117,10 +115,7 @@ describe('UsersService', () => {
|
||||
};
|
||||
|
||||
const module = await Test.createTestingModule({
|
||||
providers: [
|
||||
UsersService,
|
||||
{ provide: getRepositoryToken(User), useValue: mockRepo },
|
||||
],
|
||||
providers: [UsersService, { provide: getRepositoryToken(User), useValue: mockRepo }],
|
||||
}).compile();
|
||||
|
||||
service = module.get(UsersService);
|
||||
|
||||
@@ -86,9 +86,7 @@ describe('UsersService', () => {
|
||||
it('should throw on duplicate email', async () => {
|
||||
repo.findOne.mockResolvedValue({ id: '1', email: 'test@test.com' });
|
||||
|
||||
await expect(
|
||||
service.create({ name: 'Test', email: 'test@test.com' }),
|
||||
).rejects.toThrow(ConflictException);
|
||||
await expect(service.create({ name: 'Test', email: 'test@test.com' })).rejects.toThrow(ConflictException);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ function parseFrontmatter(content: string): { frontmatter: RuleFrontmatter | nul
|
||||
|
||||
return {
|
||||
frontmatter: frontmatter as RuleFrontmatter,
|
||||
body: body.trim()
|
||||
body: body.trim(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -118,8 +118,7 @@ function readMetadata(): any {
|
||||
|
||||
function readRules(): Rule[] {
|
||||
const rulesDir = path.join(__dirname, '..', 'rules');
|
||||
const files = fs.readdirSync(rulesDir)
|
||||
.filter(f => f.endsWith('.md') && !f.startsWith('_'));
|
||||
const files = fs.readdirSync(rulesDir).filter((f) => f.endsWith('.md') && !f.startsWith('_'));
|
||||
|
||||
const rules: Rule[] = [];
|
||||
|
||||
@@ -144,7 +143,7 @@ function readRules(): Rule[] {
|
||||
frontmatter,
|
||||
content: body,
|
||||
category: category.name,
|
||||
categorySection: category.section
|
||||
categorySection: category.section,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user