260322:1648 Correct Coresspondence / Doing RFA / Correct CI
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user