260322:1648 Correct Coresspondence / Doing RFA / Correct CI
CI Pipeline / build (push) Failing after 12m41s
Build and Deploy / deploy (push) Failing after 2m44s

This commit is contained in:
admin
2026-03-22 16:48:12 +07:00
parent e5deedb42e
commit 11984bfa29
683 changed files with 105251 additions and 29068 deletions
+25 -25
View File
@@ -258,37 +258,37 @@ If you change your mind mid-project:
### 📊 Current Status: UAT Ready (2026-03-11) ### 📊 Current Status: UAT Ready (2026-03-11)
| Area | Status | | Area | Status |
|------|--------| | ------------- | ------------------------------------- |
| Backend | ✅ 18 Modules, Production Ready | | Backend | ✅ 18 Modules, Production Ready |
| Frontend | ✅ 100% Complete | | Frontend | ✅ 100% Complete |
| Database | ✅ Schema v1.8.0 Stable | | Database | ✅ Schema v1.8.0 Stable |
| Documentation | ✅ **10/10 Gaps Closed** | | Documentation | ✅ **10/10 Gaps Closed** |
| AI Migration | 🔄 Pre-migration Setup (n8n + Ollama) | | AI Migration | 🔄 Pre-migration Setup (n8n + Ollama) |
| UAT | 🔄 In Progress | | UAT | 🔄 In Progress |
| Deployment | 📋 Pending Go-Live | | Deployment | 📋 Pending Go-Live |
### 📁 Key Spec Files (Always Check Before Writing Code) ### 📁 Key Spec Files (Always Check Before Writing Code)
| เอกสาร | Path | ใช้เมื่อ | | เอกสาร | Path | ใช้เมื่อ |
|--------|------|--------| | --------------- | ---------------------------------------------------------------- | ------------------- |
| Schema Tables | `specs/03-Data-and-Storage/lcbp3-v1.8.0-schema-02-tables.sql` | ก่อนเขียน Query | | Schema Tables | `specs/03-Data-and-Storage/lcbp3-v1.8.0-schema-02-tables.sql` | ก่อนเขียน Query |
| Data Dictionary | `specs/03-Data-and-Storage/03-01-data-dictionary.md` | ตรวจ Business Rules | | Data Dictionary | `specs/03-Data-and-Storage/03-01-data-dictionary.md` | ตรวจ Business Rules |
| Edge Cases | `specs/01-Requirements/01-06-edge-cases-and-rules.md` | 37 Rules | | Edge Cases | `specs/01-Requirements/01-06-edge-cases-and-rules.md` | 37 Rules |
| Migration Scope | `specs/03-Data-and-Storage/03-06-migration-business-scope.md` | Migration Bot | | Migration Scope | `specs/03-Data-and-Storage/03-06-migration-business-scope.md` | Migration Bot |
| Release Policy | `specs/04-Infrastructure-OPS/04-08-release-management-policy.md` | ก่อน Deploy | | Release Policy | `specs/04-Infrastructure-OPS/04-08-release-management-policy.md` | ก่อน Deploy |
| UAT Criteria | `specs/01-Requirements/01-05-acceptance-criteria.md` | ตรวจ Feature | | UAT Criteria | `specs/01-Requirements/01-05-acceptance-criteria.md` | ตรวจ Feature |
### ⚡ Project-Specific Workflow Cheatsheet ### ⚡ Project-Specific Workflow Cheatsheet
| Task | Workflow / Command | Notes | | Task | Workflow / Command | Notes |
|------|--------------------|-------| | --------------------- | ------------------------- | --------------------------------- |
| Create Backend Module | `/create-backend-module` | Scaffolds NestJS module | | Create Backend Module | `/create-backend-module` | Scaffolds NestJS module |
| Create Frontend Page | `/create-frontend-page` | Next.js App Router page | | Create Frontend Page | `/create-frontend-page` | Next.js App Router page |
| Schema Change | `/schema-change` | ADR-009: No migrations | | Schema Change | `/schema-change` | ADR-009: No migrations |
| Deploy | `/deploy` | Blue-Green via Gitea CI/CD | | Deploy | `/deploy` | Blue-Green via Gitea CI/CD |
| UAT Feature Check | `/11-speckit-validate` | vs `01-05-acceptance-criteria.md` | | UAT Feature Check | `/11-speckit-validate` | vs `01-05-acceptance-criteria.md` |
| Security Audit | `@speckit-security-audit` | OWASP + CASL + ClamAV | | Security Audit | `@speckit-security-audit` | OWASP + CASL + ClamAV |
### 🚫 Critical Forbidden Actions ### 🚫 Critical Forbidden Actions
+87 -182
View File
@@ -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.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) - 9.3 [Use Message Queues for Background Jobs](#93-use-message-queues-for-background-jobs)
10. [DevOps & Deployment](#10-devops-deployment) — **LOW-MEDIUM** 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.1 [Implement Graceful Shutdown](#101-implement-graceful-shutdown)
- 10.3 [Use Structured Logging](#103-use-structured-logging) - 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 userRepo: UserRepository,
private orderRepo: OrderRepository, private orderRepo: OrderRepository,
private mailer: MailService, private mailer: MailService,
private payment: PaymentService, private payment: PaymentService
) {} ) {}
async createUser(dto: CreateUserDto) { async createUser(dto: CreateUserDto) {
@@ -461,7 +462,7 @@ export class OrdersController {
constructor( constructor(
private orders: OrdersService, private orders: OrdersService,
private payment: PaymentService, private payment: PaymentService,
private notifications: NotificationService, private notifications: NotificationService
) {} ) {}
@Post() @Post()
@@ -495,7 +496,7 @@ export class OrdersService {
private emailService: EmailService, private emailService: EmailService,
private analyticsService: AnalyticsService, private analyticsService: AnalyticsService,
private notificationService: NotificationService, private notificationService: NotificationService,
private loyaltyService: LoyaltyService, private loyaltyService: LoyaltyService
) {} ) {}
async createOrder(dto: CreateOrderDto): Promise<Order> { async createOrder(dto: CreateOrderDto): Promise<Order> {
@@ -526,7 +527,7 @@ export class OrderCreatedEvent {
public readonly orderId: string, public readonly orderId: string,
public readonly userId: string, public readonly userId: string,
public readonly items: OrderItem[], public readonly items: OrderItem[],
public readonly total: number, public readonly total: number
) {} ) {}
} }
@@ -535,17 +536,14 @@ export class OrderCreatedEvent {
export class OrdersService { export class OrdersService {
constructor( constructor(
private eventEmitter: EventEmitter2, private eventEmitter: EventEmitter2,
private repo: Repository<Order>, private repo: Repository<Order>
) {} ) {}
async createOrder(dto: CreateOrderDto): Promise<Order> { async createOrder(dto: CreateOrderDto): Promise<Order> {
const order = await this.repo.save(dto); const order = await this.repo.save(dto);
// Emit event - no knowledge of consumers // Emit event - no knowledge of consumers
this.eventEmitter.emit( this.eventEmitter.emit('order.created', new OrderCreatedEvent(order.id, order.userId, order.items, order.total));
'order.created',
new OrderCreatedEvent(order.id, order.userId, order.items, order.total),
);
return order; return order;
} }
@@ -596,9 +594,7 @@ Create custom repositories to encapsulate complex queries and database logic. Th
// Complex queries in services // Complex queries in services
@Injectable() @Injectable()
export class UsersService { export class UsersService {
constructor( constructor(@InjectRepository(User) private repo: Repository<User>) {}
@InjectRepository(User) private repo: Repository<User>,
) {}
async findActiveWithOrders(minOrders: number): Promise<User[]> { async findActiveWithOrders(minOrders: number): Promise<User[]> {
// Complex query logic mixed with business logic // Complex query logic mixed with business logic
@@ -623,9 +619,7 @@ export class UsersService {
// Custom repository with encapsulated queries // Custom repository with encapsulated queries
@Injectable() @Injectable()
export class UsersRepository { export class UsersRepository {
constructor( constructor(@InjectRepository(User) private repo: Repository<User>) {}
@InjectRepository(User) private repo: Repository<User>,
) {}
async findById(id: string): Promise<User | null> { async findById(id: string): Promise<User | null> {
return this.repo.findOne({ where: { id } }); return this.repo.findOne({ where: { id } });
@@ -735,7 +729,7 @@ export class OrdersService {
constructor( constructor(
private usersService: UsersService, private usersService: UsersService,
private inventoryService: InventoryService, private inventoryService: InventoryService,
private paymentService: PaymentService, private paymentService: PaymentService
) {} ) {}
async createOrder(dto: CreateOrderDto): Promise<Order> { async createOrder(dto: CreateOrderDto): Promise<Order> {
@@ -810,14 +804,14 @@ interface NotificationService {
@Injectable() @Injectable()
export class OrdersService { export class OrdersService {
constructor( 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> { async confirmOrder(order: Order): Promise<void> {
await this.notifications.sendEmail( await this.notifications.sendEmail(
order.customer.email, order.customer.email,
'Order Confirmed', '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 // Testing is painful - must mock unused methods
const mockNotificationService = { const mockNotificationService = {
sendEmail: jest.fn(), sendEmail: jest.fn(),
sendSms: jest.fn(), // Never used, but required sendSms: jest.fn(), // Never used, but required
sendPush: jest.fn(), // Never used, but required sendPush: jest.fn(), // Never used, but required
sendSlack: jest.fn(), // Never used, but required sendSlack: jest.fn(), // Never used, but required
logNotification: jest.fn(), // Never used, but required logNotification: jest.fn(), // Never used, but required
getDeliveryStatus: 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 scheduleNotification: jest.fn(), // Never used, but required
}; };
``` ```
@@ -887,14 +881,14 @@ export class SendGridEmailService implements EmailSender {
@Injectable() @Injectable()
export class OrdersService { export class OrdersService {
constructor( constructor(
@Inject(EMAIL_SENDER) private emailSender: EmailSender, // Minimal dependency @Inject(EMAIL_SENDER) private emailSender: EmailSender // Minimal dependency
) {} ) {}
async confirmOrder(order: Order): Promise<void> { async confirmOrder(order: Order): Promise<void> {
await this.emailSender.sendEmail( await this.emailSender.sendEmail(
order.customer.email, order.customer.email,
'Order Confirmed', '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 { export class AlertService {
constructor( constructor(
@Inject(MULTI_CHANNEL_SENDER) @Inject(MULTI_CHANNEL_SENDER)
private sender: EmailSender & SmsSender, private sender: EmailSender & SmsSender
) {} ) {}
async sendCriticalAlert(user: User, message: string): Promise<void> { async sendCriticalAlert(user: User, message: string): Promise<void> {
@@ -1123,9 +1117,7 @@ export class OrdersService {
```typescript ```typescript
// Shared test suite that any implementation must pass // Shared test suite that any implementation must pass
function testPaymentGatewayContract( function testPaymentGatewayContract(createGateway: () => PaymentGateway) {
createGateway: () => PaymentGateway,
) {
describe('PaymentGateway contract', () => { describe('PaymentGateway contract', () => {
let gateway: PaymentGateway; let gateway: PaymentGateway;
@@ -1142,13 +1134,11 @@ function testPaymentGatewayContract(
}); });
it('throws InvalidCurrencyException for unsupported currency', async () => { it('throws InvalidCurrencyException for unsupported currency', async () => {
await expect(gateway.charge(1000, 'INVALID')) await expect(gateway.charge(1000, 'INVALID')).rejects.toThrow(InvalidCurrencyException);
.rejects.toThrow(InvalidCurrencyException);
}); });
it('throws TransactionNotFoundException for invalid refund', async () => { it('throws TransactionNotFoundException for invalid refund', async () => {
await expect(gateway.refund('nonexistent')) await expect(gateway.refund('nonexistent')).rejects.toThrow(TransactionNotFoundException);
.rejects.toThrow(TransactionNotFoundException);
}); });
}); });
} }
@@ -1204,7 +1194,7 @@ export class UsersService {
export class UsersService { export class UsersService {
constructor( constructor(
private readonly userRepo: UserRepository, private readonly userRepo: UserRepository,
@Inject('CONFIG') private readonly config: ConfigType, @Inject('CONFIG') private readonly config: ConfigType
) {} ) {}
async findAll(): Promise<User[]> { async findAll(): Promise<User[]> {
@@ -1359,7 +1349,9 @@ interface PaymentGateway {
@Injectable() @Injectable()
export class StripeService implements PaymentGateway { export class StripeService implements PaymentGateway {
charge(amount: number) { /* ... */ } charge(amount: number) {
/* ... */
}
} }
@Injectable() @Injectable()
@@ -1398,9 +1390,7 @@ export class MockPaymentService implements PaymentGateway {
providers: [ providers: [
{ {
provide: PAYMENT_GATEWAY, provide: PAYMENT_GATEWAY,
useClass: process.env.NODE_ENV === 'test' useClass: process.env.NODE_ENV === 'test' ? MockPaymentService : StripeService,
? MockPaymentService
: StripeService,
}, },
], ],
exports: [PAYMENT_GATEWAY], exports: [PAYMENT_GATEWAY],
@@ -1410,9 +1400,7 @@ export class PaymentModule {}
// Injection // Injection
@Injectable() @Injectable()
export class OrdersService { export class OrdersService {
constructor( constructor(@Inject(PAYMENT_GATEWAY) private payment: PaymentGateway) {}
@Inject(PAYMENT_GATEWAY) private payment: PaymentGateway,
) {}
async createOrder(dto: CreateOrderDto) { async createOrder(dto: CreateOrderDto) {
await this.payment.charge(dto.amount); await this.payment.charge(dto.amount);
@@ -1654,7 +1642,7 @@ export class UsersController {
export class EntityNotFoundException extends Error { export class EntityNotFoundException extends Error {
constructor( constructor(
public readonly entity: string, public readonly entity: string,
public readonly id: string, public readonly id: string
) { ) {
super(`${entity} with ID "${id}" not found`); super(`${entity} with ID "${id}" not found`);
} }
@@ -1773,20 +1761,11 @@ export class AllExceptionsFilter implements ExceptionFilter {
const response = ctx.getResponse<Response>(); const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>(); const request = ctx.getRequest<Request>();
const status = const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR;
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const message = const message = exception instanceof HttpException ? exception.message : 'Internal server error';
exception instanceof HttpException
? exception.message
: 'Internal server error';
this.logger.error( this.logger.error(`${request.method} ${request.url}`, exception instanceof Error ? exception.stack : exception);
`${request.method} ${request.url}`,
exception instanceof Error ? exception.stack : exception,
);
response.status(status).json({ response.status(status).json({
statusCode: status, statusCode: status,
@@ -1798,10 +1777,7 @@ export class AllExceptionsFilter implements ExceptionFilter {
} }
// Register globally in main.ts // Register globally in main.ts
app.useGlobalFilters( app.useGlobalFilters(new AllExceptionsFilter(app.get(Logger)), new DomainExceptionFilter());
new AllExceptionsFilter(app.get(Logger)),
new DomainExceptionFilter(),
);
// Or via module // Or via module
@Module({ @Module({
@@ -1931,7 +1907,7 @@ export class AuthService {
export class JwtStrategy extends PassportStrategy(Strategy) { export class JwtStrategy extends PassportStrategy(Strategy) {
constructor( constructor(
private config: ConfigService, private config: ConfigService,
private usersService: UsersService, private usersService: UsersService
) { ) {
super({ super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
@@ -2271,15 +2247,12 @@ export class AdminController {
export class JwtAuthGuard implements CanActivate { export class JwtAuthGuard implements CanActivate {
constructor( constructor(
private jwtService: JwtService, private jwtService: JwtService,
private reflector: Reflector, private reflector: Reflector
) {} ) {}
async canActivate(context: ExecutionContext): Promise<boolean> { async canActivate(context: ExecutionContext): Promise<boolean> {
// Check for @Public() decorator // Check for @Public() decorator
const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [ const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [context.getHandler(), context.getClass()]);
context.getHandler(),
context.getClass(),
]);
if (isPublic) return true; if (isPublic) return true;
const request = context.switchToHttp().getRequest(); const request = context.switchToHttp().getRequest();
@@ -2309,10 +2282,7 @@ export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {} constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean { canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<Role[]>('roles', [ const requiredRoles = this.reflector.getAllAndOverride<Role[]>('roles', [context.getHandler(), context.getClass()]);
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) return true; if (!requiredRoles) return true;
@@ -2387,9 +2357,9 @@ export class UsersController {
// DTOs without validation decorators // DTOs without validation decorators
export class CreateUserDto { export class CreateUserDto {
name: string; // No validation name: string; // No validation
email: string; // Could be "not-an-email" email: string; // Could be "not-an-email"
age: number; // Could be "abc" or -999 age: number; // Could be "abc" or -999
} }
``` ```
@@ -2402,13 +2372,13 @@ async function bootstrap() {
app.useGlobalPipes( app.useGlobalPipes(
new ValidationPipe({ new ValidationPipe({
whitelist: true, // Strip unknown properties whitelist: true, // Strip unknown properties
forbidNonWhitelisted: true, // Throw on unknown properties forbidNonWhitelisted: true, // Throw on unknown properties
transform: true, // Auto-transform to DTO types transform: true, // Auto-transform to DTO types
transformOptions: { transformOptions: {
enableImplicitConversion: true, enableImplicitConversion: true,
}, },
}), })
); );
await app.listen(3000); await app.listen(3000);
@@ -2573,7 +2543,7 @@ export class DatabaseService implements OnModuleInit {
export class CacheWarmerService implements OnApplicationBootstrap { export class CacheWarmerService implements OnApplicationBootstrap {
constructor( constructor(
private cache: CacheService, private cache: CacheService,
private products: ProductsService, private products: ProductsService
) {} ) {}
async onApplicationBootstrap(): Promise<void> { async onApplicationBootstrap(): Promise<void> {
@@ -2697,10 +2667,7 @@ export class ModuleLoaderService {
constructor(private lazyModuleLoader: LazyModuleLoader) {} constructor(private lazyModuleLoader: LazyModuleLoader) {}
async load<T>( async load<T>(key: string, importFn: () => Promise<{ default: Type<T> } | Type<T>>): Promise<ModuleRef> {
key: string,
importFn: () => Promise<{ default: Type<T> } | Type<T>>,
): Promise<ModuleRef> {
if (!this.loadedModules.has(key)) { if (!this.loadedModules.has(key)) {
const module = await importFn(); const module = await importFn();
const moduleType = 'default' in module ? module.default : module; const moduleType = 'default' in module ? module.default : module;
@@ -2915,9 +2882,7 @@ export class UsersService {
imports: [ConfigModule], imports: [ConfigModule],
inject: [ConfigService], inject: [ConfigService],
useFactory: (config: ConfigService) => ({ useFactory: (config: ConfigService) => ({
stores: [ stores: [new KeyvRedis(config.get('REDIS_URL'))],
new KeyvRedis(config.get('REDIS_URL')),
],
ttl: 60 * 1000, // Default 60s ttl: 60 * 1000, // Default 60s
}), }),
}), }),
@@ -2930,7 +2895,7 @@ export class AppModule {}
export class ProductsService { export class ProductsService {
constructor( constructor(
@Inject(CACHE_MANAGER) private cache: Cache, @Inject(CACHE_MANAGER) private cache: Cache,
private productsRepo: ProductRepository, private productsRepo: ProductRepository
) {} ) {}
async getPopular(): Promise<Product[]> { async getPopular(): Promise<Product[]> {
@@ -2981,10 +2946,7 @@ export class CacheInvalidationService {
@OnEvent('product.updated') @OnEvent('product.updated')
@OnEvent('product.deleted') @OnEvent('product.deleted')
async invalidateProductCaches(event: ProductEvent) { async invalidateProductCaches(event: ProductEvent) {
await Promise.all([ await Promise.all([this.cache.del('products:popular'), this.cache.del(`product:${event.productId}`)]);
this.cache.del('products:popular'),
this.cache.del(`product:${event.productId}`),
]);
} }
} }
``` ```
@@ -3055,7 +3017,7 @@ describe('UsersController (e2e)', () => {
whitelist: true, whitelist: true,
transform: true, transform: true,
forbidNonWhitelisted: true, forbidNonWhitelisted: true,
}), })
); );
await app.init(); await app.init();
@@ -3091,9 +3053,7 @@ describe('UsersController (e2e)', () => {
describe('/users/:id (GET)', () => { describe('/users/:id (GET)', () => {
it('should return 404 for non-existent user', () => { it('should return 404 for non-existent user', () => {
return request(app.getHttpServer()) return request(app.getHttpServer()).get('/users/non-existent-id').expect(404);
.get('/users/non-existent-id')
.expect(404);
}); });
}); });
}); });
@@ -3121,9 +3081,7 @@ describe('Protected Routes (e2e)', () => {
}); });
it('should return 401 without token', () => { it('should return 401 without token', () => {
return request(app.getHttpServer()) return request(app.getHttpServer()).get('/users/me').expect(401);
.get('/users/me')
.expect(401);
}); });
it('should return user profile with valid token', () => { it('should return user profile with valid token', () => {
@@ -3254,9 +3212,7 @@ describe('WeatherService', () => {
}); });
it('should handle API timeout', async () => { it('should handle API timeout', async () => {
httpService.get.mockReturnValue( httpService.get.mockReturnValue(throwError(() => new Error('ETIMEDOUT')));
throwError(() => new Error('ETIMEDOUT')),
);
await expect(service.getWeather('NYC')).rejects.toThrow('Weather service unavailable'); await expect(service.getWeather('NYC')).rejects.toThrow('Weather service unavailable');
}); });
@@ -3265,7 +3221,7 @@ describe('WeatherService', () => {
httpService.get.mockReturnValue( httpService.get.mockReturnValue(
throwError(() => ({ throwError(() => ({
response: { status: 429, data: { message: 'Rate limited' } }, response: { status: 429, data: { message: 'Rate limited' } },
})), }))
); );
await expect(service.getWeather('NYC')).rejects.toThrow(TooManyRequestsException); await expect(service.getWeather('NYC')).rejects.toThrow(TooManyRequestsException);
@@ -3287,10 +3243,7 @@ describe('UsersService', () => {
}; };
const module = await Test.createTestingModule({ const module = await Test.createTestingModule({
providers: [ providers: [UsersService, { provide: getRepositoryToken(User), useValue: mockRepo }],
UsersService,
{ provide: getRepositoryToken(User), useValue: mockRepo },
],
}).compile(); }).compile();
service = module.get(UsersService); service = module.get(UsersService);
@@ -3433,9 +3386,7 @@ describe('UsersService', () => {
it('should throw on duplicate email', async () => { it('should throw on duplicate email', async () => {
repo.findOne.mockResolvedValue({ id: '1', email: 'test@test.com' }); repo.findOne.mockResolvedValue({ id: '1', email: 'test@test.com' });
await expect( await expect(service.create({ name: 'Test', email: 'test@test.com' })).rejects.toThrow(ConflictException);
service.create({ name: 'Test', email: 'test@test.com' }),
).rejects.toThrow(ConflictException);
}); });
}); });
@@ -3813,12 +3764,7 @@ export class OrdersService {
for (const item of items) { for (const item of items) {
await manager.save(OrderItem, { orderId: order.id, ...item }); await manager.save(OrderItem, { orderId: order.id, ...item });
await manager.decrement( await manager.decrement(Inventory, { productId: item.productId }, 'stock', item.quantity);
Inventory,
{ productId: item.productId },
'stock',
item.quantity,
);
} }
// If this throws, everything rolls back // If this throws, everything rolls back
@@ -3841,12 +3787,7 @@ export class TransferService {
try { try {
// Debit source account // Debit source account
await queryRunner.manager.decrement( await queryRunner.manager.decrement(Account, { id: fromId }, 'balance', amount);
Account,
{ id: fromId },
'balance',
amount,
);
// Verify sufficient funds // Verify sufficient funds
const source = await queryRunner.manager.findOne(Account, { const source = await queryRunner.manager.findOne(Account, {
@@ -3857,12 +3798,7 @@ export class TransferService {
} }
// Credit destination account // Credit destination account
await queryRunner.manager.increment( await queryRunner.manager.increment(Account, { id: toId }, 'balance', amount);
Account,
{ id: toId },
'balance',
amount,
);
// Log the transaction // Log the transaction
await queryRunner.manager.save(TransactionLog, { await queryRunner.manager.save(TransactionLog, {
@@ -3887,13 +3823,10 @@ export class TransferService {
export class UsersRepository { export class UsersRepository {
constructor( constructor(
@InjectRepository(User) private repo: Repository<User>, @InjectRepository(User) private repo: Repository<User>,
private dataSource: DataSource, private dataSource: DataSource
) {} ) {}
async createWithProfile( async createWithProfile(userData: CreateUserDto, profileData: CreateProfileDto): Promise<User> {
userData: CreateUserDto,
profileData: CreateProfileDto,
): Promise<User> {
return this.dataSource.transaction(async (manager) => { return this.dataSource.transaction(async (manager) => {
const user = await manager.save(User, userData); const user = await manager.save(User, userData);
await manager.save(Profile, { ...profileData, userId: user.id }); await manager.save(Profile, { ...profileData, userId: user.id });
@@ -4034,7 +3967,7 @@ export class UsersController {
@SerializeOptions({ type: UserResponseDto }) @SerializeOptions({ type: UserResponseDto })
async findAll(): Promise<UserResponseDto[]> { async findAll(): Promise<UserResponseDto[]> {
const users = await this.usersService.findAll(); const users = await this.usersService.findAll();
return users.map(u => plainToInstance(UserResponseDto, u)); return users.map((u) => plainToInstance(UserResponseDto, u));
} }
@Get(':id') @Get(':id')
@@ -4650,10 +4583,7 @@ export class UsersService {
@Controller('users') @Controller('users')
export class UsersController { export class UsersController {
@Get(':id') @Get(':id')
async findOne( async findOne(@Param('id') id: string, @Headers('X-API-Version') version: string = '1'): Promise<any> {
@Param('id') id: string,
@Headers('X-API-Version') version: string = '1',
): Promise<any> {
return this.usersService.findOne(id, version); return this.usersService.findOne(id, version);
} }
} }
@@ -5137,11 +5067,7 @@ import { BullModule } from '@nestjs/bullmq';
}, },
}, },
}), }),
BullModule.registerQueue( BullModule.registerQueue({ name: 'email' }, { name: 'reports' }, { name: 'notifications' }),
{ name: 'email' },
{ name: 'reports' },
{ name: 'notifications' },
),
], ],
}) })
export class QueueModule {} export class QueueModule {}
@@ -5149,9 +5075,7 @@ export class QueueModule {}
// Producer: Add jobs to queue // Producer: Add jobs to queue
@Injectable() @Injectable()
export class ReportsService { export class ReportsService {
constructor( constructor(@InjectQueue('reports') private reportsQueue: Queue) {}
@InjectQueue('reports') private reportsQueue: Queue,
) {}
async requestReport(dto: GenerateReportDto): Promise<{ jobId: string }> { async requestReport(dto: GenerateReportDto): Promise<{ jobId: string }> {
// Return immediately, process in background // Return immediately, process in background
@@ -5249,7 +5173,7 @@ export class NotificationService {
{ {
attempts: 5, attempts: 5,
backoff: { type: 'exponential', delay: 5000 }, backoff: { type: 'exponential', delay: 5000 },
}, }
); );
} }
} }
@@ -5267,7 +5191,7 @@ export class ScheduledJobsService implements OnModuleInit {
{ {
repeat: { cron: '0 0 * * *' }, repeat: { cron: '0 0 * * *' },
jobId: 'daily-cleanup', // Prevent duplicates jobId: 'daily-cleanup', // Prevent duplicates
}, }
); );
// Send digest every hour // Send digest every hour
@@ -5277,7 +5201,7 @@ export class ScheduledJobsService implements OnModuleInit {
{ {
repeat: { every: 60 * 60 * 1000 }, repeat: { every: 60 * 60 * 1000 },
jobId: 'hourly-digest', jobId: 'hourly-digest',
}, }
); );
} }
} }
@@ -5406,9 +5330,7 @@ export class DatabaseService implements OnApplicationShutdown {
console.log(`Database service shutting down on ${signal}`); console.log(`Database service shutting down on ${signal}`);
// Close all connections gracefully // Close all connections gracefully
await Promise.all( await Promise.all(this.connections.map((conn) => conn.close()));
this.connections.map((conn) => conn.close()),
);
console.log('All database connections closed'); console.log('All database connections closed');
} }
@@ -5477,9 +5399,7 @@ export class HealthController {
throw new ServiceUnavailableException('Shutting down'); throw new ServiceUnavailableException('Shutting down');
} }
return this.health.check([ return this.health.check([() => this.db.pingCheck('database')]);
() => this.db.pingCheck('database'),
]);
} }
} }
@@ -5535,10 +5455,7 @@ export class RequestTracker implements NestMiddleware, OnApplicationShutdown {
}); });
// Wait with timeout // Wait with timeout
await Promise.race([ await Promise.race([this.shutdownPromise, new Promise((resolve) => setTimeout(resolve, 30000))]);
this.shutdownPromise,
new Promise((resolve) => setTimeout(resolve, 30000)),
]);
} }
console.log('All requests completed'); console.log('All requests completed');
@@ -5608,9 +5525,7 @@ export const appConfig = registerAs('app', () => ({
// config/validation.schema.ts // config/validation.schema.ts
export const validationSchema = Joi.object({ export const validationSchema = Joi.object({
NODE_ENV: Joi.string() NODE_ENV: Joi.string().valid('development', 'production', 'test').default('development'),
.valid('development', 'production', 'test')
.default('development'),
PORT: Joi.number().default(3000), PORT: Joi.number().default(3000),
DB_HOST: Joi.string().required(), DB_HOST: Joi.string().required(),
DB_PORT: Joi.number().default(5432), DB_PORT: Joi.number().default(5432),
@@ -5684,7 +5599,7 @@ export class AppService {
export class DatabaseService { export class DatabaseService {
constructor( constructor(
@Inject(databaseConfig.KEY) @Inject(databaseConfig.KEY)
private dbConfig: ConfigType<typeof databaseConfig>, private dbConfig: ConfigType<typeof databaseConfig>
) { ) {
// Full type inference! // Full type inference!
const host = this.dbConfig.host; // string const host = this.dbConfig.host; // string
@@ -5694,12 +5609,7 @@ export class DatabaseService {
// Environment files support // Environment files support
ConfigModule.forRoot({ ConfigModule.forRoot({
envFilePath: [ envFilePath: [`.env.${process.env.NODE_ENV}.local`, `.env.${process.env.NODE_ENV}`, '.env.local', '.env'],
`.env.${process.env.NODE_ENV}.local`,
`.env.${process.env.NODE_ENV}`,
'.env.local',
'.env',
],
}); });
// .env.development // .env.development
@@ -5757,9 +5667,7 @@ logger.log('User ' + userId + ' created at ' + new Date());
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create(AppModule, { const app = await NestFactory.create(AppModule, {
logger: logger:
process.env.NODE_ENV === 'production' process.env.NODE_ENV === 'production' ? ['error', 'warn', 'log'] : ['error', 'warn', 'log', 'debug', 'verbose'],
? ['error', 'warn', 'log']
: ['error', 'warn', 'log', 'debug', 'verbose'],
}); });
} }
@@ -5794,7 +5702,7 @@ export class JsonLogger implements LoggerService {
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
message, message,
...context, ...context,
}), })
); );
} }
@@ -5806,7 +5714,7 @@ export class JsonLogger implements LoggerService {
message, message,
trace, trace,
...context, ...context,
}), })
); );
} }
@@ -5817,7 +5725,7 @@ export class JsonLogger implements LoggerService {
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
message, message,
...context, ...context,
}), })
); );
} }
@@ -5828,7 +5736,7 @@ export class JsonLogger implements LoggerService {
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
message, message,
...context, ...context,
}), })
); );
} }
} }
@@ -5878,7 +5786,7 @@ export class ContextLogger {
userId: this.cls.get('userId'), userId: this.cls.get('userId'),
message, message,
...data, ...data,
}), })
); );
} }
@@ -5893,7 +5801,7 @@ export class ContextLogger {
error: error.message, error: error.message,
stack: error.stack, stack: error.stack,
...data, ...data,
}), })
); );
} }
} }
@@ -5906,10 +5814,7 @@ import { LoggerModule } from 'nestjs-pino';
LoggerModule.forRoot({ LoggerModule.forRoot({
pinoHttp: { pinoHttp: {
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug', level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
transport: transport: process.env.NODE_ENV !== 'production' ? { target: 'pino-pretty' } : undefined,
process.env.NODE_ENV !== 'production'
? { target: 'pino-pretty' }
: undefined,
redact: ['req.headers.authorization', 'req.body.password'], redact: ['req.headers.authorization', 'req.body.password'],
serializers: { serializers: {
req: (req) => ({ 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_
+11 -11
View File
@@ -36,11 +36,12 @@ npx skills add Kadajett/agent-nestjs-skills -a claude-code -a cursor
- `area-description.md` - Individual rule files - `area-description.md` - Individual rule files
- `scripts/` - Build scripts and utilities - `scripts/` - Build scripts and utilities
- `metadata.json` - Document metadata (version, organization, abstract) - `metadata.json` - Document metadata (version, organization, abstract)
- __`AGENTS.md`__ - Compiled output (generated) - **`AGENTS.md`** - Compiled output (generated)
## Getting Started ## Getting Started
1. Install dependencies: 1. Install dependencies:
```bash ```bash
cd scripts && npm install 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: Each rule file should follow this structure:
```markdown ````markdown
--- ---
title: Rule Title Here title: Rule Title Here
impact: MEDIUM impact: MEDIUM
@@ -91,6 +92,7 @@ Brief explanation of the rule and why it matters.
```typescript ```typescript
// Bad code example // Bad code example
``` ```
````
**Correct (description of what's right):** **Correct (description of what's right):**
@@ -102,7 +104,6 @@ Optional explanatory text after examples.
Reference: [NestJS Documentation](https://docs.nestjs.com) Reference: [NestJS Documentation](https://docs.nestjs.com)
## File Naming Convention ## File Naming Convention
- Files starting with `_` are special (excluded from build) - Files starting with `_` are special (excluded from build)
@@ -113,13 +114,13 @@ Reference: [NestJS Documentation](https://docs.nestjs.com)
## Impact Levels ## Impact Levels
| Level | Description | | Level | Description |
|-------|-------------| | ----------- | ------------------------------------------------------------------------------------- |
| CRITICAL | Violations cause runtime errors, security vulnerabilities, or architectural breakdown | | CRITICAL | Violations cause runtime errors, security vulnerabilities, or architectural breakdown |
| HIGH | Significant impact on reliability, security, or maintainability | | HIGH | Significant impact on reliability, security, or maintainability |
| MEDIUM-HIGH | Notable impact on quality and developer experience | | MEDIUM-HIGH | Notable impact on quality and developer experience |
| MEDIUM | Moderate impact on code quality and best practices | | MEDIUM | Moderate impact on code quality and best practices |
| LOW-MEDIUM | Minor improvements for consistency and maintainability | | LOW-MEDIUM | Minor improvements for consistency and maintainability |
## Scripts ## Scripts
@@ -160,4 +161,3 @@ These NestJS skills work with:
- [Claude Code](https://claude.ai/code) - Anthropic's official CLI - [Claude Code](https://claude.ai/code) - Anthropic's official CLI
- [AdaL](https://sylph.ai/adal) - Self-evolving AI coding agent with MCP support - [AdaL](https://sylph.ai/adal) - Self-evolving AI coding agent with MCP support
+14 -13
View File
@@ -4,7 +4,7 @@ description: NestJS best practices and architecture patterns for building produc
license: MIT license: MIT
metadata: metadata:
author: Kadajett author: Kadajett
version: "1.1.0" version: '1.1.0'
--- ---
# NestJS Best Practices # NestJS Best Practices
@@ -24,18 +24,18 @@ Reference these guidelines when:
## Rule Categories by Priority ## Rule Categories by Priority
| Priority | Category | Impact | Prefix | | Priority | Category | Impact | Prefix |
|----------|----------|--------|--------| | -------- | -------------------- | ----------- | ----------- |
| 1 | Architecture | CRITICAL | `arch-` | | 1 | Architecture | CRITICAL | `arch-` |
| 2 | Dependency Injection | CRITICAL | `di-` | | 2 | Dependency Injection | CRITICAL | `di-` |
| 3 | Error Handling | HIGH | `error-` | | 3 | Error Handling | HIGH | `error-` |
| 4 | Security | HIGH | `security-` | | 4 | Security | HIGH | `security-` |
| 5 | Performance | HIGH | `perf-` | | 5 | Performance | HIGH | `perf-` |
| 6 | Testing | MEDIUM-HIGH | `test-` | | 6 | Testing | MEDIUM-HIGH | `test-` |
| 7 | Database & ORM | MEDIUM-HIGH | `db-` | | 7 | Database & ORM | MEDIUM-HIGH | `db-` |
| 8 | API Design | MEDIUM | `api-` | | 8 | API Design | MEDIUM | `api-` |
| 9 | Microservices | MEDIUM | `micro-` | | 9 | Microservices | MEDIUM | `micro-` |
| 10 | DevOps & Deployment | LOW-MEDIUM | `devops-` | | 10 | DevOps & Deployment | LOW-MEDIUM | `devops-` |
## Quick Reference ## Quick Reference
@@ -120,6 +120,7 @@ rules/_sections.md
``` ```
Each rule file contains: Each rule file contains:
- Brief explanation of why it matters - Brief explanation of why it matters
- Incorrect code example with explanation - Incorrect code example with explanation
- Correct code example with explanation - Correct code example with explanation
@@ -126,7 +126,7 @@ export class UsersController {
@SerializeOptions({ type: UserResponseDto }) @SerializeOptions({ type: UserResponseDto })
async findAll(): Promise<UserResponseDto[]> { async findAll(): Promise<UserResponseDto[]> {
const users = await this.usersService.findAll(); const users = await this.usersService.findAll();
return users.map(u => plainToInstance(UserResponseDto, u)); return users.map((u) => plainToInstance(UserResponseDto, u));
} }
@Get(':id') @Get(':id')
@@ -159,10 +159,7 @@ export class UsersService {
@Controller('users') @Controller('users')
export class UsersController { export class UsersController {
@Get(':id') @Get(':id')
async findOne( async findOne(@Param('id') id: string, @Headers('X-API-Version') version: string = '1'): Promise<any> {
@Param('id') id: string,
@Headers('X-API-Version') version: string = '1',
): Promise<any> {
return this.usersService.findOne(id, version); return this.usersService.findOne(id, version);
} }
} }
@@ -1,7 +1,7 @@
--- ---
title: Avoid Circular Dependencies title: Avoid Circular Dependencies
impact: CRITICAL impact: CRITICAL
impactDescription: "#1 cause of runtime crashes" impactDescription: '#1 cause of runtime crashes'
tags: architecture, modules, dependencies tags: architecture, modules, dependencies
--- ---
@@ -1,7 +1,7 @@
--- ---
title: Organize by Feature Modules title: Organize by Feature Modules
impact: CRITICAL impact: CRITICAL
impactDescription: "3-5x faster onboarding and development" impactDescription: '3-5x faster onboarding and development'
tags: architecture, modules, organization tags: architecture, modules, organization
--- ---
@@ -1,7 +1,7 @@
--- ---
title: Single Responsibility for Services title: Single Responsibility for Services
impact: CRITICAL impact: CRITICAL
impactDescription: "40%+ improvement in testability" impactDescription: '40%+ improvement in testability'
tags: architecture, services, single-responsibility tags: architecture, services, single-responsibility
--- ---
@@ -19,7 +19,7 @@ export class UserAndOrderService {
private userRepo: UserRepository, private userRepo: UserRepository,
private orderRepo: OrderRepository, private orderRepo: OrderRepository,
private mailer: MailService, private mailer: MailService,
private payment: PaymentService, private payment: PaymentService
) {} ) {}
async createUser(dto: CreateUserDto) { async createUser(dto: CreateUserDto) {
@@ -90,7 +90,7 @@ export class OrdersController {
constructor( constructor(
private orders: OrdersService, private orders: OrdersService,
private payment: PaymentService, private payment: PaymentService,
private notifications: NotificationService, private notifications: NotificationService
) {} ) {}
@Post() @Post()
@@ -20,7 +20,7 @@ export class OrdersService {
private emailService: EmailService, private emailService: EmailService,
private analyticsService: AnalyticsService, private analyticsService: AnalyticsService,
private notificationService: NotificationService, private notificationService: NotificationService,
private loyaltyService: LoyaltyService, private loyaltyService: LoyaltyService
) {} ) {}
async createOrder(dto: CreateOrderDto): Promise<Order> { async createOrder(dto: CreateOrderDto): Promise<Order> {
@@ -51,7 +51,7 @@ export class OrderCreatedEvent {
public readonly orderId: string, public readonly orderId: string,
public readonly userId: string, public readonly userId: string,
public readonly items: OrderItem[], public readonly items: OrderItem[],
public readonly total: number, public readonly total: number
) {} ) {}
} }
@@ -60,17 +60,14 @@ export class OrderCreatedEvent {
export class OrdersService { export class OrdersService {
constructor( constructor(
private eventEmitter: EventEmitter2, private eventEmitter: EventEmitter2,
private repo: Repository<Order>, private repo: Repository<Order>
) {} ) {}
async createOrder(dto: CreateOrderDto): Promise<Order> { async createOrder(dto: CreateOrderDto): Promise<Order> {
const order = await this.repo.save(dto); const order = await this.repo.save(dto);
// Emit event - no knowledge of consumers // Emit event - no knowledge of consumers
this.eventEmitter.emit( this.eventEmitter.emit('order.created', new OrderCreatedEvent(order.id, order.userId, order.items, order.total));
'order.created',
new OrderCreatedEvent(order.id, order.userId, order.items, order.total),
);
return order; return order;
} }
@@ -15,9 +15,7 @@ Create custom repositories to encapsulate complex queries and database logic. Th
// Complex queries in services // Complex queries in services
@Injectable() @Injectable()
export class UsersService { export class UsersService {
constructor( constructor(@InjectRepository(User) private repo: Repository<User>) {}
@InjectRepository(User) private repo: Repository<User>,
) {}
async findActiveWithOrders(minOrders: number): Promise<User[]> { async findActiveWithOrders(minOrders: number): Promise<User[]> {
// Complex query logic mixed with business logic // Complex query logic mixed with business logic
@@ -42,9 +40,7 @@ export class UsersService {
// Custom repository with encapsulated queries // Custom repository with encapsulated queries
@Injectable() @Injectable()
export class UsersRepository { export class UsersRepository {
constructor( constructor(@InjectRepository(User) private repo: Repository<User>) {}
@InjectRepository(User) private repo: Repository<User>,
) {}
async findById(id: string): Promise<User | null> { async findById(id: string): Promise<User | null> {
return this.repo.findOne({ where: { id } }); return this.repo.findOne({ where: { id } });
@@ -47,12 +47,7 @@ export class OrdersService {
for (const item of items) { for (const item of items) {
await manager.save(OrderItem, { orderId: order.id, ...item }); await manager.save(OrderItem, { orderId: order.id, ...item });
await manager.decrement( await manager.decrement(Inventory, { productId: item.productId }, 'stock', item.quantity);
Inventory,
{ productId: item.productId },
'stock',
item.quantity,
);
} }
// If this throws, everything rolls back // If this throws, everything rolls back
@@ -75,12 +70,7 @@ export class TransferService {
try { try {
// Debit source account // Debit source account
await queryRunner.manager.decrement( await queryRunner.manager.decrement(Account, { id: fromId }, 'balance', amount);
Account,
{ id: fromId },
'balance',
amount,
);
// Verify sufficient funds // Verify sufficient funds
const source = await queryRunner.manager.findOne(Account, { const source = await queryRunner.manager.findOne(Account, {
@@ -91,12 +81,7 @@ export class TransferService {
} }
// Credit destination account // Credit destination account
await queryRunner.manager.increment( await queryRunner.manager.increment(Account, { id: toId }, 'balance', amount);
Account,
{ id: toId },
'balance',
amount,
);
// Log the transaction // Log the transaction
await queryRunner.manager.save(TransactionLog, { await queryRunner.manager.save(TransactionLog, {
@@ -121,13 +106,10 @@ export class TransferService {
export class UsersRepository { export class UsersRepository {
constructor( constructor(
@InjectRepository(User) private repo: Repository<User>, @InjectRepository(User) private repo: Repository<User>,
private dataSource: DataSource, private dataSource: DataSource
) {} ) {}
async createWithProfile( async createWithProfile(userData: CreateUserDto, profileData: CreateProfileDto): Promise<User> {
userData: CreateUserDto,
profileData: CreateProfileDto,
): Promise<User> {
return this.dataSource.transaction(async (manager) => { return this.dataSource.transaction(async (manager) => {
const user = await manager.save(User, userData); const user = await manager.save(User, userData);
await manager.save(Profile, { ...profileData, userId: user.id }); 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}`); console.log(`Database service shutting down on ${signal}`);
// Close all connections gracefully // Close all connections gracefully
await Promise.all( await Promise.all(this.connections.map((conn) => conn.close()));
this.connections.map((conn) => conn.close()),
);
console.log('All database connections closed'); console.log('All database connections closed');
} }
@@ -150,9 +148,7 @@ export class HealthController {
throw new ServiceUnavailableException('Shutting down'); throw new ServiceUnavailableException('Shutting down');
} }
return this.health.check([ return this.health.check([() => this.db.pingCheck('database')]);
() => this.db.pingCheck('database'),
]);
} }
} }
@@ -208,10 +204,7 @@ export class RequestTracker implements NestMiddleware, OnApplicationShutdown {
}); });
// Wait with timeout // Wait with timeout
await Promise.race([ await Promise.race([this.shutdownPromise, new Promise((resolve) => setTimeout(resolve, 30000))]);
this.shutdownPromise,
new Promise((resolve) => setTimeout(resolve, 30000)),
]);
} }
console.log('All requests completed'); console.log('All requests completed');
@@ -61,9 +61,7 @@ export const appConfig = registerAs('app', () => ({
// config/validation.schema.ts // config/validation.schema.ts
export const validationSchema = Joi.object({ export const validationSchema = Joi.object({
NODE_ENV: Joi.string() NODE_ENV: Joi.string().valid('development', 'production', 'test').default('development'),
.valid('development', 'production', 'test')
.default('development'),
PORT: Joi.number().default(3000), PORT: Joi.number().default(3000),
DB_HOST: Joi.string().required(), DB_HOST: Joi.string().required(),
DB_PORT: Joi.number().default(5432), DB_PORT: Joi.number().default(5432),
@@ -137,7 +135,7 @@ export class AppService {
export class DatabaseService { export class DatabaseService {
constructor( constructor(
@Inject(databaseConfig.KEY) @Inject(databaseConfig.KEY)
private dbConfig: ConfigType<typeof databaseConfig>, private dbConfig: ConfigType<typeof databaseConfig>
) { ) {
// Full type inference! // Full type inference!
const host = this.dbConfig.host; // string const host = this.dbConfig.host; // string
@@ -147,12 +145,7 @@ export class DatabaseService {
// Environment files support // Environment files support
ConfigModule.forRoot({ ConfigModule.forRoot({
envFilePath: [ envFilePath: [`.env.${process.env.NODE_ENV}.local`, `.env.${process.env.NODE_ENV}`, '.env.local', '.env'],
`.env.${process.env.NODE_ENV}.local`,
`.env.${process.env.NODE_ENV}`,
'.env.local',
'.env',
],
}); });
// .env.development // .env.development
@@ -45,9 +45,7 @@ logger.log('User ' + userId + ' created at ' + new Date());
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create(AppModule, { const app = await NestFactory.create(AppModule, {
logger: logger:
process.env.NODE_ENV === 'production' process.env.NODE_ENV === 'production' ? ['error', 'warn', 'log'] : ['error', 'warn', 'log', 'debug', 'verbose'],
? ['error', 'warn', 'log']
: ['error', 'warn', 'log', 'debug', 'verbose'],
}); });
} }
@@ -82,7 +80,7 @@ export class JsonLogger implements LoggerService {
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
message, message,
...context, ...context,
}), })
); );
} }
@@ -94,7 +92,7 @@ export class JsonLogger implements LoggerService {
message, message,
trace, trace,
...context, ...context,
}), })
); );
} }
@@ -105,7 +103,7 @@ export class JsonLogger implements LoggerService {
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
message, message,
...context, ...context,
}), })
); );
} }
@@ -116,7 +114,7 @@ export class JsonLogger implements LoggerService {
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
message, message,
...context, ...context,
}), })
); );
} }
} }
@@ -166,7 +164,7 @@ export class ContextLogger {
userId: this.cls.get('userId'), userId: this.cls.get('userId'),
message, message,
...data, ...data,
}), })
); );
} }
@@ -181,7 +179,7 @@ export class ContextLogger {
error: error.message, error: error.message,
stack: error.stack, stack: error.stack,
...data, ...data,
}), })
); );
} }
} }
@@ -194,10 +192,7 @@ import { LoggerModule } from 'nestjs-pino';
LoggerModule.forRoot({ LoggerModule.forRoot({
pinoHttp: { pinoHttp: {
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug', level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
transport: transport: process.env.NODE_ENV !== 'production' ? { target: 'pino-pretty' } : undefined,
process.env.NODE_ENV !== 'production'
? { target: 'pino-pretty' }
: undefined,
redact: ['req.headers.authorization', 'req.body.password'], redact: ['req.headers.authorization', 'req.body.password'],
serializers: { serializers: {
req: (req) => ({ req: (req) => ({
@@ -55,7 +55,7 @@ export class OrdersService {
constructor( constructor(
private usersService: UsersService, private usersService: UsersService,
private inventoryService: InventoryService, private inventoryService: InventoryService,
private paymentService: PaymentService, private paymentService: PaymentService
) {} ) {}
async createOrder(dto: CreateOrderDto): Promise<Order> { async createOrder(dto: CreateOrderDto): Promise<Order> {
@@ -28,14 +28,14 @@ interface NotificationService {
@Injectable() @Injectable()
export class OrdersService { export class OrdersService {
constructor( 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> { async confirmOrder(order: Order): Promise<void> {
await this.notifications.sendEmail( await this.notifications.sendEmail(
order.customer.email, order.customer.email,
'Order Confirmed', '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 // Testing is painful - must mock unused methods
const mockNotificationService = { const mockNotificationService = {
sendEmail: jest.fn(), sendEmail: jest.fn(),
sendSms: jest.fn(), // Never used, but required sendSms: jest.fn(), // Never used, but required
sendPush: jest.fn(), // Never used, but required sendPush: jest.fn(), // Never used, but required
sendSlack: jest.fn(), // Never used, but required sendSlack: jest.fn(), // Never used, but required
logNotification: jest.fn(), // Never used, but required logNotification: jest.fn(), // Never used, but required
getDeliveryStatus: 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 scheduleNotification: jest.fn(), // Never used, but required
}; };
``` ```
@@ -105,14 +105,14 @@ export class SendGridEmailService implements EmailSender {
@Injectable() @Injectable()
export class OrdersService { export class OrdersService {
constructor( constructor(
@Inject(EMAIL_SENDER) private emailSender: EmailSender, // Minimal dependency @Inject(EMAIL_SENDER) private emailSender: EmailSender // Minimal dependency
) {} ) {}
async confirmOrder(order: Order): Promise<void> { async confirmOrder(order: Order): Promise<void> {
await this.emailSender.sendEmail( await this.emailSender.sendEmail(
order.customer.email, order.customer.email,
'Order Confirmed', '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 { export class AlertService {
constructor( constructor(
@Inject(MULTI_CHANNEL_SENDER) @Inject(MULTI_CHANNEL_SENDER)
private sender: EmailSender & SmsSender, private sender: EmailSender & SmsSender
) {} ) {}
async sendCriticalAlert(user: User, message: string): Promise<void> { async sendCriticalAlert(user: User, message: string): Promise<void> {
@@ -178,9 +178,7 @@ export class OrdersService {
```typescript ```typescript
// Shared test suite that any implementation must pass // Shared test suite that any implementation must pass
function testPaymentGatewayContract( function testPaymentGatewayContract(createGateway: () => PaymentGateway) {
createGateway: () => PaymentGateway,
) {
describe('PaymentGateway contract', () => { describe('PaymentGateway contract', () => {
let gateway: PaymentGateway; let gateway: PaymentGateway;
@@ -197,13 +195,11 @@ function testPaymentGatewayContract(
}); });
it('throws InvalidCurrencyException for unsupported currency', async () => { it('throws InvalidCurrencyException for unsupported currency', async () => {
await expect(gateway.charge(1000, 'INVALID')) await expect(gateway.charge(1000, 'INVALID')).rejects.toThrow(InvalidCurrencyException);
.rejects.toThrow(InvalidCurrencyException);
}); });
it('throws TransactionNotFoundException for invalid refund', async () => { it('throws TransactionNotFoundException for invalid refund', async () => {
await expect(gateway.refund('nonexistent')) await expect(gateway.refund('nonexistent')).rejects.toThrow(TransactionNotFoundException);
.rejects.toThrow(TransactionNotFoundException);
}); });
}); });
} }
@@ -40,7 +40,7 @@ export class UsersService {
export class UsersService { export class UsersService {
constructor( constructor(
private readonly userRepo: UserRepository, private readonly userRepo: UserRepository,
@Inject('CONFIG') private readonly config: ConfigType, @Inject('CONFIG') private readonly config: ConfigType
) {} ) {}
async findAll(): Promise<User[]> { async findAll(): Promise<User[]> {
@@ -19,7 +19,9 @@ interface PaymentGateway {
@Injectable() @Injectable()
export class StripeService implements PaymentGateway { export class StripeService implements PaymentGateway {
charge(amount: number) { /* ... */ } charge(amount: number) {
/* ... */
}
} }
@Injectable() @Injectable()
@@ -58,9 +60,7 @@ export class MockPaymentService implements PaymentGateway {
providers: [ providers: [
{ {
provide: PAYMENT_GATEWAY, provide: PAYMENT_GATEWAY,
useClass: process.env.NODE_ENV === 'test' useClass: process.env.NODE_ENV === 'test' ? MockPaymentService : StripeService,
? MockPaymentService
: StripeService,
}, },
], ],
exports: [PAYMENT_GATEWAY], exports: [PAYMENT_GATEWAY],
@@ -70,9 +70,7 @@ export class PaymentModule {}
// Injection // Injection
@Injectable() @Injectable()
export class OrdersService { export class OrdersService {
constructor( constructor(@Inject(PAYMENT_GATEWAY) private payment: PaymentGateway) {}
@Inject(PAYMENT_GATEWAY) private payment: PaymentGateway,
) {}
async createOrder(dto: CreateOrderDto) { async createOrder(dto: CreateOrderDto) {
await this.payment.charge(dto.amount); await this.payment.charge(dto.amount);
@@ -88,7 +88,7 @@ export class UsersController {
export class EntityNotFoundException extends Error { export class EntityNotFoundException extends Error {
constructor( constructor(
public readonly entity: string, public readonly entity: string,
public readonly id: string, public readonly id: string
) { ) {
super(`${entity} with ID "${id}" not found`); super(`${entity} with ID "${id}" not found`);
} }
@@ -95,20 +95,11 @@ export class AllExceptionsFilter implements ExceptionFilter {
const response = ctx.getResponse<Response>(); const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>(); const request = ctx.getRequest<Request>();
const status = const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR;
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const message = const message = exception instanceof HttpException ? exception.message : 'Internal server error';
exception instanceof HttpException
? exception.message
: 'Internal server error';
this.logger.error( this.logger.error(`${request.method} ${request.url}`, exception instanceof Error ? exception.stack : exception);
`${request.method} ${request.url}`,
exception instanceof Error ? exception.stack : exception,
);
response.status(status).json({ response.status(status).json({
statusCode: status, statusCode: status,
@@ -120,10 +111,7 @@ export class AllExceptionsFilter implements ExceptionFilter {
} }
// Register globally in main.ts // Register globally in main.ts
app.useGlobalFilters( app.useGlobalFilters(new AllExceptionsFilter(app.get(Logger)), new DomainExceptionFilter());
new AllExceptionsFilter(app.get(Logger)),
new DomainExceptionFilter(),
);
// Or via module // Or via module
@Module({ @Module({
@@ -64,11 +64,7 @@ import { BullModule } from '@nestjs/bullmq';
}, },
}, },
}), }),
BullModule.registerQueue( BullModule.registerQueue({ name: 'email' }, { name: 'reports' }, { name: 'notifications' }),
{ name: 'email' },
{ name: 'reports' },
{ name: 'notifications' },
),
], ],
}) })
export class QueueModule {} export class QueueModule {}
@@ -76,9 +72,7 @@ export class QueueModule {}
// Producer: Add jobs to queue // Producer: Add jobs to queue
@Injectable() @Injectable()
export class ReportsService { export class ReportsService {
constructor( constructor(@InjectQueue('reports') private reportsQueue: Queue) {}
@InjectQueue('reports') private reportsQueue: Queue,
) {}
async requestReport(dto: GenerateReportDto): Promise<{ jobId: string }> { async requestReport(dto: GenerateReportDto): Promise<{ jobId: string }> {
// Return immediately, process in background // Return immediately, process in background
@@ -176,7 +170,7 @@ export class NotificationService {
{ {
attempts: 5, attempts: 5,
backoff: { type: 'exponential', delay: 5000 }, backoff: { type: 'exponential', delay: 5000 },
}, }
); );
} }
} }
@@ -194,7 +188,7 @@ export class ScheduledJobsService implements OnModuleInit {
{ {
repeat: { cron: '0 0 * * *' }, repeat: { cron: '0 0 * * *' },
jobId: 'daily-cleanup', // Prevent duplicates jobId: 'daily-cleanup', // Prevent duplicates
}, }
); );
// Send digest every hour // Send digest every hour
@@ -204,7 +198,7 @@ export class ScheduledJobsService implements OnModuleInit {
{ {
repeat: { every: 60 * 60 * 1000 }, repeat: { every: 60 * 60 * 1000 },
jobId: 'hourly-digest', jobId: 'hourly-digest',
}, }
); );
} }
} }
@@ -64,7 +64,7 @@ export class DatabaseService implements OnModuleInit {
export class CacheWarmerService implements OnApplicationBootstrap { export class CacheWarmerService implements OnApplicationBootstrap {
constructor( constructor(
private cache: CacheService, private cache: CacheService,
private products: ProductsService, private products: ProductsService
) {} ) {}
async onApplicationBootstrap(): Promise<void> { async onApplicationBootstrap(): Promise<void> {
@@ -81,10 +81,7 @@ export class ModuleLoaderService {
constructor(private lazyModuleLoader: LazyModuleLoader) {} constructor(private lazyModuleLoader: LazyModuleLoader) {}
async load<T>( async load<T>(key: string, importFn: () => Promise<{ default: Type<T> } | Type<T>>): Promise<ModuleRef> {
key: string,
importFn: () => Promise<{ default: Type<T> } | Type<T>>,
): Promise<ModuleRef> {
if (!this.loadedModules.has(key)) { if (!this.loadedModules.has(key)) {
const module = await importFn(); const module = await importFn();
const moduleType = 'default' in module ? module.default : module; const moduleType = 'default' in module ? module.default : module;
@@ -51,9 +51,7 @@ export class UsersService {
imports: [ConfigModule], imports: [ConfigModule],
inject: [ConfigService], inject: [ConfigService],
useFactory: (config: ConfigService) => ({ useFactory: (config: ConfigService) => ({
stores: [ stores: [new KeyvRedis(config.get('REDIS_URL'))],
new KeyvRedis(config.get('REDIS_URL')),
],
ttl: 60 * 1000, // Default 60s ttl: 60 * 1000, // Default 60s
}), }),
}), }),
@@ -66,7 +64,7 @@ export class AppModule {}
export class ProductsService { export class ProductsService {
constructor( constructor(
@Inject(CACHE_MANAGER) private cache: Cache, @Inject(CACHE_MANAGER) private cache: Cache,
private productsRepo: ProductRepository, private productsRepo: ProductRepository
) {} ) {}
async getPopular(): Promise<Product[]> { async getPopular(): Promise<Product[]> {
@@ -117,10 +115,7 @@ export class CacheInvalidationService {
@OnEvent('product.updated') @OnEvent('product.updated')
@OnEvent('product.deleted') @OnEvent('product.deleted')
async invalidateProductCaches(event: ProductEvent) { async invalidateProductCaches(event: ProductEvent) {
await Promise.all([ await Promise.all([this.cache.del('products:popular'), this.cache.del(`product:${event.productId}`)]);
this.cache.del('products:popular'),
this.cache.del(`product:${event.productId}`),
]);
} }
} }
``` ```
@@ -111,7 +111,7 @@ export class AuthService {
export class JwtStrategy extends PassportStrategy(Strategy) { export class JwtStrategy extends PassportStrategy(Strategy) {
constructor( constructor(
private config: ConfigService, private config: ConfigService,
private usersService: UsersService, private usersService: UsersService
) { ) {
super({ super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
@@ -47,15 +47,12 @@ export class AdminController {
export class JwtAuthGuard implements CanActivate { export class JwtAuthGuard implements CanActivate {
constructor( constructor(
private jwtService: JwtService, private jwtService: JwtService,
private reflector: Reflector, private reflector: Reflector
) {} ) {}
async canActivate(context: ExecutionContext): Promise<boolean> { async canActivate(context: ExecutionContext): Promise<boolean> {
// Check for @Public() decorator // Check for @Public() decorator
const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [ const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [context.getHandler(), context.getClass()]);
context.getHandler(),
context.getClass(),
]);
if (isPublic) return true; if (isPublic) return true;
const request = context.switchToHttp().getRequest(); const request = context.switchToHttp().getRequest();
@@ -85,10 +82,7 @@ export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {} constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean { canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<Role[]>('roles', [ const requiredRoles = this.reflector.getAllAndOverride<Role[]>('roles', [context.getHandler(), context.getClass()]);
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) return true; if (!requiredRoles) return true;
@@ -30,9 +30,9 @@ export class UsersController {
// DTOs without validation decorators // DTOs without validation decorators
export class CreateUserDto { export class CreateUserDto {
name: string; // No validation name: string; // No validation
email: string; // Could be "not-an-email" email: string; // Could be "not-an-email"
age: number; // Could be "abc" or -999 age: number; // Could be "abc" or -999
} }
``` ```
@@ -45,13 +45,13 @@ async function bootstrap() {
app.useGlobalPipes( app.useGlobalPipes(
new ValidationPipe({ new ValidationPipe({
whitelist: true, // Strip unknown properties whitelist: true, // Strip unknown properties
forbidNonWhitelisted: true, // Throw on unknown properties forbidNonWhitelisted: true, // Throw on unknown properties
transform: true, // Auto-transform to DTO types transform: true, // Auto-transform to DTO types
transformOptions: { transformOptions: {
enableImplicitConversion: true, enableImplicitConversion: true,
}, },
}), })
); );
await app.listen(3000); await app.listen(3000);
@@ -61,7 +61,7 @@ describe('UsersController (e2e)', () => {
whitelist: true, whitelist: true,
transform: true, transform: true,
forbidNonWhitelisted: true, forbidNonWhitelisted: true,
}), })
); );
await app.init(); await app.init();
@@ -97,9 +97,7 @@ describe('UsersController (e2e)', () => {
describe('/users/:id (GET)', () => { describe('/users/:id (GET)', () => {
it('should return 404 for non-existent user', () => { it('should return 404 for non-existent user', () => {
return request(app.getHttpServer()) return request(app.getHttpServer()).get('/users/non-existent-id').expect(404);
.get('/users/non-existent-id')
.expect(404);
}); });
}); });
}); });
@@ -127,9 +125,7 @@ describe('Protected Routes (e2e)', () => {
}); });
it('should return 401 without token', () => { it('should return 401 without token', () => {
return request(app.getHttpServer()) return request(app.getHttpServer()).get('/users/me').expect(401);
.get('/users/me')
.expect(401);
}); });
it('should return user profile with valid token', () => { it('should return user profile with valid token', () => {
@@ -84,9 +84,7 @@ describe('WeatherService', () => {
}); });
it('should handle API timeout', async () => { it('should handle API timeout', async () => {
httpService.get.mockReturnValue( httpService.get.mockReturnValue(throwError(() => new Error('ETIMEDOUT')));
throwError(() => new Error('ETIMEDOUT')),
);
await expect(service.getWeather('NYC')).rejects.toThrow('Weather service unavailable'); await expect(service.getWeather('NYC')).rejects.toThrow('Weather service unavailable');
}); });
@@ -95,7 +93,7 @@ describe('WeatherService', () => {
httpService.get.mockReturnValue( httpService.get.mockReturnValue(
throwError(() => ({ throwError(() => ({
response: { status: 429, data: { message: 'Rate limited' } }, response: { status: 429, data: { message: 'Rate limited' } },
})), }))
); );
await expect(service.getWeather('NYC')).rejects.toThrow(TooManyRequestsException); await expect(service.getWeather('NYC')).rejects.toThrow(TooManyRequestsException);
@@ -117,10 +115,7 @@ describe('UsersService', () => {
}; };
const module = await Test.createTestingModule({ const module = await Test.createTestingModule({
providers: [ providers: [UsersService, { provide: getRepositoryToken(User), useValue: mockRepo }],
UsersService,
{ provide: getRepositoryToken(User), useValue: mockRepo },
],
}).compile(); }).compile();
service = module.get(UsersService); service = module.get(UsersService);
@@ -86,9 +86,7 @@ describe('UsersService', () => {
it('should throw on duplicate email', async () => { it('should throw on duplicate email', async () => {
repo.findOne.mockResolvedValue({ id: '1', email: 'test@test.com' }); repo.findOne.mockResolvedValue({ id: '1', email: 'test@test.com' });
await expect( await expect(service.create({ name: 'Test', email: 'test@test.com' })).rejects.toThrow(ConflictException);
service.create({ name: 'Test', email: 'test@test.com' }),
).rejects.toThrow(ConflictException);
}); });
}); });
@@ -98,7 +98,7 @@ function parseFrontmatter(content: string): { frontmatter: RuleFrontmatter | nul
return { return {
frontmatter: frontmatter as RuleFrontmatter, frontmatter: frontmatter as RuleFrontmatter,
body: body.trim() body: body.trim(),
}; };
} }
@@ -118,8 +118,7 @@ function readMetadata(): any {
function readRules(): Rule[] { function readRules(): Rule[] {
const rulesDir = path.join(__dirname, '..', 'rules'); const rulesDir = path.join(__dirname, '..', 'rules');
const files = fs.readdirSync(rulesDir) const files = fs.readdirSync(rulesDir).filter((f) => f.endsWith('.md') && !f.startsWith('_'));
.filter(f => f.endsWith('.md') && !f.startsWith('_'));
const rules: Rule[] = []; const rules: Rule[] = [];
@@ -144,7 +143,7 @@ function readRules(): Rule[] {
frontmatter, frontmatter,
content: body, content: body,
category: category.name, category: category.name,
categorySection: category.section categorySection: category.section,
}); });
} }
+19 -1
View File
@@ -11,6 +11,7 @@ Apply these rules when writing or reviewing Next.js code.
## File Conventions ## File Conventions
See [file-conventions.md](./file-conventions.md) for: See [file-conventions.md](./file-conventions.md) for:
- Project structure and special files - Project structure and special files
- Route segments (dynamic, catch-all, groups) - Route segments (dynamic, catch-all, groups)
- Parallel and intercepting routes - Parallel and intercepting routes
@@ -21,6 +22,7 @@ See [file-conventions.md](./file-conventions.md) for:
Detect invalid React Server Component patterns. Detect invalid React Server Component patterns.
See [rsc-boundaries.md](./rsc-boundaries.md) for: See [rsc-boundaries.md](./rsc-boundaries.md) for:
- Async client component detection (invalid) - Async client component detection (invalid)
- Non-serializable props detection - Non-serializable props detection
- Server Action exceptions - Server Action exceptions
@@ -30,6 +32,7 @@ See [rsc-boundaries.md](./rsc-boundaries.md) for:
Next.js 15+ async API changes. Next.js 15+ async API changes.
See [async-patterns.md](./async-patterns.md) for: See [async-patterns.md](./async-patterns.md) for:
- Async `params` and `searchParams` - Async `params` and `searchParams`
- Async `cookies()` and `headers()` - Async `cookies()` and `headers()`
- Migration codemod - Migration codemod
@@ -37,18 +40,21 @@ See [async-patterns.md](./async-patterns.md) for:
## Runtime Selection ## Runtime Selection
See [runtime-selection.md](./runtime-selection.md) for: See [runtime-selection.md](./runtime-selection.md) for:
- Default to Node.js runtime - Default to Node.js runtime
- When Edge runtime is appropriate - When Edge runtime is appropriate
## Directives ## Directives
See [directives.md](./directives.md) for: See [directives.md](./directives.md) for:
- `'use client'`, `'use server'` (React) - `'use client'`, `'use server'` (React)
- `'use cache'` (Next.js) - `'use cache'` (Next.js)
## Functions ## Functions
See [functions.md](./functions.md) for: See [functions.md](./functions.md) for:
- Navigation hooks: `useRouter`, `usePathname`, `useSearchParams`, `useParams` - Navigation hooks: `useRouter`, `usePathname`, `useSearchParams`, `useParams`
- Server functions: `cookies`, `headers`, `draftMode`, `after` - Server functions: `cookies`, `headers`, `draftMode`, `after`
- Generate functions: `generateStaticParams`, `generateMetadata` - Generate functions: `generateStaticParams`, `generateMetadata`
@@ -56,6 +62,7 @@ See [functions.md](./functions.md) for:
## Error Handling ## Error Handling
See [error-handling.md](./error-handling.md) for: See [error-handling.md](./error-handling.md) for:
- `error.tsx`, `global-error.tsx`, `not-found.tsx` - `error.tsx`, `global-error.tsx`, `not-found.tsx`
- `redirect`, `permanentRedirect`, `notFound` - `redirect`, `permanentRedirect`, `notFound`
- `forbidden`, `unauthorized` (auth errors) - `forbidden`, `unauthorized` (auth errors)
@@ -64,6 +71,7 @@ See [error-handling.md](./error-handling.md) for:
## Data Patterns ## Data Patterns
See [data-patterns.md](./data-patterns.md) for: See [data-patterns.md](./data-patterns.md) for:
- Server Components vs Server Actions vs Route Handlers - Server Components vs Server Actions vs Route Handlers
- Avoiding data waterfalls (`Promise.all`, Suspense, preload) - Avoiding data waterfalls (`Promise.all`, Suspense, preload)
- Client component data fetching - Client component data fetching
@@ -71,6 +79,7 @@ See [data-patterns.md](./data-patterns.md) for:
## Route Handlers ## Route Handlers
See [route-handlers.md](./route-handlers.md) for: See [route-handlers.md](./route-handlers.md) for:
- `route.ts` basics - `route.ts` basics
- GET handler conflicts with `page.tsx` - GET handler conflicts with `page.tsx`
- Environment behavior (no React DOM) - Environment behavior (no React DOM)
@@ -79,6 +88,7 @@ See [route-handlers.md](./route-handlers.md) for:
## Metadata & OG Images ## Metadata & OG Images
See [metadata.md](./metadata.md) for: See [metadata.md](./metadata.md) for:
- Static and dynamic metadata - Static and dynamic metadata
- `generateMetadata` function - `generateMetadata` function
- OG image generation with `next/og` - OG image generation with `next/og`
@@ -87,6 +97,7 @@ See [metadata.md](./metadata.md) for:
## Image Optimization ## Image Optimization
See [image.md](./image.md) for: See [image.md](./image.md) for:
- Always use `next/image` over `<img>` - Always use `next/image` over `<img>`
- Remote images configuration - Remote images configuration
- Responsive `sizes` attribute - Responsive `sizes` attribute
@@ -96,6 +107,7 @@ See [image.md](./image.md) for:
## Font Optimization ## Font Optimization
See [font.md](./font.md) for: See [font.md](./font.md) for:
- `next/font` setup - `next/font` setup
- Google Fonts, local fonts - Google Fonts, local fonts
- Tailwind CSS integration - Tailwind CSS integration
@@ -104,6 +116,7 @@ See [font.md](./font.md) for:
## Bundling ## Bundling
See [bundling.md](./bundling.md) for: See [bundling.md](./bundling.md) for:
- Server-incompatible packages - Server-incompatible packages
- CSS imports (not link tags) - CSS imports (not link tags)
- Polyfills (already included) - Polyfills (already included)
@@ -113,6 +126,7 @@ See [bundling.md](./bundling.md) for:
## Scripts ## Scripts
See [scripts.md](./scripts.md) for: See [scripts.md](./scripts.md) for:
- `next/script` vs native script tags - `next/script` vs native script tags
- Inline scripts need `id` - Inline scripts need `id`
- Loading strategies - Loading strategies
@@ -121,6 +135,7 @@ See [scripts.md](./scripts.md) for:
## Hydration Errors ## Hydration Errors
See [hydration-error.md](./hydration-error.md) for: See [hydration-error.md](./hydration-error.md) for:
- Common causes (browser APIs, dates, invalid HTML) - Common causes (browser APIs, dates, invalid HTML)
- Debugging with error overlay - Debugging with error overlay
- Fixes for each cause - Fixes for each cause
@@ -128,12 +143,14 @@ See [hydration-error.md](./hydration-error.md) for:
## Suspense Boundaries ## Suspense Boundaries
See [suspense-boundaries.md](./suspense-boundaries.md) for: See [suspense-boundaries.md](./suspense-boundaries.md) for:
- CSR bailout with `useSearchParams` and `usePathname` - CSR bailout with `useSearchParams` and `usePathname`
- Which hooks require Suspense boundaries - Which hooks require Suspense boundaries
## Parallel & Intercepting Routes ## Parallel & Intercepting Routes
See [parallel-routes.md](./parallel-routes.md) for: See [parallel-routes.md](./parallel-routes.md) for:
- Modal patterns with `@slot` and `(.)` interceptors - Modal patterns with `@slot` and `(.)` interceptors
- `default.tsx` for fallbacks - `default.tsx` for fallbacks
- Closing modals correctly with `router.back()` - Closing modals correctly with `router.back()`
@@ -141,6 +158,7 @@ See [parallel-routes.md](./parallel-routes.md) for:
## Self-Hosting ## Self-Hosting
See [self-hosting.md](./self-hosting.md) for: See [self-hosting.md](./self-hosting.md) for:
- `output: 'standalone'` for Docker - `output: 'standalone'` for Docker
- Cache handlers for multi-instance ISR - Cache handlers for multi-instance ISR
- What works vs needs extra setup - What works vs needs extra setup
@@ -148,6 +166,6 @@ See [self-hosting.md](./self-hosting.md) for:
## Debug Tricks ## Debug Tricks
See [debug-tricks.md](./debug-tricks.md) for: See [debug-tricks.md](./debug-tricks.md) for:
- MCP endpoint for AI-assisted debugging - MCP endpoint for AI-assisted debugging
- Rebuild specific routes with `--debug-build-paths` - Rebuild specific routes with `--debug-build-paths`
@@ -9,21 +9,18 @@ Always type them as `Promise<...>` and await them.
### Pages and Layouts ### Pages and Layouts
```tsx ```tsx
type Props = { params: Promise<{ slug: string }> } type Props = { params: Promise<{ slug: string }> };
export default async function Page({ params }: Props) { export default async function Page({ params }: Props) {
const { slug } = await params const { slug } = await params;
} }
``` ```
### Route Handlers ### Route Handlers
```tsx ```tsx
export async function GET( export async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) {
request: Request, const { id } = await params;
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params
} }
``` ```
@@ -31,13 +28,13 @@ export async function GET(
```tsx ```tsx
type Props = { type Props = {
params: Promise<{ slug: string }> params: Promise<{ slug: string }>;
searchParams: Promise<{ query?: string }> searchParams: Promise<{ query?: string }>;
} };
export default async function Page({ params, searchParams }: Props) { export default async function Page({ params, searchParams }: Props) {
const { slug } = await params const { slug } = await params;
const { query } = await searchParams const { query } = await searchParams;
} }
``` ```
@@ -46,37 +43,37 @@ export default async function Page({ params, searchParams }: Props) {
Use `React.use()` for non-async components: Use `React.use()` for non-async components:
```tsx ```tsx
import { use } from 'react' import { use } from 'react';
type Props = { params: Promise<{ slug: string }> } type Props = { params: Promise<{ slug: string }> };
export default function Page({ params }: Props) { export default function Page({ params }: Props) {
const { slug } = use(params) const { slug } = use(params);
} }
``` ```
### generateMetadata ### generateMetadata
```tsx ```tsx
type Props = { params: Promise<{ slug: string }> } type Props = { params: Promise<{ slug: string }> };
export async function generateMetadata({ params }: Props): Promise<Metadata> { export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params const { slug } = await params;
return { title: slug } return { title: slug };
} }
``` ```
## Async Cookies and Headers ## Async Cookies and Headers
```tsx ```tsx
import { cookies, headers } from 'next/headers' import { cookies, headers } from 'next/headers';
export default async function Page() { export default async function Page() {
const cookieStore = await cookies() const cookieStore = await cookies();
const headersList = await headers() const headersList = await headers();
const theme = cookieStore.get('theme') const theme = cookieStore.get('theme');
const userAgent = headersList.get('user-agent') const userAgent = headersList.get('user-agent');
} }
``` ```
+28 -26
View File
@@ -21,21 +21,21 @@ If the package is only needed on client:
```tsx ```tsx
// Bad: Fails - package uses window // Bad: Fails - package uses window
import SomeChart from 'some-chart-library' import SomeChart from 'some-chart-library';
export default function Page() { export default function Page() {
return <SomeChart /> return <SomeChart />;
} }
// Good: Use dynamic import with ssr: false // Good: Use dynamic import with ssr: false
import dynamic from 'next/dynamic' import dynamic from 'next/dynamic';
const SomeChart = dynamic(() => import('some-chart-library'), { const SomeChart = dynamic(() => import('some-chart-library'), {
ssr: false, ssr: false,
}) });
export default function Page() { export default function Page() {
return <SomeChart /> return <SomeChart />;
} }
``` ```
@@ -47,10 +47,11 @@ For packages that should run on server but have bundling issues:
// next.config.js // next.config.js
module.exports = { module.exports = {
serverExternalPackages: ['problematic-package'], serverExternalPackages: ['problematic-package'],
} };
``` ```
Use this for: Use this for:
- Packages with native bindings (sharp, bcrypt) - Packages with native bindings (sharp, bcrypt)
- Packages that don't bundle well (some ORMs) - Packages that don't bundle well (some ORMs)
- Packages with circular dependencies - Packages with circular dependencies
@@ -61,19 +62,19 @@ Wrap the entire usage in a client component:
```tsx ```tsx
// components/ChartWrapper.tsx // components/ChartWrapper.tsx
'use client' 'use client';
import { Chart } from 'chart-library' import { Chart } from 'chart-library';
export function ChartWrapper(props) { export function ChartWrapper(props) {
return <Chart {...props} /> return <Chart {...props} />;
} }
// app/page.tsx (server component) // app/page.tsx (server component)
import { ChartWrapper } from '@/components/ChartWrapper' import { ChartWrapper } from '@/components/ChartWrapper';
export default function Page() { export default function Page() {
return <ChartWrapper data={data} /> return <ChartWrapper data={data} />;
} }
``` ```
@@ -83,13 +84,13 @@ Import CSS files instead of using `<link>` tags. Next.js handles bundling and op
```tsx ```tsx
// Bad: Manual link tag // Bad: Manual link tag
<link rel="stylesheet" href="/styles.css" /> <link rel="stylesheet" href="/styles.css" />;
// Good: Import CSS // Good: Import CSS
import './styles.css' import './styles.css';
// Good: CSS Modules // Good: CSS Modules
import styles from './Button.module.css' import styles from './Button.module.css';
``` ```
## Polyfills ## Polyfills
@@ -121,21 +122,21 @@ Module not found: ESM packages need to be imported
// next.config.js // next.config.js
module.exports = { module.exports = {
transpilePackages: ['some-esm-package', 'another-package'], transpilePackages: ['some-esm-package', 'another-package'],
} };
``` ```
## Common Problematic Packages ## Common Problematic Packages
| Package | Issue | Solution | | Package | Issue | Solution |
|---------|-------|----------| | --------------- | --------------- | --------------------------------------------------------------- |
| `sharp` | Native bindings | `serverExternalPackages: ['sharp']` | | `sharp` | Native bindings | `serverExternalPackages: ['sharp']` |
| `bcrypt` | Native bindings | `serverExternalPackages: ['bcrypt']` or use `bcryptjs` | | `bcrypt` | Native bindings | `serverExternalPackages: ['bcrypt']` or use `bcryptjs` |
| `canvas` | Native bindings | `serverExternalPackages: ['canvas']` | | `canvas` | Native bindings | `serverExternalPackages: ['canvas']` |
| `recharts` | Uses window | `dynamic(() => import('recharts'), { ssr: false })` | | `recharts` | Uses window | `dynamic(() => import('recharts'), { ssr: false })` |
| `react-quill` | Uses document | `dynamic(() => import('react-quill'), { ssr: false })` | | `react-quill` | Uses document | `dynamic(() => import('react-quill'), { ssr: false })` |
| `mapbox-gl` | Uses window | `dynamic(() => import('mapbox-gl'), { ssr: false })` | | `mapbox-gl` | Uses window | `dynamic(() => import('mapbox-gl'), { ssr: false })` |
| `monaco-editor` | Uses window | `dynamic(() => import('@monaco-editor/react'), { ssr: false })` | | `monaco-editor` | Uses window | `dynamic(() => import('@monaco-editor/react'), { ssr: false })` |
| `lottie-web` | Uses document | `dynamic(() => import('lottie-react'), { ssr: false })` | | `lottie-web` | Uses document | `dynamic(() => import('lottie-react'), { ssr: false })` |
## Bundle Analysis ## Bundle Analysis
@@ -146,6 +147,7 @@ next experimental-analyze
``` ```
This opens an interactive UI to: This opens an interactive UI to:
- Filter by route, environment (client/server), and type - Filter by route, environment (client/server), and type
- Inspect module sizes and import chains - Inspect module sizes and import chains
- View treemap visualization - View treemap visualization
@@ -174,7 +176,7 @@ module.exports = {
webpack: (config) => { webpack: (config) => {
// custom webpack config // custom webpack config
}, },
} };
``` ```
Reference: https://nextjs.org/docs/app/building-your-application/upgrading/from-webpack-to-turbopack Reference: https://nextjs.org/docs/app/building-your-application/upgrading/from-webpack-to-turbopack
@@ -33,17 +33,20 @@ async function UsersPage() {
const users = await db.user.findMany(); const users = await db.user.findMany();
// Or fetch from external API // Or fetch from external API
const posts = await fetch('https://api.example.com/posts').then(r => r.json()); const posts = await fetch('https://api.example.com/posts').then((r) => r.json());
return ( return (
<ul> <ul>
{users.map(user => <li key={user.id}>{user.name}</li>)} {users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul> </ul>
); );
} }
``` ```
**Benefits**: **Benefits**:
- No API to maintain - No API to maintain
- No client-server waterfall - No client-server waterfall
- Secrets stay on server - Secrets stay on server
@@ -89,12 +92,14 @@ export default function NewPost() {
``` ```
**Benefits**: **Benefits**:
- End-to-end type safety - End-to-end type safety
- Progressive enhancement (works without JS) - Progressive enhancement (works without JS)
- Automatic request handling - Automatic request handling
- Integrated with React transitions - Integrated with React transitions
**Constraints**: **Constraints**:
- POST only (no GET caching semantics) - POST only (no GET caching semantics)
- Internal use only (no external access) - Internal use only (no external access)
- Cannot return non-serializable data - Cannot return non-serializable data
@@ -122,12 +127,14 @@ export async function POST(request: NextRequest) {
``` ```
**When to use**: **When to use**:
- External API access (mobile apps, third parties) - External API access (mobile apps, third parties)
- Webhooks from external services - Webhooks from external services
- GET endpoints that need HTTP caching - GET endpoints that need HTTP caching
- OpenAPI/Swagger documentation needed - OpenAPI/Swagger documentation needed
**When NOT to use**: **When NOT to use**:
- Internal data fetching (use Server Components) - Internal data fetching (use Server Components)
- Mutations from your UI (use Server Actions) - Mutations from your UI (use Server Actions)
@@ -138,8 +145,8 @@ export async function POST(request: NextRequest) {
```tsx ```tsx
// Bad: Sequential waterfalls // Bad: Sequential waterfalls
async function Dashboard() { async function Dashboard() {
const user = await getUser(); // Wait... const user = await getUser(); // Wait...
const posts = await getPosts(); // Then wait... const posts = await getPosts(); // Then wait...
const comments = await getComments(); // Then wait... const comments = await getComments(); // Then wait...
return <div>...</div>; return <div>...</div>;
@@ -151,11 +158,7 @@ async function Dashboard() {
```tsx ```tsx
// Good: Parallel fetching // Good: Parallel fetching
async function Dashboard() { async function Dashboard() {
const [user, posts, comments] = await Promise.all([ const [user, posts, comments] = await Promise.all([getUser(), getPosts(), getComments()]);
getUser(),
getPosts(),
getComments(),
]);
return <div>...</div>; return <div>...</div>;
} }
@@ -238,7 +241,7 @@ async function Page() {
} }
// Client Component // Client Component
'use client'; ('use client');
function ClientComponent({ initialData }) { function ClientComponent({ initialData }) {
const [data, setData] = useState(initialData); const [data, setData] = useState(initialData);
// ... // ...
@@ -256,7 +259,7 @@ function ClientComponent() {
useEffect(() => { useEffect(() => {
fetch('/api/data') fetch('/api/data')
.then(r => r.json()) .then((r) => r.json())
.then(setData); .then(setData);
}, []); }, []);
@@ -289,9 +292,9 @@ function ClientComponent() {
## Quick Reference ## Quick Reference
| Pattern | Use Case | HTTP Method | Caching | | Pattern | Use Case | HTTP Method | Caching |
|---------|----------|-------------|---------| | ---------------------- | --------------------------- | ----------- | -------------------- |
| Server Component fetch | Internal reads | Any | Full Next.js caching | | Server Component fetch | Internal reads | Any | Full Next.js caching |
| Server Action | Mutations, form submissions | POST only | No | | Server Action | Mutations, form submissions | POST only | No |
| Route Handler | External APIs, webhooks | Any | GET can be cached | | Route Handler | External APIs, webhooks | Any | GET can be cached |
| Client fetch to API | Client-side reads | Any | HTTP cache headers | | Client fetch to API | Client-side reads | Any | HTTP cache headers |
@@ -35,42 +35,58 @@ curl -X POST http://localhost:<port>/_next/mcp \
### Available Tools ### Available Tools
#### `get_errors` #### `get_errors`
Get current errors from dev server (build errors, runtime errors with source-mapped stacks): Get current errors from dev server (build errors, runtime errors with source-mapped stacks):
```json ```json
{ "name": "get_errors", "arguments": {} } { "name": "get_errors", "arguments": {} }
``` ```
#### `get_routes` #### `get_routes`
Discover all routes by scanning filesystem: Discover all routes by scanning filesystem:
```json ```json
{ "name": "get_routes", "arguments": {} } { "name": "get_routes", "arguments": {} }
// Optional: { "name": "get_routes", "arguments": { "routerType": "app" } } // Optional: { "name": "get_routes", "arguments": { "routerType": "app" } }
``` ```
Returns: `{ "appRouter": ["/", "/api/users/[id]", ...], "pagesRouter": [...] }` Returns: `{ "appRouter": ["/", "/api/users/[id]", ...], "pagesRouter": [...] }`
#### `get_project_metadata` #### `get_project_metadata`
Get project path and dev server URL: Get project path and dev server URL:
```json ```json
{ "name": "get_project_metadata", "arguments": {} } { "name": "get_project_metadata", "arguments": {} }
``` ```
Returns: `{ "projectPath": "/path/to/project", "devServerUrl": "http://localhost:3000" }` Returns: `{ "projectPath": "/path/to/project", "devServerUrl": "http://localhost:3000" }`
#### `get_page_metadata` #### `get_page_metadata`
Get runtime metadata about current page render (requires active browser session): Get runtime metadata about current page render (requires active browser session):
```json ```json
{ "name": "get_page_metadata", "arguments": {} } { "name": "get_page_metadata", "arguments": {} }
``` ```
Returns segment trie data showing layouts, boundaries, and page components. Returns segment trie data showing layouts, boundaries, and page components.
#### `get_logs` #### `get_logs`
Get path to Next.js development log file: Get path to Next.js development log file:
```json ```json
{ "name": "get_logs", "arguments": {} } { "name": "get_logs", "arguments": {} }
``` ```
Returns path to `<distDir>/logs/next-development.log` Returns path to `<distDir>/logs/next-development.log`
#### `get_server_action_by_id` #### `get_server_action_by_id`
Locate a Server Action by ID: Locate a Server Action by ID:
```json ```json
{ "name": "get_server_action_by_id", "arguments": { "actionId": "<action-id>" } } { "name": "get_server_action_by_id", "arguments": { "actionId": "<action-id>" } }
``` ```
@@ -100,6 +116,7 @@ next build --debug-build-paths "/blog/[slug]"
``` ```
Use this to: Use this to:
- Quickly verify a build fix without full rebuild - Quickly verify a build fix without full rebuild
- Debug static generation issues for specific pages - Debug static generation issues for specific pages
- Iterate faster on build errors - Iterate faster on build errors
@@ -7,18 +7,19 @@ These are React directives, not Next.js specific.
### `'use client'` ### `'use client'`
Marks a component as a Client Component. Required for: Marks a component as a Client Component. Required for:
- React hooks (`useState`, `useEffect`, etc.) - React hooks (`useState`, `useEffect`, etc.)
- Event handlers (`onClick`, `onChange`) - Event handlers (`onClick`, `onChange`)
- Browser APIs (`window`, `localStorage`) - Browser APIs (`window`, `localStorage`)
```tsx ```tsx
'use client' 'use client';
import { useState } from 'react' import { useState } from 'react';
export function Counter() { export function Counter() {
const [count, setCount] = useState(0) const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button> return <button onClick={() => setCount(count + 1)}>{count}</button>;
} }
``` ```
@@ -29,7 +30,7 @@ Reference: https://react.dev/reference/rsc/use-client
Marks a function as a Server Action. Can be passed to Client Components. Marks a function as a Server Action. Can be passed to Client Components.
```tsx ```tsx
'use server' 'use server';
export async function submitForm(formData: FormData) { export async function submitForm(formData: FormData) {
// Runs on server // Runs on server
@@ -41,10 +42,10 @@ Or inline within a Server Component:
```tsx ```tsx
export default function Page() { export default function Page() {
async function submit() { async function submit() {
'use server' 'use server';
// Runs on server // Runs on server
} }
return <form action={submit}>...</form> return <form action={submit}>...</form>;
} }
``` ```
@@ -59,10 +60,10 @@ Reference: https://react.dev/reference/rsc/use-server
Marks a function or component for caching. Part of Next.js Cache Components. Marks a function or component for caching. Part of Next.js Cache Components.
```tsx ```tsx
'use cache' 'use cache';
export async function getCachedData() { export async function getCachedData() {
return await fetchData() return await fetchData();
} }
``` ```
@@ -11,21 +11,15 @@ Reference: https://nextjs.org/docs/app/getting-started/error-handling
Catches errors in a route segment and its children: Catches errors in a route segment and its children:
```tsx ```tsx
'use client' 'use client';
export default function Error({ export default function Error({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return ( return (
<div> <div>
<h2>Something went wrong!</h2> <h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button> <button onClick={() => reset()}>Try again</button>
</div> </div>
) );
} }
``` ```
@@ -36,15 +30,9 @@ export default function Error({
Catches errors in root layout: Catches errors in root layout:
```tsx ```tsx
'use client' 'use client';
export default function GlobalError({ export default function GlobalError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return ( return (
<html> <html>
<body> <body>
@@ -52,7 +40,7 @@ export default function GlobalError({
<button onClick={() => reset()}>Try again</button> <button onClick={() => reset()}>Try again</button>
</body> </body>
</html> </html>
) );
} }
``` ```
@@ -107,6 +95,7 @@ async function createPost(formData: FormData) {
``` ```
Same applies to: Same applies to:
- `redirect()` - 307 temporary redirect - `redirect()` - 307 temporary redirect
- `permanentRedirect()` - 308 permanent redirect - `permanentRedirect()` - 308 permanent redirect
- `notFound()` - 404 not found - `notFound()` - 404 not found
@@ -116,15 +105,15 @@ Same applies to:
Use `unstable_rethrow()` to re-throw these errors in catch blocks: Use `unstable_rethrow()` to re-throw these errors in catch blocks:
```tsx ```tsx
import { unstable_rethrow } from 'next/navigation' import { unstable_rethrow } from 'next/navigation';
async function action() { async function action() {
try { try {
// ... // ...
redirect('/success') redirect('/success');
} catch (error) { } catch (error) {
unstable_rethrow(error) // Re-throws Next.js internal errors unstable_rethrow(error); // Re-throws Next.js internal errors
return { error: 'Something went wrong' } return { error: 'Something went wrong' };
} }
} }
``` ```
@@ -132,13 +121,13 @@ async function action() {
## Redirects ## Redirects
```tsx ```tsx
import { redirect, permanentRedirect } from 'next/navigation' import { redirect, permanentRedirect } from 'next/navigation';
// 307 Temporary - use for most cases // 307 Temporary - use for most cases
redirect('/new-path') redirect('/new-path');
// 308 Permanent - use for URL migrations (cached by browsers) // 308 Permanent - use for URL migrations (cached by browsers)
permanentRedirect('/new-url') permanentRedirect('/new-url');
``` ```
## Auth Errors ## Auth Errors
@@ -146,20 +135,20 @@ permanentRedirect('/new-url')
Trigger auth-related error pages: Trigger auth-related error pages:
```tsx ```tsx
import { forbidden, unauthorized } from 'next/navigation' import { forbidden, unauthorized } from 'next/navigation';
async function Page() { async function Page() {
const session = await getSession() const session = await getSession();
if (!session) { if (!session) {
unauthorized() // Renders unauthorized.tsx (401) unauthorized(); // Renders unauthorized.tsx (401)
} }
if (!session.hasAccess) { if (!session.hasAccess) {
forbidden() // Renders forbidden.tsx (403) forbidden(); // Renders forbidden.tsx (403)
} }
return <Dashboard /> return <Dashboard />;
} }
``` ```
@@ -168,12 +157,12 @@ Create corresponding error pages:
```tsx ```tsx
// app/forbidden.tsx // app/forbidden.tsx
export default function Forbidden() { export default function Forbidden() {
return <div>You don't have access to this resource</div> return <div>You don't have access to this resource</div>;
} }
// app/unauthorized.tsx // app/unauthorized.tsx
export default function Unauthorized() { export default function Unauthorized() {
return <div>Please log in to continue</div> return <div>Please log in to continue</div>;
} }
``` ```
@@ -190,24 +179,24 @@ export default function NotFound() {
<h2>Not Found</h2> <h2>Not Found</h2>
<p>Could not find the requested resource</p> <p>Could not find the requested resource</p>
</div> </div>
) );
} }
``` ```
### Triggering Not Found ### Triggering Not Found
```tsx ```tsx
import { notFound } from 'next/navigation' import { notFound } from 'next/navigation';
export default async function Page({ params }: { params: Promise<{ id: string }> }) { export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params const { id } = await params;
const post = await getPost(id) const post = await getPost(id);
if (!post) { if (!post) {
notFound() // Renders closest not-found.tsx notFound(); // Renders closest not-found.tsx
} }
return <div>{post.title}</div> return <div>{post.title}</div>;
} }
``` ```
@@ -27,16 +27,16 @@ app/
## Special Files ## Special Files
| File | Purpose | | File | Purpose |
|------|---------| | --------------- | ---------------------------------------- |
| `page.tsx` | UI for a route segment | | `page.tsx` | UI for a route segment |
| `layout.tsx` | Shared UI for segment and children | | `layout.tsx` | Shared UI for segment and children |
| `loading.tsx` | Loading UI (Suspense boundary) | | `loading.tsx` | Loading UI (Suspense boundary) |
| `error.tsx` | Error UI (Error boundary) | | `error.tsx` | Error UI (Error boundary) |
| `not-found.tsx` | 404 UI | | `not-found.tsx` | 404 UI |
| `route.ts` | API endpoint | | `route.ts` | API endpoint |
| `template.tsx` | Like layout but re-renders on navigation | | `template.tsx` | Like layout but re-renders on navigation |
| `default.tsx` | Fallback for parallel routes | | `default.tsx` | Fallback for parallel routes |
## Route Segments ## Route Segments
@@ -74,6 +74,7 @@ app/
``` ```
Conventions: Conventions:
- `(.)` - same level - `(.)` - same level
- `(..)` - one level up - `(..)` - one level up
- `(..)(..)` - two levels up - `(..)(..)` - two levels up
@@ -128,10 +129,10 @@ export const proxyConfig = {
}; };
``` ```
| Version | File | Export | Config | | Version | File | Export | Config |
|---------|------|--------|--------| | ------- | --------------- | -------------- | ------------- |
| v14-15 | `middleware.ts` | `middleware()` | `config` | | v14-15 | `middleware.ts` | `middleware()` | `config` |
| v16+ | `proxy.ts` | `proxy()` | `proxyConfig` | | v16+ | `proxy.ts` | `proxy()` | `proxyConfig` |
**Migration**: Run `npx @next/codemod@latest upgrade` to auto-rename. **Migration**: Run `npx @next/codemod@latest upgrade` to auto-rename.
+28 -27
View File
@@ -6,44 +6,45 @@ Use `next/font` for automatic font optimization with zero layout shift.
```tsx ```tsx
// app/layout.tsx // app/layout.tsx
import { Inter } from 'next/font/google' import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] }) const inter = Inter({ subsets: ['latin'] });
export default function RootLayout({ children }: { children: React.ReactNode }) { export default function RootLayout({ children }: { children: React.ReactNode }) {
return ( return (
<html lang="en" className={inter.className}> <html lang="en" className={inter.className}>
<body>{children}</body> <body>{children}</body>
</html> </html>
) );
} }
``` ```
## Multiple Fonts ## Multiple Fonts
```tsx ```tsx
import { Inter, Roboto_Mono } from 'next/font/google' import { Inter, Roboto_Mono } from 'next/font/google';
const inter = Inter({ const inter = Inter({
subsets: ['latin'], subsets: ['latin'],
variable: '--font-inter', variable: '--font-inter',
}) });
const robotoMono = Roboto_Mono({ const robotoMono = Roboto_Mono({
subsets: ['latin'], subsets: ['latin'],
variable: '--font-roboto-mono', variable: '--font-roboto-mono',
}) });
export default function RootLayout({ children }: { children: React.ReactNode }) { export default function RootLayout({ children }: { children: React.ReactNode }) {
return ( return (
<html lang="en" className={`${inter.variable} ${robotoMono.variable}`}> <html lang="en" className={`${inter.variable} ${robotoMono.variable}`}>
<body>{children}</body> <body>{children}</body>
</html> </html>
) );
} }
``` ```
Use in CSS: Use in CSS:
```css ```css
body { body {
font-family: var(--font-inter); font-family: var(--font-inter);
@@ -61,35 +62,35 @@ code {
const inter = Inter({ const inter = Inter({
subsets: ['latin'], subsets: ['latin'],
weight: '400', weight: '400',
}) });
// Multiple weights // Multiple weights
const inter = Inter({ const inter = Inter({
subsets: ['latin'], subsets: ['latin'],
weight: ['400', '500', '700'], weight: ['400', '500', '700'],
}) });
// Variable font (recommended) - includes all weights // Variable font (recommended) - includes all weights
const inter = Inter({ const inter = Inter({
subsets: ['latin'], subsets: ['latin'],
// No weight needed - variable fonts support all weights // No weight needed - variable fonts support all weights
}) });
// With italic // With italic
const inter = Inter({ const inter = Inter({
subsets: ['latin'], subsets: ['latin'],
style: ['normal', 'italic'], style: ['normal', 'italic'],
}) });
``` ```
## Local Fonts ## Local Fonts
```tsx ```tsx
import localFont from 'next/font/local' import localFont from 'next/font/local';
const myFont = localFont({ const myFont = localFont({
src: './fonts/MyFont.woff2', src: './fonts/MyFont.woff2',
}) });
// Multiple files for different weights // Multiple files for different weights
const myFont = localFont({ const myFont = localFont({
@@ -105,32 +106,32 @@ const myFont = localFont({
style: 'normal', style: 'normal',
}, },
], ],
}) });
// Variable font // Variable font
const myFont = localFont({ const myFont = localFont({
src: './fonts/MyFont-Variable.woff2', src: './fonts/MyFont-Variable.woff2',
variable: '--font-my-font', variable: '--font-my-font',
}) });
``` ```
## Tailwind CSS Integration ## Tailwind CSS Integration
```tsx ```tsx
// app/layout.tsx // app/layout.tsx
import { Inter } from 'next/font/google' import { Inter } from 'next/font/google';
const inter = Inter({ const inter = Inter({
subsets: ['latin'], subsets: ['latin'],
variable: '--font-inter', variable: '--font-inter',
}) });
export default function RootLayout({ children }) { export default function RootLayout({ children }) {
return ( return (
<html lang="en" className={inter.variable}> <html lang="en" className={inter.variable}>
<body>{children}</body> <body>{children}</body>
</html> </html>
) );
} }
``` ```
@@ -144,7 +145,7 @@ module.exports = {
}, },
}, },
}, },
} };
``` ```
## Preloading Subsets ## Preloading Subsets
@@ -153,10 +154,10 @@ Only load needed character subsets:
```tsx ```tsx
// Latin only (most common) // Latin only (most common)
const inter = Inter({ subsets: ['latin'] }) const inter = Inter({ subsets: ['latin'] });
// Multiple subsets // Multiple subsets
const inter = Inter({ subsets: ['latin', 'latin-ext', 'cyrillic'] }) const inter = Inter({ subsets: ['latin', 'latin-ext', 'cyrillic'] });
``` ```
## Display Strategy ## Display Strategy
@@ -167,7 +168,7 @@ Control font loading behavior:
const inter = Inter({ const inter = Inter({
subsets: ['latin'], subsets: ['latin'],
display: 'swap', // Default - shows fallback, swaps when loaded display: 'swap', // Default - shows fallback, swaps when loaded
}) });
// Options: // Options:
// 'auto' - browser decides // 'auto' - browser decides
@@ -231,15 +232,15 @@ const inter = Inter({ subsets: ['latin'] })
```tsx ```tsx
// For component-specific fonts, export from a shared file // For component-specific fonts, export from a shared file
// lib/fonts.ts // lib/fonts.ts
import { Inter, Playfair_Display } from 'next/font/google' import { Inter, Playfair_Display } from 'next/font/google';
export const inter = Inter({ subsets: ['latin'], variable: '--font-inter' }) export const inter = Inter({ subsets: ['latin'], variable: '--font-inter' });
export const playfair = Playfair_Display({ subsets: ['latin'], variable: '--font-playfair' }) export const playfair = Playfair_Display({ subsets: ['latin'], variable: '--font-playfair' });
// components/Heading.tsx // components/Heading.tsx
import { playfair } from '@/lib/fonts' import { playfair } from '@/lib/fonts';
export function Heading({ children }) { export function Heading({ children }) {
return <h1 className={playfair.className}>{children}</h1> return <h1 className={playfair.className}>{children}</h1>;
} }
``` ```
+44 -44
View File
@@ -6,45 +6,45 @@ Reference: https://nextjs.org/docs/app/api-reference/functions
## Navigation Hooks (Client) ## Navigation Hooks (Client)
| Hook | Purpose | Reference | | Hook | Purpose | Reference |
|------|---------|-----------| | --------------------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| `useRouter` | Programmatic navigation (`push`, `replace`, `back`, `refresh`) | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-router) | | `useRouter` | Programmatic navigation (`push`, `replace`, `back`, `refresh`) | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-router) |
| `usePathname` | Get current pathname | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-pathname) | | `usePathname` | Get current pathname | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-pathname) |
| `useSearchParams` | Read URL search parameters | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-search-params) | | `useSearchParams` | Read URL search parameters | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-search-params) |
| `useParams` | Access dynamic route parameters | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-params) | | `useParams` | Access dynamic route parameters | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-params) |
| `useSelectedLayoutSegment` | Active child segment (one level) | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-selected-layout-segment) | | `useSelectedLayoutSegment` | Active child segment (one level) | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-selected-layout-segment) |
| `useSelectedLayoutSegments` | All active segments below layout | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-selected-layout-segments) | | `useSelectedLayoutSegments` | All active segments below layout | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-selected-layout-segments) |
| `useLinkStatus` | Check link prefetch status | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-link-status) | | `useLinkStatus` | Check link prefetch status | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-link-status) |
| `useReportWebVitals` | Report Core Web Vitals metrics | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-report-web-vitals) | | `useReportWebVitals` | Report Core Web Vitals metrics | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-report-web-vitals) |
## Server Functions ## Server Functions
| Function | Purpose | Reference | | Function | Purpose | Reference |
|----------|---------|-----------| | ------------ | -------------------------------------------- | ---------------------------------------------------------------------- |
| `cookies` | Read/write cookies | [Docs](https://nextjs.org/docs/app/api-reference/functions/cookies) | | `cookies` | Read/write cookies | [Docs](https://nextjs.org/docs/app/api-reference/functions/cookies) |
| `headers` | Read request headers | [Docs](https://nextjs.org/docs/app/api-reference/functions/headers) | | `headers` | Read request headers | [Docs](https://nextjs.org/docs/app/api-reference/functions/headers) |
| `draftMode` | Enable preview of unpublished CMS content | [Docs](https://nextjs.org/docs/app/api-reference/functions/draft-mode) | | `draftMode` | Enable preview of unpublished CMS content | [Docs](https://nextjs.org/docs/app/api-reference/functions/draft-mode) |
| `after` | Run code after response finishes streaming | [Docs](https://nextjs.org/docs/app/api-reference/functions/after) | | `after` | Run code after response finishes streaming | [Docs](https://nextjs.org/docs/app/api-reference/functions/after) |
| `connection` | Wait for connection before dynamic rendering | [Docs](https://nextjs.org/docs/app/api-reference/functions/connection) | | `connection` | Wait for connection before dynamic rendering | [Docs](https://nextjs.org/docs/app/api-reference/functions/connection) |
| `userAgent` | Parse User-Agent header | [Docs](https://nextjs.org/docs/app/api-reference/functions/userAgent) | | `userAgent` | Parse User-Agent header | [Docs](https://nextjs.org/docs/app/api-reference/functions/userAgent) |
## Generate Functions ## Generate Functions
| Function | Purpose | Reference | | Function | Purpose | Reference |
|----------|---------|-----------| | ----------------------- | --------------------------------------- | ----------------------------------------------------------------------------------- |
| `generateStaticParams` | Pre-render dynamic routes at build time | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-static-params) | | `generateStaticParams` | Pre-render dynamic routes at build time | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-static-params) |
| `generateMetadata` | Dynamic metadata | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-metadata) | | `generateMetadata` | Dynamic metadata | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-metadata) |
| `generateViewport` | Dynamic viewport config | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-viewport) | | `generateViewport` | Dynamic viewport config | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-viewport) |
| `generateSitemaps` | Multiple sitemaps for large sites | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-sitemaps) | | `generateSitemaps` | Multiple sitemaps for large sites | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-sitemaps) |
| `generateImageMetadata` | Multiple OG images per route | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-image-metadata) | | `generateImageMetadata` | Multiple OG images per route | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-image-metadata) |
## Request/Response ## Request/Response
| Function | Purpose | Reference | | Function | Purpose | Reference |
|----------|---------|-----------| | --------------- | ------------------------------ | -------------------------------------------------------------------------- |
| `NextRequest` | Extended Request with helpers | [Docs](https://nextjs.org/docs/app/api-reference/functions/next-request) | | `NextRequest` | Extended Request with helpers | [Docs](https://nextjs.org/docs/app/api-reference/functions/next-request) |
| `NextResponse` | Extended Response with helpers | [Docs](https://nextjs.org/docs/app/api-reference/functions/next-response) | | `NextResponse` | Extended Response with helpers | [Docs](https://nextjs.org/docs/app/api-reference/functions/next-response) |
| `ImageResponse` | Generate OG images | [Docs](https://nextjs.org/docs/app/api-reference/functions/image-response) | | `ImageResponse` | Generate OG images | [Docs](https://nextjs.org/docs/app/api-reference/functions/image-response) |
## Common Examples ## Common Examples
@@ -54,30 +54,30 @@ Use `next/link` for internal navigation instead of `<a>` tags.
```tsx ```tsx
// Bad: Plain anchor tag // Bad: Plain anchor tag
<a href="/about">About</a> <a href="/about">About</a>;
// Good: Next.js Link // Good: Next.js Link
import Link from 'next/link' import Link from 'next/link';
<Link href="/about">About</Link> <Link href="/about">About</Link>;
``` ```
Active link styling: Active link styling:
```tsx ```tsx
'use client' 'use client';
import Link from 'next/link' import Link from 'next/link';
import { usePathname } from 'next/navigation' import { usePathname } from 'next/navigation';
export function NavLink({ href, children }) { export function NavLink({ href, children }) {
const pathname = usePathname() const pathname = usePathname();
return ( return (
<Link href={href} className={pathname === href ? 'active' : ''}> <Link href={href} className={pathname === href ? 'active' : ''}>
{children} {children}
</Link> </Link>
) );
} }
``` ```
@@ -86,23 +86,23 @@ export function NavLink({ href, children }) {
```tsx ```tsx
// app/blog/[slug]/page.tsx // app/blog/[slug]/page.tsx
export async function generateStaticParams() { export async function generateStaticParams() {
const posts = await getPosts() const posts = await getPosts();
return posts.map((post) => ({ slug: post.slug })) return posts.map((post) => ({ slug: post.slug }));
} }
``` ```
### After Response ### After Response
```tsx ```tsx
import { after } from 'next/server' import { after } from 'next/server';
export async function POST(request: Request) { export async function POST(request: Request) {
const data = await processRequest(request) const data = await processRequest(request);
after(async () => { after(async () => {
await logAnalytics(data) await logAnalytics(data);
}) });
return Response.json({ success: true }) return Response.json({ success: true });
} }
``` ```
@@ -17,16 +17,16 @@ In development, click the hydration error to see the server/client diff.
```tsx ```tsx
// Bad: Causes mismatch - window doesn't exist on server // Bad: Causes mismatch - window doesn't exist on server
<div>{window.innerWidth}</div> <div>{window.innerWidth}</div>;
// Good: Use client component with mounted check // Good: Use client component with mounted check
'use client' ('use client');
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react';
export function ClientOnly({ children }: { children: React.ReactNode }) { export function ClientOnly({ children }: { children: React.ReactNode }) {
const [mounted, setMounted] = useState(false) const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []) useEffect(() => setMounted(true), []);
return mounted ? children : null return mounted ? children : null;
} }
``` ```
@@ -36,12 +36,12 @@ Server and client may be in different timezones:
```tsx ```tsx
// Bad: Causes mismatch // Bad: Causes mismatch
<span>{new Date().toLocaleString()}</span> <span>{new Date().toLocaleString()}</span>;
// Good: Render on client only // Good: Render on client only
'use client' ('use client');
const [time, setTime] = useState<string>() const [time, setTime] = useState<string>();
useEffect(() => setTime(new Date().toLocaleString()), []) useEffect(() => setTime(new Date().toLocaleString()), []);
``` ```
### Random Values or IDs ### Random Values or IDs
@@ -78,14 +78,9 @@ Scripts that modify DOM during hydration.
```tsx ```tsx
// Good: Use next/script with afterInteractive // Good: Use next/script with afterInteractive
import Script from 'next/script' import Script from 'next/script';
export default function Page() { export default function Page() {
return ( return <Script src="https://example.com/script.js" strategy="afterInteractive" />;
<Script
src="https://example.com/script.js"
strategy="afterInteractive"
/>
)
} }
``` ```
+9 -9
View File
@@ -6,11 +6,11 @@ Use `next/image` for automatic image optimization.
```tsx ```tsx
// Bad: Avoid native img // Bad: Avoid native img
<img src="/hero.png" alt="Hero" /> <img src="/hero.png" alt="Hero" />;
// Good: Use next/image // Good: Use next/image
import Image from 'next/image' import Image from 'next/image';
<Image src="/hero.png" alt="Hero" width={800} height={400} /> <Image src="/hero.png" alt="Hero" width={800} height={400} />;
``` ```
## Required Props ## Required Props
@@ -51,7 +51,7 @@ module.exports = {
}, },
], ],
}, },
} };
``` ```
## Responsive Images ## Responsive Images
@@ -155,19 +155,19 @@ When using `output: 'export'`, use `unoptimized` or custom loader:
```tsx ```tsx
// Option 1: Disable optimization // Option 1: Disable optimization
<Image src="/hero.png" alt="Hero" width={800} height={400} unoptimized /> <Image src="/hero.png" alt="Hero" width={800} height={400} unoptimized />;
// Option 2: Global config // Option 2: Global config
// next.config.js // next.config.js
module.exports = { module.exports = {
output: 'export', output: 'export',
images: { unoptimized: true }, images: { unoptimized: true },
} };
// Option 3: Custom loader (Cloudinary, Imgix, etc.) // Option 3: Custom loader (Cloudinary, Imgix, etc.)
const cloudinaryLoader = ({ src, width, quality }) => { const cloudinaryLoader = ({ src, width, quality }) => {
return `https://res.cloudinary.com/demo/image/upload/w_${width},q_${quality || 75}/${src}` return `https://res.cloudinary.com/demo/image/upload/w_${width},q_${quality || 75}/${src}`;
} };
<Image loader={cloudinaryLoader} src="sample.jpg" alt="Sample" width={800} height={400} /> <Image loader={cloudinaryLoader} src="sample.jpg" alt="Sample" width={800} height={400} />;
``` ```
+64 -72
View File
@@ -7,6 +7,7 @@ Add SEO metadata to Next.js pages using the Metadata API.
The `metadata` object and `generateMetadata` function are **only supported in Server Components**. They cannot be used in Client Components. The `metadata` object and `generateMetadata` function are **only supported in Server Components**. They cannot be used in Client Components.
If the target page has `'use client'`: If the target page has `'use client'`:
1. Remove `'use client'` if possible, move client logic to child components 1. Remove `'use client'` if possible, move client logic to child components
2. Or extract metadata to a parent Server Component layout 2. Or extract metadata to a parent Server Component layout
3. Or split the file: Server Component with metadata imports Client Components 3. Or split the file: Server Component with metadata imports Client Components
@@ -14,25 +15,25 @@ If the target page has `'use client'`:
## Static Metadata ## Static Metadata
```tsx ```tsx
import type { Metadata } from 'next' import type { Metadata } from 'next';
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Page Title', title: 'Page Title',
description: 'Page description for search engines', description: 'Page description for search engines',
} };
``` ```
## Dynamic Metadata ## Dynamic Metadata
```tsx ```tsx
import type { Metadata } from 'next' import type { Metadata } from 'next';
type Props = { params: Promise<{ slug: string }> } type Props = { params: Promise<{ slug: string }> };
export async function generateMetadata({ params }: Props): Promise<Metadata> { export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params const { slug } = await params;
const post = await getPost(slug) const post = await getPost(slug);
return { title: post.title, description: post.description } return { title: post.title, description: post.description };
} }
``` ```
@@ -41,11 +42,11 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
Use React `cache()` when the same data is needed for both metadata and page: Use React `cache()` when the same data is needed for both metadata and page:
```tsx ```tsx
import { cache } from 'react' import { cache } from 'react';
export const getPost = cache(async (slug: string) => { export const getPost = cache(async (slug: string) => {
return await db.posts.findFirst({ where: { slug } }) return await db.posts.findFirst({ where: { slug } });
}) });
``` ```
## Viewport ## Viewport
@@ -53,17 +54,17 @@ export const getPost = cache(async (slug: string) => {
Separate from metadata for streaming support: Separate from metadata for streaming support:
```tsx ```tsx
import type { Viewport } from 'next' import type { Viewport } from 'next';
export const viewport: Viewport = { export const viewport: Viewport = {
width: 'device-width', width: 'device-width',
initialScale: 1, initialScale: 1,
themeColor: '#000000', themeColor: '#000000',
} };
// Or dynamic // Or dynamic
export function generateViewport({ params }): Viewport { export function generateViewport({ params }): Viewport {
return { themeColor: getThemeColor(params) } return { themeColor: getThemeColor(params) };
} }
``` ```
@@ -74,7 +75,7 @@ In root layout for consistent naming:
```tsx ```tsx
export const metadata: Metadata = { export const metadata: Metadata = {
title: { default: 'Site Name', template: '%s | Site Name' }, title: { default: 'Site Name', template: '%s | Site Name' },
} };
``` ```
## Metadata File Conventions ## Metadata File Conventions
@@ -83,16 +84,16 @@ Reference: https://nextjs.org/docs/app/getting-started/project-structure#metadat
Place these files in `app/` directory (or route segments): Place these files in `app/` directory (or route segments):
| File | Purpose | | File | Purpose |
|------|---------| | ------------------------------- | --------------------------------------------- |
| `favicon.ico` | Favicon | | `favicon.ico` | Favicon |
| `icon.png` / `icon.svg` | App icon | | `icon.png` / `icon.svg` | App icon |
| `apple-icon.png` | Apple app icon | | `apple-icon.png` | Apple app icon |
| `opengraph-image.png` | OG image | | `opengraph-image.png` | OG image |
| `twitter-image.png` | Twitter card image | | `twitter-image.png` | Twitter card image |
| `sitemap.ts` / `sitemap.xml` | Sitemap (use `generateSitemaps` for multiple) | | `sitemap.ts` / `sitemap.xml` | Sitemap (use `generateSitemaps` for multiple) |
| `robots.ts` / `robots.txt` | Robots directives | | `robots.ts` / `robots.txt` | Robots directives |
| `manifest.ts` / `manifest.json` | Web app manifest | | `manifest.ts` / `manifest.json` | Web app manifest |
## SEO Best Practice: Static Files Are Often Enough ## SEO Best Practice: Static Files Are Often Enough
@@ -108,6 +109,7 @@ app/
``` ```
**Tips:** **Tips:**
- A single `opengraph-image.png` covers both Open Graph and Twitter (Twitter falls back to OG) - A single `opengraph-image.png` covers both Open Graph and Twitter (Twitter falls back to OG)
- Static `title` and `description` in layout metadata is sufficient for most pages - Static `title` and `description` in layout metadata is sufficient for most pages
- Only use dynamic `generateMetadata` when content varies per page - Only use dynamic `generateMetadata` when content varies per page
@@ -126,7 +128,7 @@ Generate dynamic Open Graph images using `next/og`.
```tsx ```tsx
// Good // Good
import { ImageResponse } from 'next/og' import { ImageResponse } from 'next/og';
// Bad // Bad
// import { ImageResponse } from '@vercel/og' // import { ImageResponse } from '@vercel/og'
@@ -137,11 +139,11 @@ import { ImageResponse } from 'next/og'
```tsx ```tsx
// app/opengraph-image.tsx // app/opengraph-image.tsx
import { ImageResponse } from 'next/og' import { ImageResponse } from 'next/og';
export const alt = 'Site Name' export const alt = 'Site Name';
export const size = { width: 1200, height: 630 } export const size = { width: 1200, height: 630 };
export const contentType = 'image/png' export const contentType = 'image/png';
export default function Image() { export default function Image() {
return new ImageResponse( return new ImageResponse(
@@ -161,7 +163,7 @@ export default function Image() {
</div> </div>
), ),
{ ...size } { ...size }
) );
} }
``` ```
@@ -169,17 +171,17 @@ export default function Image() {
```tsx ```tsx
// app/blog/[slug]/opengraph-image.tsx // app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og' import { ImageResponse } from 'next/og';
export const alt = 'Blog Post' export const alt = 'Blog Post';
export const size = { width: 1200, height: 630 } export const size = { width: 1200, height: 630 };
export const contentType = 'image/png' export const contentType = 'image/png';
type Props = { params: Promise<{ slug: string }> } type Props = { params: Promise<{ slug: string }> };
export default async function Image({ params }: Props) { export default async function Image({ params }: Props) {
const { slug } = await params const { slug } = await params;
const post = await getPost(slug) const post = await getPost(slug);
return new ImageResponse( return new ImageResponse(
( (
@@ -202,33 +204,26 @@ export default async function Image({ params }: Props) {
</div> </div>
), ),
{ ...size } { ...size }
) );
} }
``` ```
## Custom Fonts ## Custom Fonts
```tsx ```tsx
import { ImageResponse } from 'next/og' import { ImageResponse } from 'next/og';
import { join } from 'path' import { join } from 'path';
import { readFile } from 'fs/promises' import { readFile } from 'fs/promises';
export default async function Image() { export default async function Image() {
const fontPath = join(process.cwd(), 'assets/fonts/Inter-Bold.ttf') const fontPath = join(process.cwd(), 'assets/fonts/Inter-Bold.ttf');
const fontData = await readFile(fontPath) const fontData = await readFile(fontPath);
return new ImageResponse( return new ImageResponse(<div style={{ fontFamily: 'Inter', fontSize: 64 }}>Custom Font Text</div>, {
( width: 1200,
<div style={{ fontFamily: 'Inter', fontSize: 64 }}> height: 630,
Custom Font Text fonts: [{ name: 'Inter', data: fontData, style: 'normal' }],
</div> });
),
{
width: 1200,
height: 630,
fonts: [{ name: 'Inter', data: fontData, style: 'normal' }],
}
)
} }
``` ```
@@ -240,6 +235,7 @@ export default async function Image() {
## Styling Notes ## Styling Notes
ImageResponse uses Flexbox layout: ImageResponse uses Flexbox layout:
- Use `display: 'flex'` - Use `display: 'flex'`
- No CSS Grid support - No CSS Grid support
- Styles must be inline objects - Styles must be inline objects
@@ -250,22 +246,22 @@ Use `generateImageMetadata` for multiple images per route:
```tsx ```tsx
// app/blog/[slug]/opengraph-image.tsx // app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og' import { ImageResponse } from 'next/og';
export async function generateImageMetadata({ params }) { export async function generateImageMetadata({ params }) {
const images = await getPostImages(params.slug) const images = await getPostImages(params.slug);
return images.map((img, idx) => ({ return images.map((img, idx) => ({
id: idx, id: idx,
alt: img.alt, alt: img.alt,
size: { width: 1200, height: 630 }, size: { width: 1200, height: 630 },
contentType: 'image/png', contentType: 'image/png',
})) }));
} }
export default async function Image({ params, id }) { export default async function Image({ params, id }) {
const images = await getPostImages(params.slug) const images = await getPostImages(params.slug);
const image = images[id] const image = images[id];
return new ImageResponse(/* ... */) return new ImageResponse(/* ... */);
} }
``` ```
@@ -275,26 +271,22 @@ Use `generateSitemaps` for large sites:
```tsx ```tsx
// app/sitemap.ts // app/sitemap.ts
import type { MetadataRoute } from 'next' import type { MetadataRoute } from 'next';
export async function generateSitemaps() { export async function generateSitemaps() {
// Return array of sitemap IDs // Return array of sitemap IDs
return [{ id: 0 }, { id: 1 }, { id: 2 }] return [{ id: 0 }, { id: 1 }, { id: 2 }];
} }
export default async function sitemap({ export default async function sitemap({ id }: { id: number }): Promise<MetadataRoute.Sitemap> {
id, const start = id * 50000;
}: { const end = start + 50000;
id: number const products = await getProducts(start, end);
}): Promise<MetadataRoute.Sitemap> {
const start = id * 50000
const end = start + 50000
const products = await getProducts(start, end)
return products.map((product) => ({ return products.map((product) => ({
url: `https://example.com/product/${product.id}`, url: `https://example.com/product/${product.id}`,
lastModified: product.updatedAt, lastModified: product.updatedAt,
})) }));
} }
``` ```
@@ -24,13 +24,7 @@ app/
```tsx ```tsx
// app/layout.tsx // app/layout.tsx
export default function RootLayout({ export default function RootLayout({ children, modal }: { children: React.ReactNode; modal: React.ReactNode }) {
children,
modal,
}: {
children: React.ReactNode;
modal: React.ReactNode;
}) {
return ( return (
<html> <html>
<body> <body>
@@ -63,11 +57,7 @@ The `(.)` prefix intercepts routes at the same level.
// app/@modal/(.)photos/[id]/page.tsx // app/@modal/(.)photos/[id]/page.tsx
import { Modal } from '@/components/modal'; import { Modal } from '@/components/modal';
export default async function PhotoModal({ export default async function PhotoModal({ params }: { params: Promise<{ id: string }> }) {
params
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params; const { id } = await params;
const photo = await getPhoto(id); const photo = await getPhoto(id);
@@ -83,11 +73,7 @@ export default async function PhotoModal({
```tsx ```tsx
// app/photos/[id]/page.tsx // app/photos/[id]/page.tsx
export default async function PhotoPage({ export default async function PhotoPage({ params }: { params: Promise<{ id: string }> }) {
params
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params; const { id } = await params;
const photo = await getPhoto(id); const photo = await getPhoto(id);
@@ -127,11 +113,14 @@ export function Modal({ children }: { children: React.ReactNode }) {
}, [router]); }, [router]);
// Close on overlay click // Close on overlay click
const handleOverlayClick = useCallback((e: React.MouseEvent) => { const handleOverlayClick = useCallback(
if (e.target === overlayRef.current) { (e: React.MouseEvent) => {
router.back(); // Correct if (e.target === overlayRef.current) {
} router.back(); // Correct
}, [router]); }
},
[router]
);
return ( return (
<div <div
@@ -156,11 +145,13 @@ export function Modal({ children }: { children: React.ReactNode }) {
### Why NOT `router.push('/')` or `<Link href="/">`? ### Why NOT `router.push('/')` or `<Link href="/">`?
Using `push` or `Link` to "close" a modal: Using `push` or `Link` to "close" a modal:
1. Adds a new history entry (back button shows modal again) 1. Adds a new history entry (back button shows modal again)
2. Doesn't properly clear the intercepted route 2. Doesn't properly clear the intercepted route
3. Can cause the modal to flash or persist unexpectedly 3. Can cause the modal to flash or persist unexpectedly
`router.back()` correctly: `router.back()` correctly:
1. Removes the intercepted route from history 1. Removes the intercepted route from history
2. Returns to the previous page 2. Returns to the previous page
3. Properly unmounts the modal 3. Properly unmounts the modal
@@ -169,18 +160,19 @@ Using `push` or `Link` to "close" a modal:
Matchers match **route segments**, not filesystem paths: Matchers match **route segments**, not filesystem paths:
| Matcher | Matches | Example | | Matcher | Matches | Example |
|---------|---------|---------| | ---------- | ------------- | --------------------------------------------------------------------- |
| `(.)` | Same level | `@modal/(.)photos` intercepts `/photos` | | `(.)` | Same level | `@modal/(.)photos` intercepts `/photos` |
| `(..)` | One level up | `@modal/(..)settings` from `/dashboard/@modal` intercepts `/settings` | | `(..)` | One level up | `@modal/(..)settings` from `/dashboard/@modal` intercepts `/settings` |
| `(..)(..)` | Two levels up | Rarely used | | `(..)(..)` | Two levels up | Rarely used |
| `(...)` | From root | `@modal/(...)photos` intercepts `/photos` from anywhere | | `(...)` | From root | `@modal/(...)photos` intercepts `/photos` from anywhere |
**Common mistake**: Thinking `(..)` means "parent folder" - it means "parent route segment". **Common mistake**: Thinking `(..)` means "parent folder" - it means "parent route segment".
## Handling Hard Navigation ## Handling Hard Navigation
When users directly visit `/photos/123` (bookmark, refresh, shared link): When users directly visit `/photos/123` (bookmark, refresh, shared link):
- The intercepting route is bypassed - The intercepting route is bypassed
- The full `photos/[id]/page.tsx` renders - The full `photos/[id]/page.tsx` renders
- Modal doesn't appear (expected behavior) - Modal doesn't appear (expected behavior)
@@ -230,6 +222,7 @@ app/
### 4. Intercepted Route Shows Wrong Content ### 4. Intercepted Route Shows Wrong Content
Check your matcher: Check your matcher:
- `(.)photos` intercepts `/photos` from the same route level - `(.)photos` intercepts `/photos` from the same route level
- If your `@modal` is in `app/dashboard/@modal`, use `(.)photos` to intercept `/dashboard/photos`, not `/photos` - If your `@modal` is in `app/dashboard/@modal`, use `(.)photos` to intercept `/dashboard/photos`, not `/photos`
@@ -272,7 +265,7 @@ export default async function Gallery() {
return ( return (
<div className="grid grid-cols-3 gap-4"> <div className="grid grid-cols-3 gap-4">
{photos.map(photo => ( {photos.map((photo) => (
<Link key={photo.id} href={`/photos/${photo.id}`}> <Link key={photo.id} href={`/photos/${photo.id}`}>
<img src={photo.thumbnail} alt={photo.title} /> <img src={photo.thumbnail} alt={photo.title} />
</Link> </Link>
@@ -7,14 +7,14 @@ Create API endpoints with `route.ts` files.
```tsx ```tsx
// app/api/users/route.ts // app/api/users/route.ts
export async function GET() { export async function GET() {
const users = await getUsers() const users = await getUsers();
return Response.json(users) return Response.json(users);
} }
export async function POST(request: Request) { export async function POST(request: Request) {
const body = await request.json() const body = await request.json();
const user = await createUser(body) const user = await createUser(body);
return Response.json(user, { status: 201 }) return Response.json(user, { status: 201 });
} }
``` ```
@@ -60,11 +60,11 @@ Route handlers run in a **Server Component-like environment**:
```tsx ```tsx
// Bad: This won't work - no React DOM in route handlers // Bad: This won't work - no React DOM in route handlers
import { renderToString } from 'react-dom/server' import { renderToString } from 'react-dom/server';
export async function GET() { export async function GET() {
const html = renderToString(<Component />) // Error! const html = renderToString(<Component />); // Error!
return new Response(html) return new Response(html);
} }
``` ```
@@ -72,18 +72,15 @@ export async function GET() {
```tsx ```tsx
// app/api/users/[id]/route.ts // app/api/users/[id]/route.ts
export async function GET( export async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) {
request: Request, const { id } = await params;
{ params }: { params: Promise<{ id: string }> } const user = await getUser(id);
) {
const { id } = await params
const user = await getUser(id)
if (!user) { if (!user) {
return Response.json({ error: 'Not found' }, { status: 404 }) return Response.json({ error: 'Not found' }, { status: 404 });
} }
return Response.json(user) return Response.json(user);
} }
``` ```
@@ -92,17 +89,17 @@ export async function GET(
```tsx ```tsx
export async function GET(request: Request) { export async function GET(request: Request) {
// URL and search params // URL and search params
const { searchParams } = new URL(request.url) const { searchParams } = new URL(request.url);
const query = searchParams.get('q') const query = searchParams.get('q');
// Headers // Headers
const authHeader = request.headers.get('authorization') const authHeader = request.headers.get('authorization');
// Cookies (Next.js helper) // Cookies (Next.js helper)
const cookieStore = await cookies() const cookieStore = await cookies();
const token = cookieStore.get('token') const token = cookieStore.get('token');
return Response.json({ query, token }) return Response.json({ query, token });
} }
``` ```
@@ -110,37 +107,37 @@ export async function GET(request: Request) {
```tsx ```tsx
// JSON response // JSON response
return Response.json({ data }) return Response.json({ data });
// With status // With status
return Response.json({ error: 'Not found' }, { status: 404 }) return Response.json({ error: 'Not found' }, { status: 404 });
// With headers // With headers
return Response.json(data, { return Response.json(data, {
headers: { headers: {
'Cache-Control': 'max-age=3600', 'Cache-Control': 'max-age=3600',
}, },
}) });
// Redirect // Redirect
return Response.redirect(new URL('/login', request.url)) return Response.redirect(new URL('/login', request.url));
// Stream // Stream
return new Response(stream, { return new Response(stream, {
headers: { 'Content-Type': 'text/event-stream' }, headers: { 'Content-Type': 'text/event-stream' },
}) });
``` ```
## When to Use Route Handlers vs Server Actions ## When to Use Route Handlers vs Server Actions
| Use Case | Route Handlers | Server Actions | | Use Case | Route Handlers | Server Actions |
|----------|----------------|----------------| | ------------------------ | -------------- | -------------- |
| Form submissions | No | Yes | | Form submissions | No | Yes |
| Data mutations from UI | No | Yes | | Data mutations from UI | No | Yes |
| Third-party webhooks | Yes | No | | Third-party webhooks | Yes | No |
| External API consumption | Yes | No | | External API consumption | Yes | No |
| Public REST API | Yes | No | | Public REST API | Yes | No |
| File uploads | Both work | Both work | | File uploads | Both work | Both work |
**Prefer Server Actions** for mutations triggered from your UI. **Prefer Server Actions** for mutations triggered from your UI.
**Use Route Handlers** for external integrations and public APIs. **Use Route Handlers** for external integrations and public APIs.
@@ -12,33 +12,33 @@ Client components **cannot** be async functions. Only Server Components can be a
```tsx ```tsx
// Bad: async client component // Bad: async client component
'use client' 'use client';
export default async function UserProfile() { export default async function UserProfile() {
const user = await getUser() // Cannot await in client component const user = await getUser(); // Cannot await in client component
return <div>{user.name}</div> return <div>{user.name}</div>;
} }
// Good: Remove async, fetch data in parent server component // Good: Remove async, fetch data in parent server component
// page.tsx (server component - no 'use client') // page.tsx (server component - no 'use client')
export default async function Page() { export default async function Page() {
const user = await getUser() const user = await getUser();
return <UserProfile user={user} /> return <UserProfile user={user} />;
} }
// UserProfile.tsx (client component) // UserProfile.tsx (client component)
'use client' ('use client');
export function UserProfile({ user }: { user: User }) { export function UserProfile({ user }: { user: User }) {
return <div>{user.name}</div> return <div>{user.name}</div>;
} }
``` ```
```tsx ```tsx
// Bad: async arrow function client component // Bad: async arrow function client component
'use client' 'use client';
const Dashboard = async () => { const Dashboard = async () => {
const data = await fetchDashboard() const data = await fetchDashboard();
return <div>{data}</div> return <div>{data}</div>;
} };
// Good: Fetch in server component, pass data down // Good: Fetch in server component, pass data down
``` ```
@@ -48,6 +48,7 @@ const Dashboard = async () => {
Props passed from Server → Client must be JSON-serializable. Props passed from Server → Client must be JSON-serializable.
**Detect:** Server component passes these to a client component: **Detect:** Server component passes these to a client component:
- Functions (except Server Actions with `'use server'`) - Functions (except Server Actions with `'use server'`)
- `Date` objects - `Date` objects
- `Map`, `Set`, `WeakMap`, `WeakSet` - `Map`, `Set`, `WeakMap`, `WeakSet`
@@ -59,16 +60,16 @@ Props passed from Server → Client must be JSON-serializable.
// Bad: Function prop // Bad: Function prop
// page.tsx (server) // page.tsx (server)
export default function Page() { export default function Page() {
const handleClick = () => console.log('clicked') const handleClick = () => console.log('clicked');
return <ClientButton onClick={handleClick} /> return <ClientButton onClick={handleClick} />;
} }
// Good: Define function inside client component // Good: Define function inside client component
// ClientButton.tsx // ClientButton.tsx
'use client' ('use client');
export function ClientButton() { export function ClientButton() {
const handleClick = () => console.log('clicked') const handleClick = () => console.log('clicked');
return <button onClick={handleClick}>Click</button> return <button onClick={handleClick}>Click</button>;
} }
``` ```
@@ -76,28 +77,28 @@ export function ClientButton() {
// Bad: Date object (silently becomes string, then crashes) // Bad: Date object (silently becomes string, then crashes)
// page.tsx (server) // page.tsx (server)
export default async function Page() { export default async function Page() {
const post = await getPost() const post = await getPost();
return <PostCard createdAt={post.createdAt} /> // Date object return <PostCard createdAt={post.createdAt} />; // Date object
} }
// PostCard.tsx (client) - will crash on .getFullYear() // PostCard.tsx (client) - will crash on .getFullYear()
'use client' ('use client');
export function PostCard({ createdAt }: { createdAt: Date }) { export function PostCard({ createdAt }: { createdAt: Date }) {
return <span>{createdAt.getFullYear()}</span> // Runtime error! return <span>{createdAt.getFullYear()}</span>; // Runtime error!
} }
// Good: Serialize to string on server // Good: Serialize to string on server
// page.tsx (server) // page.tsx (server)
export default async function Page() { export default async function Page() {
const post = await getPost() const post = await getPost();
return <PostCard createdAt={post.createdAt.toISOString()} /> return <PostCard createdAt={post.createdAt.toISOString()} />;
} }
// PostCard.tsx (client) // PostCard.tsx (client)
'use client' ('use client');
export function PostCard({ createdAt }: { createdAt: string }) { export function PostCard({ createdAt }: { createdAt: string }) {
const date = new Date(createdAt) const date = new Date(createdAt);
return <span>{date.getFullYear()}</span> return <span>{date.getFullYear()}</span>;
} }
``` ```
@@ -127,33 +128,33 @@ Functions marked with `'use server'` CAN be passed to client components.
```tsx ```tsx
// Valid: Server Action can be passed // Valid: Server Action can be passed
// actions.ts // actions.ts
'use server' 'use server';
export async function submitForm(formData: FormData) { export async function submitForm(formData: FormData) {
// server-side logic // server-side logic
} }
// page.tsx (server) // page.tsx (server)
import { submitForm } from './actions' import { submitForm } from './actions';
export default function Page() { export default function Page() {
return <ClientForm onSubmit={submitForm} /> // OK! return <ClientForm onSubmit={submitForm} />; // OK!
} }
// ClientForm.tsx (client) // ClientForm.tsx (client)
'use client' ('use client');
export function ClientForm({ onSubmit }: { onSubmit: (data: FormData) => Promise<void> }) { export function ClientForm({ onSubmit }: { onSubmit: (data: FormData) => Promise<void> }) {
return <form action={onSubmit}>...</form> return <form action={onSubmit}>...</form>;
} }
``` ```
## Quick Reference ## Quick Reference
| Pattern | Valid? | Fix | | Pattern | Valid? | Fix |
|---------|--------|-----| | --------------------------------- | ------ | ------------------------------------- |
| `'use client'` + `async function` | No | Fetch in server parent, pass data | | `'use client'` + `async function` | No | Fetch in server parent, pass data |
| Pass `() => {}` to client | No | Define in client or use server action | | Pass `() => {}` to client | No | Define in client or use server action |
| Pass `new Date()` to client | No | Use `.toISOString()` | | Pass `new Date()` to client | No | Use `.toISOString()` |
| Pass `new Map()` to client | No | Convert to object/array | | Pass `new Map()` to client | No | Convert to object/array |
| Pass class instance to client | No | Pass plain object | | Pass class instance to client | No | Pass plain object |
| Pass server action to client | Yes | - | | Pass server action to client | Yes | - |
| Pass `string/number/boolean` | Yes | - | | Pass `string/number/boolean` | Yes | - |
| Pass plain object/array | Yes | - | | Pass plain object/array | Yes | - |
@@ -32,6 +32,7 @@ export const runtime = 'edge'
## Detection ## Detection
**Before adding `runtime = 'edge'`**, check: **Before adding `runtime = 'edge'`**, check:
1. Does the project already use Edge runtime? 1. Does the project already use Edge runtime?
2. Is there a specific latency requirement? 2. Is there a specific latency requirement?
3. Are all dependencies Edge-compatible? 3. Are all dependencies Edge-compatible?
+16 -20
View File
@@ -8,12 +8,12 @@ Always use `next/script` instead of native `<script>` tags for better performanc
```tsx ```tsx
// Bad: Native script tag // Bad: Native script tag
<script src="https://example.com/script.js"></script> <script src="https://example.com/script.js"></script>;
// Good: Next.js Script component // Good: Next.js Script component
import Script from 'next/script' import Script from 'next/script';
<Script src="https://example.com/script.js" /> <Script src="https://example.com/script.js" />;
``` ```
## Inline Scripts Need ID ## Inline Scripts Need ID
@@ -100,7 +100,7 @@ export default function Layout({ children }) {
## Google Tag Manager ## Google Tag Manager
```tsx ```tsx
import { GoogleTagManager } from '@next/third-parties/google' import { GoogleTagManager } from '@next/third-parties/google';
export default function Layout({ children }) { export default function Layout({ children }) {
return ( return (
@@ -108,7 +108,7 @@ export default function Layout({ children }) {
<GoogleTagManager gtmId="GTM-XXXXX" /> <GoogleTagManager gtmId="GTM-XXXXX" />
<body>{children}</body> <body>{children}</body>
</html> </html>
) );
} }
``` ```
@@ -116,26 +116,22 @@ export default function Layout({ children }) {
```tsx ```tsx
// YouTube embed // YouTube embed
import { YouTubeEmbed } from '@next/third-parties/google' import { YouTubeEmbed } from '@next/third-parties/google';
<YouTubeEmbed videoid="dQw4w9WgXcQ" /> <YouTubeEmbed videoid="dQw4w9WgXcQ" />;
// Google Maps // Google Maps
import { GoogleMapsEmbed } from '@next/third-parties/google' import { GoogleMapsEmbed } from '@next/third-parties/google';
<GoogleMapsEmbed <GoogleMapsEmbed apiKey="YOUR_API_KEY" mode="place" q="Brooklyn+Bridge,New+York,NY" />;
apiKey="YOUR_API_KEY"
mode="place"
q="Brooklyn+Bridge,New+York,NY"
/>
``` ```
## Quick Reference ## Quick Reference
| Pattern | Issue | Fix | | Pattern | Issue | Fix |
|---------|-------|-----| | --------------------------------------------- | -------------------------- | ------------------------- |
| `<script src="...">` | No optimization | Use `next/script` | | `<script src="...">` | No optimization | Use `next/script` |
| `<Script>` without id | Can't track inline scripts | Add `id` attribute | | `<Script>` without id | Can't track inline scripts | Add `id` attribute |
| `<Script>` inside `<Head>` | Wrong placement | Move outside Head | | `<Script>` inside `<Head>` | Wrong placement | Move outside Head |
| Inline GA/GTM scripts | No optimization | Use `@next/third-parties` | | Inline GA/GTM scripts | No optimization | Use `@next/third-parties` |
| `strategy="beforeInteractive"` outside layout | Won't work | Only use in root layout | | `strategy="beforeInteractive"` outside layout | Won't work | Only use in root layout |
@@ -77,12 +77,12 @@ services:
web: web:
build: . build: .
ports: ports:
- "3000:3000" - '3000:3000'
environment: environment:
- NODE_ENV=production - NODE_ENV=production
restart: unless-stopped restart: unless-stopped
healthcheck: healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000/api/health"] test: ['CMD', 'wget', '-q', '--spider', 'http://localhost:3000/api/health']
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 3 retries: 3
@@ -95,16 +95,18 @@ For traditional server deployments:
```js ```js
// ecosystem.config.js // ecosystem.config.js
module.exports = { module.exports = {
apps: [{ apps: [
name: 'nextjs', {
script: '.next/standalone/server.js', name: 'nextjs',
instances: 'max', script: '.next/standalone/server.js',
exec_mode: 'cluster', instances: 'max',
env: { exec_mode: 'cluster',
NODE_ENV: 'production', env: {
PORT: 3000, NODE_ENV: 'production',
PORT: 3000,
},
}, },
}], ],
}; };
``` ```
@@ -168,11 +170,7 @@ module.exports = class CacheHandler {
// Set TTL based on revalidate option // Set TTL based on revalidate option
if (ctx?.revalidate) { if (ctx?.revalidate) {
await redis.setex( await redis.setex(CACHE_PREFIX + key, ctx.revalidate, JSON.stringify(cacheData));
CACHE_PREFIX + key,
ctx.revalidate,
JSON.stringify(cacheData)
);
} else { } else {
await redis.set(CACHE_PREFIX + key, JSON.stringify(cacheData)); await redis.set(CACHE_PREFIX + key, JSON.stringify(cacheData));
} }
@@ -197,10 +195,12 @@ const BUCKET = process.env.CACHE_BUCKET;
module.exports = class CacheHandler { module.exports = class CacheHandler {
async get(key) { async get(key) {
try { try {
const response = await s3.send(new GetObjectCommand({ const response = await s3.send(
Bucket: BUCKET, new GetObjectCommand({
Key: `cache/${key}`, Bucket: BUCKET,
})); Key: `cache/${key}`,
})
);
const body = await response.Body.transformToString(); const body = await response.Body.transformToString();
return JSON.parse(body); return JSON.parse(body);
} catch (err) { } catch (err) {
@@ -210,32 +210,34 @@ module.exports = class CacheHandler {
} }
async set(key, data, ctx) { async set(key, data, ctx) {
await s3.send(new PutObjectCommand({ await s3.send(
Bucket: BUCKET, new PutObjectCommand({
Key: `cache/${key}`, Bucket: BUCKET,
Body: JSON.stringify({ Key: `cache/${key}`,
value: data, Body: JSON.stringify({
lastModified: Date.now(), value: data,
}), lastModified: Date.now(),
ContentType: 'application/json', }),
})); ContentType: 'application/json',
})
);
} }
}; };
``` ```
## What Works vs What Needs Setup ## What Works vs What Needs Setup
| Feature | Single Instance | Multi-Instance | Notes | | Feature | Single Instance | Multi-Instance | Notes |
|---------|----------------|----------------|-------| | -------------------- | --------------- | ------------------- | --------------------------- |
| SSR | Yes | Yes | No special setup | | SSR | Yes | Yes | No special setup |
| SSG | Yes | Yes | Built at deploy time | | SSG | Yes | Yes | Built at deploy time |
| ISR | Yes | Needs cache handler | Filesystem cache breaks | | ISR | Yes | Needs cache handler | Filesystem cache breaks |
| Image Optimization | Yes | Yes | CPU-intensive, consider CDN | | Image Optimization | Yes | Yes | CPU-intensive, consider CDN |
| Middleware | Yes | Yes | Runs on Node.js | | Middleware | Yes | Yes | Runs on Node.js |
| Edge Runtime | Limited | Limited | Some features Node-only | | Edge Runtime | Limited | Limited | Some features Node-only |
| `revalidatePath/Tag` | Yes | Needs cache handler | Must share cache | | `revalidatePath/Tag` | Yes | Needs cache handler | Must share cache |
| `next/font` | Yes | Yes | Fonts bundled at build | | `next/font` | Yes | Yes | Fonts bundled at build |
| Draft Mode | Yes | Yes | Cookie-based | | Draft Mode | Yes | Yes | Cookie-based |
## Image Optimization ## Image Optimization
@@ -244,6 +246,7 @@ Next.js Image Optimization works out of the box but is CPU-intensive.
### Option 1: Built-in (Simple) ### Option 1: Built-in (Simple)
Works automatically, but consider: Works automatically, but consider:
- Set `deviceSizes` and `imageSizes` in config to limit variants - Set `deviceSizes` and `imageSizes` in config to limit variants
- Use `minimumCacheTTL` to reduce regeneration - Use `minimumCacheTTL` to reduce regeneration
@@ -317,6 +320,7 @@ npx @opennextjs/aws build
``` ```
Supports: Supports:
- AWS Lambda + CloudFront - AWS Lambda + CloudFront
- Cloudflare Workers - Cloudflare Workers
- Netlify Functions - Netlify Functions
@@ -8,27 +8,27 @@ Always requires Suspense boundary in static routes. Without it, the entire page
```tsx ```tsx
// Bad: Entire page becomes CSR // Bad: Entire page becomes CSR
'use client' 'use client';
import { useSearchParams } from 'next/navigation' import { useSearchParams } from 'next/navigation';
export default function SearchBar() { export default function SearchBar() {
const searchParams = useSearchParams() const searchParams = useSearchParams();
return <div>Query: {searchParams.get('q')}</div> return <div>Query: {searchParams.get('q')}</div>;
} }
``` ```
```tsx ```tsx
// Good: Wrap in Suspense // Good: Wrap in Suspense
import { Suspense } from 'react' import { Suspense } from 'react';
import SearchBar from './search-bar' import SearchBar from './search-bar';
export default function Page() { export default function Page() {
return ( return (
<Suspense fallback={<div>Loading...</div>}> <Suspense fallback={<div>Loading...</div>}>
<SearchBar /> <SearchBar />
</Suspense> </Suspense>
) );
} }
``` ```
@@ -39,12 +39,12 @@ Requires Suspense boundary when route has dynamic parameters.
```tsx ```tsx
// In dynamic route [slug] // In dynamic route [slug]
// Bad: No Suspense // Bad: No Suspense
'use client' 'use client';
import { usePathname } from 'next/navigation' import { usePathname } from 'next/navigation';
export function Breadcrumb() { export function Breadcrumb() {
const pathname = usePathname() const pathname = usePathname();
return <nav>{pathname}</nav> return <nav>{pathname}</nav>;
} }
``` ```
@@ -59,9 +59,9 @@ If you use `generateStaticParams`, Suspense is optional.
## Quick Reference ## Quick Reference
| Hook | Suspense Required | | Hook | Suspense Required |
|------|-------------------| | ------------------- | -------------------- |
| `useSearchParams()` | Yes | | `useSearchParams()` | Yes |
| `usePathname()` | Yes (dynamic routes) | | `usePathname()` | Yes (dynamic routes) |
| `useParams()` | No | | `useParams()` | No |
| `useRouter()` | No | | `useRouter()` | No |
+5 -4
View File
@@ -21,6 +21,7 @@ You are the **Antigravity Consistency Analyst**. Your role is to identify incons
## Task ## Task
### Goal ### Goal
Identify inconsistencies, duplications, ambiguities, and underspecified items across the three core artifacts (`spec.md`, `plan.md`, `tasks.md`) before implementation. This command MUST run only after `/speckit-tasks` has successfully produced a complete `tasks.md`. Identify inconsistencies, duplications, ambiguities, and underspecified items across the three core artifacts (`spec.md`, `plan.md`, `tasks.md`) before implementation. This command MUST run only after `/speckit-tasks` has successfully produced a complete `tasks.md`.
## Operating Constraints ## Operating Constraints
@@ -135,16 +136,16 @@ Output a Markdown report (no file writes) with the following structure:
## Specification Analysis Report ## Specification Analysis Report
| ID | Category | Severity | Location(s) | Summary | Recommendation | | ID | Category | Severity | Location(s) | Summary | Recommendation |
|----|----------|----------|-------------|---------|----------------| | --- | ----------- | -------- | ---------------- | ---------------------------- | ------------------------------------ |
| A1 | Duplication | HIGH | spec.md:L120-134 | Two similar requirements ... | Merge phrasing; keep clearer version | | A1 | Duplication | HIGH | spec.md:L120-134 | Two similar requirements ... | Merge phrasing; keep clearer version |
(Add one row per finding; generate stable IDs prefixed by category initial.) (Add one row per finding; generate stable IDs prefixed by category initial.)
**Coverage Summary Table:** **Coverage Summary Table:**
| Requirement Key | Has Task? | Task IDs | Notes | | Requirement Key | Has Task? | Task IDs | Notes |
|-----------------|-----------|----------|-------| | --------------- | --------- | -------- | ----- |
**Constitution Alignment Issues:** (if any) **Constitution Alignment Issues:** (if any)
+80 -75
View File
@@ -26,119 +26,124 @@ Auto-detect available tools, run them, and aggregate results into a prioritized
### Execution Steps ### Execution Steps
1. **Detect Project Type and Tools**: 1. **Detect Project Type and Tools**:
```bash ```bash
# Check for config files # Check for config files
ls -la | grep -E "(package.json|pyproject.toml|go.mod|Cargo.toml|pom.xml)" ls -la | grep -E "(package.json|pyproject.toml|go.mod|Cargo.toml|pom.xml)"
# Check for linter configs # Check for linter configs
ls -la | grep -E "(eslint|prettier|pylint|golangci|rustfmt)" ls -la | grep -E "(eslint|prettier|pylint|golangci|rustfmt)"
``` ```
| Config | Tools to Run | | Config | Tools to Run |
|--------|-------------| | ---------------- | ----------------------------- |
| `package.json` | ESLint, TypeScript, npm audit | | `package.json` | ESLint, TypeScript, npm audit |
| `pyproject.toml` | Pylint/Ruff, mypy, bandit | | `pyproject.toml` | Pylint/Ruff, mypy, bandit |
| `go.mod` | golangci-lint, go vet | | `go.mod` | golangci-lint, go vet |
| `Cargo.toml` | clippy, cargo audit | | `Cargo.toml` | clippy, cargo audit |
| `pom.xml` | SpotBugs, PMD | | `pom.xml` | SpotBugs, PMD |
2. **Run Linting**: 2. **Run Linting**:
| Stack | Command | | Stack | Command |
|-------|---------| | ------- | ---------------------------------------------- | --- | ------------------------------------- |
| Node/TS | `npx eslint . --format json 2>/dev/null` | | Node/TS | `npx eslint . --format json 2>/dev/null` |
| Python | `ruff check . --output-format json 2>/dev/null || pylint --output-format=json **/*.py` | | Python | `ruff check . --output-format json 2>/dev/null | | pylint --output-format=json \*_/_.py` |
| Go | `golangci-lint run --out-format json` | | Go | `golangci-lint run --out-format json` |
| Rust | `cargo clippy --message-format=json` | | Rust | `cargo clippy --message-format=json` |
3. **Run Type Checking**: 3. **Run Type Checking**:
| Stack | Command | | Stack | Command |
|-------|---------| | ---------- | ------------------------------------------ |
| TypeScript | `npx tsc --noEmit 2>&1` | | TypeScript | `npx tsc --noEmit 2>&1` |
| Python | `mypy . --no-error-summary 2>&1` | | Python | `mypy . --no-error-summary 2>&1` |
| Go | `go build ./... 2>&1` (types are built-in) | | Go | `go build ./... 2>&1` (types are built-in) |
4. **Run Security Scanning**: 4. **Run Security Scanning**:
| Stack | Command | | Stack | Command |
|-------|---------| | ------ | -------------------------------- | --- | -------------------- |
| Node | `npm audit --json` | | Node | `npm audit --json` |
| Python | `bandit -r . -f json 2>/dev/null || safety check --json` | | Python | `bandit -r . -f json 2>/dev/null | | safety check --json` |
| Go | `govulncheck ./... 2>&1` | | Go | `govulncheck ./... 2>&1` |
| Rust | `cargo audit --json` | | Rust | `cargo audit --json` |
5. **Aggregate and Prioritize**: 5. **Aggregate and Prioritize**:
| Category | Priority | | Category | Priority |
|----------|----------| | ------------------------ | -------- |
| Security (Critical/High) | 🔴 P1 | | Security (Critical/High) | 🔴 P1 |
| Type Errors | 🟠 P2 | | Type Errors | 🟠 P2 |
| Security (Medium/Low) | 🟡 P3 | | Security (Medium/Low) | 🟡 P3 |
| Lint Errors | 🟡 P3 | | Lint Errors | 🟡 P3 |
| Lint Warnings | 🟢 P4 | | Lint Warnings | 🟢 P4 |
| Style Issues | ⚪ P5 | | Style Issues | ⚪ P5 |
6. **Generate Report**: 6. **Generate Report**:
```markdown
````markdown
# Static Analysis Report # Static Analysis Report
**Date**: [timestamp] **Date**: [timestamp]
**Project**: [name from package.json/pyproject.toml] **Project**: [name from package.json/pyproject.toml]
**Status**: CLEAN | ISSUES FOUND **Status**: CLEAN | ISSUES FOUND
## Tools Run ## Tools Run
| Tool | Status | Issues | | Tool | Status | Issues |
|------|--------|--------| | ---------- | ------ | ----------------- |
| ESLint | ✅ | 12 | | ESLint | ✅ | 12 |
| TypeScript | ✅ | 3 | | TypeScript | ✅ | 3 |
| npm audit | ⚠️ | 2 vulnerabilities | | npm audit | ⚠️ | 2 vulnerabilities |
## Summary by Priority ## Summary by Priority
| Priority | Count | | Priority | Count |
|----------|-------| | -------------- | ----- |
| 🔴 P1 Critical | X | | 🔴 P1 Critical | X |
| 🟠 P2 High | X | | 🟠 P2 High | X |
| 🟡 P3 Medium | X | | 🟡 P3 Medium | X |
| 🟢 P4 Low | X | | 🟢 P4 Low | X |
## Issues ## Issues
### 🔴 P1: Security Vulnerabilities ### 🔴 P1: Security Vulnerabilities
| Package | Severity | Issue | Fix | | Package | Severity | Issue | Fix |
|---------|----------|-------|-----| | ------- | -------- | ------------------- | ------------------ |
| lodash | HIGH | Prototype Pollution | Upgrade to 4.17.21 | | lodash | HIGH | Prototype Pollution | Upgrade to 4.17.21 |
### 🟠 P2: Type Errors ### 🟠 P2: Type Errors
| File | Line | Error | | File | Line | Error |
|------|------|-------| | ---------- | ---- | ------------------------------------------------ |
| src/api.ts | 45 | Type 'string' is not assignable to type 'number' | | src/api.ts | 45 | Type 'string' is not assignable to type 'number' |
### 🟡 P3: Lint Issues ### 🟡 P3: Lint Issues
| File | Line | Rule | Message | | File | Line | Rule | Message |
|------|------|------|---------| | ------------ | ---- | -------------- | ------------------------------- |
| src/utils.ts | 12 | no-unused-vars | 'foo' is defined but never used | | src/utils.ts | 12 | no-unused-vars | 'foo' is defined but never used |
## Quick Fixes ## Quick Fixes
```bash ```bash
# Fix security issues # Fix security issues
npm audit fix npm audit fix
# Auto-fix lint issues # Auto-fix lint issues
npx eslint . --fix npx eslint . --fix
``` ```
````
## Recommendations ## Recommendations
1. **Immediate**: Fix P1 security issues 1. **Immediate**: Fix P1 security issues
2. **Before merge**: Fix P2 type errors 2. **Before merge**: Fix P2 type errors
3. **Tech debt**: Address P3/P4 lint issues 3. **Tech debt**: Address P3/P4 lint issues
```
``` ```
7. **Output**: 7. **Output**:
@@ -6,16 +6,16 @@
**Note**: This checklist is generated by the `/speckit-checklist` command based on feature context and requirements. **Note**: This checklist is generated by the `/speckit-checklist` command based on feature context and requirements.
<!-- <!--
============================================================================ ============================================================================
IMPORTANT: The checklist items below are SAMPLE ITEMS for illustration only. IMPORTANT: The checklist items below are SAMPLE ITEMS for illustration only.
The /speckit-checklist command MUST replace these with actual items based on: The /speckit-checklist command MUST replace these with actual items based on:
- User's specific checklist request - User's specific checklist request
- Feature requirements from spec.md - Feature requirements from spec.md
- Technical context from plan.md - Technical context from plan.md
- Implementation details from tasks.md - Implementation details from tasks.md
DO NOT keep these sample items in the generated checklist file. DO NOT keep these sample items in the generated checklist file.
============================================================================ ============================================================================
--> -->
+58 -58
View File
@@ -4,7 +4,7 @@ description: Identify underspecified areas in the current feature spec by asking
version: 1.0.0 version: 1.0.0
depends-on: depends-on:
- speckit-specify - speckit-specify
handoffs: handoffs:
- label: Build Technical Plan - label: Build Technical Plan
agent: speckit-plan agent: speckit-plan
prompt: Create a plan for the spec. I am building with... prompt: Create a plan for the spec. I am building with...
@@ -96,69 +96,69 @@ Execution steps:
- Information is better deferred to planning phase (note internally) - Information is better deferred to planning phase (note internally)
3. Generate (internally) a prioritized queue of candidate clarification questions (maximum 5). Do NOT output them all at once. Apply these constraints: 3. Generate (internally) a prioritized queue of candidate clarification questions (maximum 5). Do NOT output them all at once. Apply these constraints:
- Maximum of 10 total questions across the whole session. - Maximum of 10 total questions across the whole session.
- Each question must be answerable with EITHER: - Each question must be answerable with EITHER:
- A short multiplechoice selection (25 distinct, mutually exclusive options), OR - A short multiplechoice selection (25 distinct, mutually exclusive options), OR
- A one-word / shortphrase answer (explicitly constrain: "Answer in <=5 words"). - A one-word / shortphrase answer (explicitly constrain: "Answer in <=5 words").
- Only include questions whose answers materially impact architecture, data modeling, task decomposition, test design, UX behavior, operational readiness, or compliance validation. - Only include questions whose answers materially impact architecture, data modeling, task decomposition, test design, UX behavior, operational readiness, or compliance validation.
- Ensure category coverage balance: attempt to cover the highest impact unresolved categories first; avoid asking two low-impact questions when a single high-impact area (e.g., security posture) is unresolved. - Ensure category coverage balance: attempt to cover the highest impact unresolved categories first; avoid asking two low-impact questions when a single high-impact area (e.g., security posture) is unresolved.
- Exclude questions already answered, trivial stylistic preferences, or plan-level execution details (unless blocking correctness). - Exclude questions already answered, trivial stylistic preferences, or plan-level execution details (unless blocking correctness).
- Favor clarifications that reduce downstream rework risk or prevent misaligned acceptance tests. - Favor clarifications that reduce downstream rework risk or prevent misaligned acceptance tests.
- If more than 5 categories remain unresolved, select the top 5 by (Impact * Uncertainty) heuristic. - If more than 5 categories remain unresolved, select the top 5 by (Impact \* Uncertainty) heuristic.
4. Sequential questioning loop (interactive): 4. Sequential questioning loop (interactive):
- Present EXACTLY ONE question at a time. - Present EXACTLY ONE question at a time.
- For multiplechoice questions: - For multiplechoice questions:
- **Analyze all options** and determine the **most suitable option** based on: - **Analyze all options** and determine the **most suitable option** based on:
- Best practices for the project type - Best practices for the project type
- Common patterns in similar implementations - Common patterns in similar implementations
- Risk reduction (security, performance, maintainability) - Risk reduction (security, performance, maintainability)
- Alignment with any explicit project goals or constraints visible in the spec - Alignment with any explicit project goals or constraints visible in the spec
- Present your **recommended option prominently** at the top with clear reasoning (1-2 sentences explaining why this is the best choice). - Present your **recommended option prominently** at the top with clear reasoning (1-2 sentences explaining why this is the best choice).
- Format as: `**Recommended:** Option [X] - <reasoning>` - Format as: `**Recommended:** Option [X] - <reasoning>`
- Then render all options as a Markdown table: - Then render all options as a Markdown table:
| Option | Description | | Option | Description |
|--------|-------------| | ------ | --------------------------------------------------------------------------------------------------- |
| A | <Option A description> | | A | <Option A description> |
| B | <Option B description> | | B | <Option B description> |
| C | <Option C description> (add D/E as needed up to 5) | | C | <Option C description> (add D/E as needed up to 5) |
| Short | Provide a different short answer (<=5 words) (Include only if free-form alternative is appropriate) | | Short | Provide a different short answer (<=5 words) (Include only if free-form alternative is appropriate) |
- After the table, add: `You can reply with the option letter (e.g., "A"), accept the recommendation by saying "yes" or "recommended", or provide your own short answer.`
- After the table, add: `You can reply with the option letter (e.g., "A"), accept the recommendation by saying "yes" or "recommended", or provide your own short answer.` - For shortanswer style (no meaningful discrete options):
- For shortanswer style (no meaningful discrete options): - Provide your **suggested answer** based on best practices and context.
- Provide your **suggested answer** based on best practices and context. - Format as: `**Suggested:** <your proposed answer> - <brief reasoning>`
- Format as: `**Suggested:** <your proposed answer> - <brief reasoning>` - Then output: `Format: Short answer (<=5 words). You can accept the suggestion by saying "yes" or "suggested", or provide your own answer.`
- Then output: `Format: Short answer (<=5 words). You can accept the suggestion by saying "yes" or "suggested", or provide your own answer.` - After the user answers:
- After the user answers: - If the user replies with "yes", "recommended", or "suggested", use your previously stated recommendation/suggestion as the answer.
- If the user replies with "yes", "recommended", or "suggested", use your previously stated recommendation/suggestion as the answer. - Otherwise, validate the answer maps to one option or fits the <=5 word constraint.
- Otherwise, validate the answer maps to one option or fits the <=5 word constraint. - If ambiguous, ask for a quick disambiguation (count still belongs to same question; do not advance).
- If ambiguous, ask for a quick disambiguation (count still belongs to same question; do not advance). - Once satisfactory, record it in working memory (do not yet write to disk) and move to the next queued question.
- Once satisfactory, record it in working memory (do not yet write to disk) and move to the next queued question. - Stop asking further questions when:
- Stop asking further questions when: - All critical ambiguities resolved early (remaining queued items become unnecessary), OR
- All critical ambiguities resolved early (remaining queued items become unnecessary), OR - User signals completion ("done", "good", "no more"), OR
- User signals completion ("done", "good", "no more"), OR - You reach 5 asked questions.
- You reach 5 asked questions. - Never reveal future queued questions in advance.
- Never reveal future queued questions in advance. - If no valid questions exist at start, immediately report no critical ambiguities.
- If no valid questions exist at start, immediately report no critical ambiguities.
5. Integration after EACH accepted answer (incremental update approach): 5. Integration after EACH accepted answer (incremental update approach):
- Maintain in-memory representation of the spec (loaded once at start) plus the raw file contents. - Maintain in-memory representation of the spec (loaded once at start) plus the raw file contents.
- For the first integrated answer in this session: - For the first integrated answer in this session:
- Ensure a `## Clarifications` section exists (create it just after the highest-level contextual/overview section per the spec template if missing). - Ensure a `## Clarifications` section exists (create it just after the highest-level contextual/overview section per the spec template if missing).
- Under it, create (if not present) a `### Session YYYY-MM-DD` subheading for today. - Under it, create (if not present) a `### Session YYYY-MM-DD` subheading for today.
- Append a bullet line immediately after acceptance: `- Q: <question> → A: <final answer>`. - Append a bullet line immediately after acceptance: `- Q: <question> → A: <final answer>`.
- Then immediately apply the clarification to the most appropriate section(s): - Then immediately apply the clarification to the most appropriate section(s):
- Functional ambiguity → Update or add a bullet in Functional Requirements. - Functional ambiguity → Update or add a bullet in Functional Requirements.
- User interaction / actor distinction → Update User Stories or Actors subsection (if present) with clarified role, constraint, or scenario. - User interaction / actor distinction → Update User Stories or Actors subsection (if present) with clarified role, constraint, or scenario.
- Data shape / entities → Update Data Model (add fields, types, relationships) preserving ordering; note added constraints succinctly. - Data shape / entities → Update Data Model (add fields, types, relationships) preserving ordering; note added constraints succinctly.
- Non-functional constraint → Add/modify measurable criteria in Non-Functional / Quality Attributes section (convert vague adjective to metric or explicit target). - Non-functional constraint → Add/modify measurable criteria in Non-Functional / Quality Attributes section (convert vague adjective to metric or explicit target).
- Edge case / negative flow → Add a new bullet under Edge Cases / Error Handling (or create such subsection if template provides placeholder for it). - Edge case / negative flow → Add a new bullet under Edge Cases / Error Handling (or create such subsection if template provides placeholder for it).
- Terminology conflict → Normalize term across spec; retain original only if necessary by adding `(formerly referred to as "X")` once. - Terminology conflict → Normalize term across spec; retain original only if necessary by adding `(formerly referred to as "X")` once.
- If the clarification invalidates an earlier ambiguous statement, replace that statement instead of duplicating; leave no obsolete contradictory text. - If the clarification invalidates an earlier ambiguous statement, replace that statement instead of duplicating; leave no obsolete contradictory text.
- Save the spec file AFTER each integration to minimize risk of context loss (atomic overwrite). - Save the spec file AFTER each integration to minimize risk of context loss (atomic overwrite).
- Preserve formatting: do not reorder unrelated sections; keep heading hierarchy intact. - Preserve formatting: do not reorder unrelated sections; keep heading hierarchy intact.
- Keep each inserted clarification minimal and testable (avoid narrative drift). - Keep each inserted clarification minimal and testable (avoid narrative drift).
6. Validation (performed after EACH write plus final pass): 6. Validation (performed after EACH write plus final pass):
- Clarifications session contains exactly one bullet per accepted answer (no duplicates). - Clarifications session contains exactly one bullet per accepted answer (no duplicates).
+15 -10
View File
@@ -31,10 +31,12 @@ Compare two versions of a specification artifact and produce a structured diff r
- If no arguments: Use `check-prerequisites.sh` to find current feature's spec.md and compare with HEAD - If no arguments: Use `check-prerequisites.sh` to find current feature's spec.md and compare with HEAD
2. **Load Files**: 2. **Load Files**:
```bash ```bash
# For git comparison # For git comparison
git show HEAD:<relative-path> > /tmp/old_version.md git show HEAD:<relative-path> > /tmp/old_version.md
``` ```
- Read both versions into memory - Read both versions into memory
3. **Semantic Diff Analysis**: 3. **Semantic Diff Analysis**:
@@ -45,26 +47,29 @@ Compare two versions of a specification artifact and produce a structured diff r
- **Moved**: Reorganized content (same meaning, different location) - **Moved**: Reorganized content (same meaning, different location)
4. **Generate Report**: 4. **Generate Report**:
```markdown ```markdown
# Diff Report: [filename] # Diff Report: [filename]
**Compared**: [version A] → [version B] **Compared**: [version A] → [version B]
**Date**: [timestamp] **Date**: [timestamp]
## Summary ## Summary
- X additions, Y removals, Z modifications - X additions, Y removals, Z modifications
## Changes by Section ## Changes by Section
### [Section Name] ### [Section Name]
| Type | Content | Impact | | Type | Content | Impact |
|------|---------|--------| | ---------- | ------------------ | ----------------- |
| + Added | [new text] | [what this means] | | + Added | [new text] | [what this means] |
| - Removed | [old text] | [what this means] | | - Removed | [old text] | [what this means] |
| ~ Modified | [before] → [after] | [what this means] | | ~ Modified | [before] → [after] | [what this means] |
## Risk Assessment ## Risk Assessment
- Breaking changes: [list any] - Breaking changes: [list any]
- Scope changes: [list any] - Scope changes: [list any]
``` ```
+29 -29
View File
@@ -53,7 +53,7 @@ If a file is critical, complex, or has high dependencies (>2 affected files):
4. **SWITCH** the imports in the consuming files one by one. 4. **SWITCH** the imports in the consuming files one by one.
5. **ANNOUNCE**: "Applying Strangler Pattern to avoid regression." 5. **ANNOUNCE**: "Applying Strangler Pattern to avoid regression."
*Benefit: If it breaks, we simply revert the import, not the whole logic.* _Benefit: If it breaks, we simply revert the import, not the whole logic._
### Protocol 3: Reproduction Script First (TDD) ### Protocol 3: Reproduction Script First (TDD)
@@ -136,12 +136,12 @@ At the start of execution and after every 3 modifications:
git rev-parse --git-dir 2>/dev/null git rev-parse --git-dir 2>/dev/null
``` ```
- Check if Dockerfile* exists or Docker in plan.md → create/verify .dockerignore - Check if Dockerfile\* exists or Docker in plan.md → create/verify .dockerignore
- Check if .eslintrc* exists → create/verify .eslintignore - Check if .eslintrc\* exists → create/verify .eslintignore
- Check if eslint.config.* exists → ensure the config's `ignores` entries cover required patterns - Check if eslint.config.\* exists → ensure the config's `ignores` entries cover required patterns
- Check if .prettierrc* exists → create/verify .prettierignore - Check if .prettierrc\* exists → create/verify .prettierignore
- Check if .npmrc or package.json exists → create/verify .npmignore (if publishing) - Check if .npmrc or package.json exists → create/verify .npmignore (if publishing)
- Check if terraform files (*.tf) exist → create/verify .terraformignore - Check if terraform files (\*.tf) exist → create/verify .terraformignore
- Check if .helmignore needed (helm charts present) → create/verify .helmignore - Check if .helmignore needed (helm charts present) → create/verify .helmignore
**If ignore file already exists**: Verify it contains essential patterns, append missing critical patterns only **If ignore file already exists**: Verify it contains essential patterns, append missing critical patterns only
@@ -179,35 +179,35 @@ At the start of execution and after every 3 modifications:
7. **Execute implementation following the task plan with Ironclad Protocols**: 7. **Execute implementation following the task plan with Ironclad Protocols**:
**For EACH task**, follow this sequence: **For EACH task**, follow this sequence:
a. **Blast Radius Analysis (Protocol 1)**: a. **Blast Radius Analysis (Protocol 1)**:
- Identify all files that will be modified - Identify all files that will be modified
- Run `grep` to find all dependents - Run `grep` to find all dependents
- Report the blast radius - Report the blast radius
b. **Strategy Decision**: b. **Strategy Decision**:
- If LOW risk (≤2 affected files): Proceed with inline modification - If LOW risk (≤2 affected files): Proceed with inline modification
- If MEDIUM/HIGH risk (>2 files): Apply Strangler Pattern (Protocol 2) - If MEDIUM/HIGH risk (>2 files): Apply Strangler Pattern (Protocol 2)
c. **Reproduction Script (Protocol 3)**: c. **Reproduction Script (Protocol 3)**:
- Create `repro_task_[ID].ts` that demonstrates expected behavior - Create `repro_task_[ID].ts` that demonstrates expected behavior
- Run it to confirm current state (should fail for new features, or fail for bugs) - Run it to confirm current state (should fail for new features, or fail for bugs)
d. **Implementation**: d. **Implementation**:
- Execute the task according to plan - Execute the task according to plan
- **Phase-by-phase execution**: Complete each phase before moving to the next - **Phase-by-phase execution**: Complete each phase before moving to the next
- **Respect dependencies**: Run sequential tasks in order, parallel tasks [P] can run together - **Respect dependencies**: Run sequential tasks in order, parallel tasks [P] can run together
- **Follow TDD approach**: Execute test tasks before their corresponding implementation tasks - **Follow TDD approach**: Execute test tasks before their corresponding implementation tasks
- **File-based coordination**: Tasks affecting the same files must run sequentially - **File-based coordination**: Tasks affecting the same files must run sequentially
e. **Verification**: e. **Verification**:
- Run the reproduction script again (should now pass) - Run the reproduction script again (should now pass)
- Run existing tests to ensure no regression - Run existing tests to ensure no regression
- If any test fails: **STOP** and report the regression - If any test fails: **STOP** and report the regression
f. **Cleanup**: f. **Cleanup**:
- Delete temporary repro scripts OR convert to permanent tests - Delete temporary repro scripts OR convert to permanent tests
- Mark task as complete `[X]` in tasks.md - Mark task as complete `[X]` in tasks.md
8. **Progress tracking and error handling**: 8. **Progress tracking and error handling**:
- Report progress after each completed task with this format: - Report progress after each completed task with this format:
+23 -11
View File
@@ -31,10 +31,11 @@ Analyze an existing codebase and generate speckit artifacts (spec.md, plan.md, t
- `--depth <n>`: Analysis depth (1=overview, 2=detailed, 3=exhaustive) - `--depth <n>`: Analysis depth (1=overview, 2=detailed, 3=exhaustive)
2. **Codebase Discovery**: 2. **Codebase Discovery**:
```bash ```bash
# Get project structure # Get project structure
tree -L 3 --dirsfirst -I 'node_modules|.git|dist|build' > /tmp/structure.txt tree -L 3 --dirsfirst -I 'node_modules|.git|dist|build' > /tmp/structure.txt
# Find key files # Find key files
find . -name "*.md" -o -name "package.json" -o -name "*.config.*" | head -50 find . -name "*.md" -o -name "package.json" -o -name "*.config.*" | head -50
``` ```
@@ -47,48 +48,59 @@ Analyze an existing codebase and generate speckit artifacts (spec.md, plan.md, t
- Map API endpoints (if applicable) - Map API endpoints (if applicable)
4. **Generate spec.md** (reverse-engineered): 4. **Generate spec.md** (reverse-engineered):
```markdown ```markdown
# [Feature Name] - Specification (Migrated) # [Feature Name] - Specification (Migrated)
> This specification was auto-generated from existing code. > This specification was auto-generated from existing code.
> Review and refine before using for future development. > Review and refine before using for future development.
## Overview ## Overview
[Inferred from README, comments, and code structure] [Inferred from README, comments, and code structure]
## Functional Requirements ## Functional Requirements
[Extracted from existing functionality] [Extracted from existing functionality]
## Key Entities ## Key Entities
[From data models, schemas, types] [From data models, schemas, types]
``` ```
5. **Generate plan.md** (reverse-engineered): 5. **Generate plan.md** (reverse-engineered):
```markdown ```markdown
# [Feature Name] - Technical Plan (Migrated) # [Feature Name] - Technical Plan (Migrated)
## Current Architecture ## Current Architecture
[Documented from codebase analysis] [Documented from codebase analysis]
## Technology Stack ## Technology Stack
[From package.json, imports, configs] [From package.json, imports, configs]
## Component Map ## Component Map
[Directory → responsibility mapping] [Directory → responsibility mapping]
``` ```
6. **Generate tasks.md** (completion status): 6. **Generate tasks.md** (completion status):
```markdown ```markdown
# [Feature Name] - Tasks (Migrated) # [Feature Name] - Tasks (Migrated)
All tasks marked [x] represent existing implemented functionality. All tasks marked [x] represent existing implemented functionality.
Tasks marked [ ] are inferred gaps or TODOs found in code. Tasks marked [ ] are inferred gaps or TODOs found in code.
## Existing Implementation ## Existing Implementation
- [x] [Component A] - Implemented in `src/componentA/` - [x] [Component A] - Implemented in `src/componentA/`
- [x] [Component B] - Implemented in `src/componentB/` - [x] [Component B] - Implemented in `src/componentB/`
## Identified Gaps ## Identified Gaps
- [ ] [Missing tests for X] - [ ] [Missing tests for X]
- [ ] [TODO comment at Y] - [ ] [TODO comment at Y]
``` ```
+2 -2
View File
@@ -4,7 +4,7 @@ description: Execute the implementation planning workflow using the plan templat
version: 1.0.0 version: 1.0.0
depends-on: depends-on:
- speckit-specify - speckit-specify
handoffs: handoffs:
- label: Create Tasks - label: Create Tasks
agent: speckit-tasks agent: speckit-tasks
prompt: Break the plan into tasks prompt: Break the plan into tasks
@@ -91,7 +91,7 @@ You are the **Antigravity System Architect**. Your role is to bridge the gap bet
- Add only new technology from current plan - Add only new technology from current plan
- Preserve manual additions between markers - Preserve manual additions between markers
**Output**: data-model.md, /contracts/*, quickstart.md, agent-specific file **Output**: data-model.md, /contracts/\*, quickstart.md, agent-specific file
## Key rules ## Key rules
@@ -29,7 +29,7 @@
## Constitution Check ## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* _GATE: Must pass before Phase 0 research. Re-check after Phase 1 design._
[Gates determined based on constitution file] [Gates determined based on constitution file]
@@ -48,6 +48,7 @@ specs/[###-feature]/
``` ```
### Source Code (repository root) ### Source Code (repository root)
<!-- <!--
ACTION REQUIRED: Replace the placeholder tree below with the concrete layout ACTION REQUIRED: Replace the placeholder tree below with the concrete layout
for this feature. Delete unused options and expand the chosen structure with for this feature. Delete unused options and expand the chosen structure with
@@ -98,7 +99,7 @@ directories captured above]
> **Fill ONLY if Constitution Check has violations that must be justified** > **Fill ONLY if Constitution Check has violations that must be justified**
| Violation | Why Needed | Simpler Alternative Rejected Because | | Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|-------------------------------------| | -------------------------- | ------------------ | ------------------------------------ |
| [e.g., 4th project] | [current need] | [why 3 projects insufficient] | | [e.g., 4th project] | [current need] | [why 3 projects insufficient] |
| [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] | | [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] |
+42 -34
View File
@@ -33,7 +33,7 @@ Review code changes and provide structured feedback with severity levels.
```bash ```bash
# Get staged changes # Get staged changes
git diff --cached --name-only git diff --cached --name-only
# Get branch changes # Get branch changes
git diff main...HEAD --name-only git diff main...HEAD --name-only
``` ```
@@ -44,14 +44,14 @@ Review code changes and provide structured feedback with severity levels.
3. **Review Categories**: 3. **Review Categories**:
| Category | What to Check | | Category | What to Check |
|----------|--------------| | ------------------- | -------------------------------------------- |
| **Correctness** | Logic errors, off-by-one, null handling | | **Correctness** | Logic errors, off-by-one, null handling |
| **Security** | SQL injection, XSS, secrets in code | | **Security** | SQL injection, XSS, secrets in code |
| **Performance** | N+1 queries, unnecessary loops, memory leaks | | **Performance** | N+1 queries, unnecessary loops, memory leaks |
| **Maintainability** | Complexity, duplication, naming | | **Maintainability** | Complexity, duplication, naming |
| **Best Practices** | Error handling, logging, typing | | **Best Practices** | Error handling, logging, typing |
| **Style** | Consistency, formatting (if no linter) | | **Style** | Consistency, formatting (if no linter) |
4. **Analyze Each File**: 4. **Analyze Each File**:
For each file, check: For each file, check:
@@ -64,63 +64,71 @@ Review code changes and provide structured feedback with severity levels.
5. **Severity Levels**: 5. **Severity Levels**:
| Level | Meaning | Block Merge? | | Level | Meaning | Block Merge? |
|-------|---------|--------------| | ------------- | ------------------------------ | ------------ |
| 🔴 CRITICAL | Security issue, data loss risk | Yes | | 🔴 CRITICAL | Security issue, data loss risk | Yes |
| 🟠 HIGH | Bug, logic error | Yes | | 🟠 HIGH | Bug, logic error | Yes |
| 🟡 MEDIUM | Code smell, maintainability | Maybe | | 🟡 MEDIUM | Code smell, maintainability | Maybe |
| 🟢 LOW | Style, minor improvement | No | | 🟢 LOW | Style, minor improvement | No |
| 💡 SUGGESTION | Nice-to-have, optional | No | | 💡 SUGGESTION | Nice-to-have, optional | No |
6. **Generate Review Report**: 6. **Generate Review Report**:
```markdown
````markdown
# Code Review Report # Code Review Report
**Date**: [timestamp] **Date**: [timestamp]
**Scope**: [files reviewed] **Scope**: [files reviewed]
**Overall**: APPROVE | REQUEST CHANGES | NEEDS DISCUSSION **Overall**: APPROVE | REQUEST CHANGES | NEEDS DISCUSSION
## Summary ## Summary
| Severity | Count | | Severity | Count |
|----------|-------| | -------------- | ----- |
| 🔴 Critical | X | | 🔴 Critical | X |
| 🟠 High | X | | 🟠 High | X |
| 🟡 Medium | X | | 🟡 Medium | X |
| 🟢 Low | X | | 🟢 Low | X |
| 💡 Suggestions | X | | 💡 Suggestions | X |
## Findings ## Findings
### 🔴 CRITICAL: SQL Injection Risk ### 🔴 CRITICAL: SQL Injection Risk
**File**: `src/db/queries.ts:45` **File**: `src/db/queries.ts:45`
**Code**: **Code**:
```typescript ```typescript
const query = `SELECT * FROM users WHERE id = ${userId}`; const query = `SELECT * FROM users WHERE id = ${userId}`;
``` ```
````
**Issue**: User input directly concatenated into SQL query **Issue**: User input directly concatenated into SQL query
**Fix**: Use parameterized queries: **Fix**: Use parameterized queries:
```typescript ```typescript
const query = 'SELECT * FROM users WHERE id = $1'; const query = 'SELECT * FROM users WHERE id = $1';
await db.query(query, [userId]); await db.query(query, [userId]);
``` ```
### 🟡 MEDIUM: Complex Function ### 🟡 MEDIUM: Complex Function
**File**: `src/auth/handler.ts:120` **File**: `src/auth/handler.ts:120`
**Issue**: Function has cyclomatic complexity of 15 **Issue**: Function has cyclomatic complexity of 15
**Suggestion**: Extract into smaller functions **Suggestion**: Extract into smaller functions
## What's Good ## What's Good
- Clear naming conventions - Clear naming conventions
- Good test coverage - Good test coverage
- Proper TypeScript types - Proper TypeScript types
## Recommended Actions ## Recommended Actions
1. **Must fix before merge**: [critical/high items] 1. **Must fix before merge**: [critical/high items]
2. **Should address**: [medium items] 2. **Should address**: [medium items]
3. **Consider for later**: [low/suggestions] 3. **Consider for later**: [low/suggestions]
```
``` ```
7. **Output**: 7. **Output**:
@@ -131,8 +131,8 @@ Check LCBP3-DMS-specific file handling per ADR-016:
## Severity Classification ## Severity Classification
| Severity | Description | Response | | Severity | Description | Response |
| -------------- | ----------------------------------------------------- | ----------------------- | | --------------- | ----------------------------------------------------- | ----------------------- |
| 🔴 **Critical** | Exploitable vulnerability, data exposure, auth bypass | Immediate fix required | | 🔴 **Critical** | Exploitable vulnerability, data exposure, auth bypass | Immediate fix required |
| 🟠 **High** | Missing security control, potential escalation path | Fix before next release | | 🟠 **High** | Missing security control, potential escalation path | Fix before next release |
| 🟡 **Medium** | Best practice violation, defense-in-depth gap | Plan fix in sprint | | 🟡 **Medium** | Best practice violation, defense-in-depth gap | Plan fix in sprint |
@@ -151,8 +151,8 @@ Generate a structured report:
## Summary ## Summary
| Severity | Count | | Severity | Count |
| ---------- | ----- | | ----------- | ----- |
| 🔴 Critical | X | | 🔴 Critical | X |
| 🟠 High | X | | 🟠 High | X |
| 🟡 Medium | X | | 🟡 Medium | X |
@@ -179,8 +179,8 @@ Generate a structured report:
| Module | Controller | Guard? | Policies? | Level | | Module | Controller | Guard? | Policies? | Level |
| ------ | --------------- | ------ | --------- | ------------ | | ------ | --------------- | ------ | --------- | ------------ |
| auth | AuthController | ✅ | ✅ | N/A (public) | | auth | AuthController | ✅ | ✅ | N/A (public) |
| users | UsersController | ✅ | ✅ | L1-L4 | | users | UsersController | ✅ | ✅ | L1-L4 |
| ... | ... | ... | ... | ... | | ... | ... | ... | ... | ... |
## Recommendations Priority ## Recommendations Priority
@@ -5,13 +5,13 @@
**Status**: Draft **Status**: Draft
**Input**: User description: "$ARGUMENTS" **Input**: User description: "$ARGUMENTS"
## User Scenarios & Testing *(mandatory)* ## User Scenarios & Testing _(mandatory)_
<!-- <!--
IMPORTANT: User stories should be PRIORITIZED as user journeys ordered by importance. IMPORTANT: User stories should be PRIORITIZED as user journeys ordered by importance.
Each user story/journey must be INDEPENDENTLY TESTABLE - meaning if you implement just ONE of them, Each user story/journey must be INDEPENDENTLY TESTABLE - meaning if you implement just ONE of them,
you should still have a viable MVP (Minimum Viable Product) that delivers value. you should still have a viable MVP (Minimum Viable Product) that delivers value.
Assign priorities (P1, P2, P3, etc.) to each story, where P1 is the most critical. Assign priorities (P1, P2, P3, etc.) to each story, where P1 is the most critical.
Think of each story as a standalone slice of functionality that can be: Think of each story as a standalone slice of functionality that can be:
- Developed independently - Developed independently
@@ -75,7 +75,7 @@
- What happens when [boundary condition]? - What happens when [boundary condition]?
- How does system handle [error scenario]? - How does system handle [error scenario]?
## Requirements *(mandatory)* ## Requirements _(mandatory)_
<!-- <!--
ACTION REQUIRED: The content in this section represents placeholders. ACTION REQUIRED: The content in this section represents placeholders.
@@ -85,22 +85,22 @@
### Functional Requirements ### Functional Requirements
- **FR-001**: System MUST [specific capability, e.g., "allow users to create accounts"] - **FR-001**: System MUST [specific capability, e.g., "allow users to create accounts"]
- **FR-002**: System MUST [specific capability, e.g., "validate email addresses"] - **FR-002**: System MUST [specific capability, e.g., "validate email addresses"]
- **FR-003**: Users MUST be able to [key interaction, e.g., "reset their password"] - **FR-003**: Users MUST be able to [key interaction, e.g., "reset their password"]
- **FR-004**: System MUST [data requirement, e.g., "persist user preferences"] - **FR-004**: System MUST [data requirement, e.g., "persist user preferences"]
- **FR-005**: System MUST [behavior, e.g., "log all security events"] - **FR-005**: System MUST [behavior, e.g., "log all security events"]
*Example of marking unclear requirements:* _Example of marking unclear requirements:_
- **FR-006**: System MUST authenticate users via [NEEDS CLARIFICATION: auth method not specified - email/password, SSO, OAuth?] - **FR-006**: System MUST authenticate users via [NEEDS CLARIFICATION: auth method not specified - email/password, SSO, OAuth?]
- **FR-007**: System MUST retain user data for [NEEDS CLARIFICATION: retention period not specified] - **FR-007**: System MUST retain user data for [NEEDS CLARIFICATION: retention period not specified]
### Key Entities *(include if feature involves data)* ### Key Entities _(include if feature involves data)_
- **[Entity 1]**: [What it represents, key attributes without implementation] - **[Entity 1]**: [What it represents, key attributes without implementation]
- **[Entity 2]**: [What it represents, relationships to other entities] - **[Entity 2]**: [What it represents, relationships to other entities]
## Success Criteria *(mandatory)* ## Success Criteria _(mandatory)_
<!-- <!--
ACTION REQUIRED: Define measurable success criteria. ACTION REQUIRED: Define measurable success criteria.
+27 -23
View File
@@ -26,6 +26,7 @@ Generate a dashboard view of all features and their completion status.
### Execution Steps ### Execution Steps
1. **Discover Features**: 1. **Discover Features**:
```bash ```bash
# Find all feature directories # Find all feature directories
find .specify/features -maxdepth 1 -type d 2>/dev/null || echo "No features found" find .specify/features -maxdepth 1 -type d 2>/dev/null || echo "No features found"
@@ -33,14 +34,15 @@ Generate a dashboard view of all features and their completion status.
2. **For Each Feature, Gather Metrics**: 2. **For Each Feature, Gather Metrics**:
| Artifact | Check | Metric | | Artifact | Check | Metric |
|----------|-------|--------| | ---------------- | ------------------ | -------------------------- |
| spec.md | Exists? | Has [NEEDS CLARIFICATION]? | | spec.md | Exists? | Has [NEEDS CLARIFICATION]? |
| plan.md | Exists? | All sections complete? | | plan.md | Exists? | All sections complete? |
| tasks.md | Exists? | Count [x] vs [ ] vs [/] | | tasks.md | Exists? | Count [x] vs [ ] vs [/] |
| checklists/*.md | All items checked? | Checklist completion % | | checklists/\*.md | All items checked? | Checklist completion % |
3. **Calculate Completion**: 3. **Calculate Completion**:
``` ```
Phase 1 (Specify): spec.md exists & no clarifications needed Phase 1 (Specify): spec.md exists & no clarifications needed
Phase 2 (Plan): plan.md exists & complete Phase 2 (Plan): plan.md exists & complete
@@ -56,40 +58,42 @@ Generate a dashboard view of all features and their completion status.
- Missing dependencies - Missing dependencies
5. **Generate Dashboard**: 5. **Generate Dashboard**:
```markdown ```markdown
# Speckit Status Dashboard # Speckit Status Dashboard
**Generated**: [timestamp] **Generated**: [timestamp]
**Total Features**: X **Total Features**: X
## Overview ## Overview
| Feature | Phase | Progress | Blockers | Next Action | | Feature | Phase | Progress | Blockers | Next Action |
|---------|-------|----------|----------|-------------| | ------------ | --------- | -------- | -------- | ------------------------ |
| auth-system | Implement | 75% | 0 | Complete remaining tasks | | auth-system | Implement | 75% | 0 | Complete remaining tasks |
| payment-flow | Plan | 40% | 2 | Resolve clarifications | | payment-flow | Plan | 40% | 2 | Resolve clarifications |
## Feature Details ## Feature Details
### [Feature Name] ### [Feature Name]
``` ```
Spec: ████████░░ 80%
Plan: ██████████ 100% Spec: ████████░░ 80%
Plan: ██████████ 100%
Tasks: ██████░░░░ 60% Tasks: ██████░░░░ 60%
``` ```
**Blockers**: **Blockers**:
- [ ] Clarification needed: "What payment providers?" - [ ] Clarification needed: "What payment providers?"
**Recent Activity**: **Recent Activity**:
- Last modified: [date] - Last modified: [date]
- Files changed: [list] - Files changed: [list]
--- ---
## Summary ## Summary
- Features Ready for Implementation: X - Features Ready for Implementation: X
- Features Blocked: Y - Features Blocked: Y
- Overall Project Completion: Z% - Overall Project Completion: Z%
+2 -2
View File
@@ -4,7 +4,7 @@ description: Generate an actionable, dependency-ordered tasks.md for the feature
version: 1.0.0 version: 1.0.0
depends-on: depends-on:
- speckit-plan - speckit-plan
handoffs: handoffs:
- label: Analyze For Consistency - label: Analyze For Consistency
agent: speckit-analyze agent: speckit-analyze
prompt: Run a project analysis for consistency prompt: Run a project analysis for consistency
@@ -96,7 +96,7 @@ Every task MUST strictly follow this format:
4. **[Story] label**: REQUIRED for user story phase tasks only 4. **[Story] label**: REQUIRED for user story phase tasks only
- Format: [US1], [US2], [US3], etc. (maps to user stories from spec.md) - Format: [US1], [US2], [US3], etc. (maps to user stories from spec.md)
- Setup phase: NO story label - Setup phase: NO story label
- Foundational phase: NO story label - Foundational phase: NO story label
- User Story phases: MUST have story label - User Story phases: MUST have story label
- Polish phase: NO story label - Polish phase: NO story label
5. **Description**: Clear action with exact file path 5. **Description**: Clear action with exact file path
@@ -1,6 +1,5 @@
--- ---
description: 'Task list template for feature implementation'
description: "Task list template for feature implementation"
--- ---
# Tasks: [FEATURE NAME] # Tasks: [FEATURE NAME]
@@ -25,21 +24,21 @@ description: "Task list template for feature implementation"
- **Mobile**: `api/src/`, `ios/src/` or `android/src/` - **Mobile**: `api/src/`, `ios/src/` or `android/src/`
- Paths shown below assume single project - adjust based on plan.md structure - Paths shown below assume single project - adjust based on plan.md structure
<!-- <!--
============================================================================ ============================================================================
IMPORTANT: The tasks below are SAMPLE TASKS for illustration purposes only. IMPORTANT: The tasks below are SAMPLE TASKS for illustration purposes only.
The /speckit-tasks command MUST replace these with actual tasks based on: The /speckit-tasks command MUST replace these with actual tasks based on:
- User stories from spec.md (with their priorities P1, P2, P3...) - User stories from spec.md (with their priorities P1, P2, P3...)
- Feature requirements from plan.md - Feature requirements from plan.md
- Entities from data-model.md - Entities from data-model.md
- Endpoints from contracts/ - Endpoints from contracts/
Tasks MUST be organized by user story so each story can be: Tasks MUST be organized by user story so each story can be:
- Implemented independently - Implemented independently
- Tested independently - Tested independently
- Delivered as an MVP increment - Delivered as an MVP increment
DO NOT keep these sample tasks in the generated tasks.md file. DO NOT keep these sample tasks in the generated tasks.md file.
============================================================================ ============================================================================
--> -->
@@ -83,8 +82,8 @@ Examples of foundational tasks (adjust based on your project):
> **NOTE: Write these tests FIRST, ensure they FAIL before implementation** > **NOTE: Write these tests FIRST, ensure they FAIL before implementation**
- [ ] T010 [P] [US1] Contract test for [endpoint] in tests/contract/test_[name].py - [ ] T010 [P] [US1] Contract test for [endpoint] in tests/contract/test\_[name].py
- [ ] T011 [P] [US1] Integration test for [user journey] in tests/integration/test_[name].py - [ ] T011 [P] [US1] Integration test for [user journey] in tests/integration/test\_[name].py
### Implementation for User Story 1 ### Implementation for User Story 1
@@ -107,8 +106,8 @@ Examples of foundational tasks (adjust based on your project):
### Tests for User Story 2 (OPTIONAL - only if tests requested) ⚠️ ### Tests for User Story 2 (OPTIONAL - only if tests requested) ⚠️
- [ ] T018 [P] [US2] Contract test for [endpoint] in tests/contract/test_[name].py - [ ] T018 [P] [US2] Contract test for [endpoint] in tests/contract/test\_[name].py
- [ ] T019 [P] [US2] Integration test for [user journey] in tests/integration/test_[name].py - [ ] T019 [P] [US2] Integration test for [user journey] in tests/integration/test\_[name].py
### Implementation for User Story 2 ### Implementation for User Story 2
@@ -129,8 +128,8 @@ Examples of foundational tasks (adjust based on your project):
### Tests for User Story 3 (OPTIONAL - only if tests requested) ⚠️ ### Tests for User Story 3 (OPTIONAL - only if tests requested) ⚠️
- [ ] T024 [P] [US3] Contract test for [endpoint] in tests/contract/test_[name].py - [ ] T024 [P] [US3] Contract test for [endpoint] in tests/contract/test\_[name].py
- [ ] T025 [P] [US3] Integration test for [user journey] in tests/integration/test_[name].py - [ ] T025 [P] [US3] Integration test for [user journey] in tests/integration/test\_[name].py
### Implementation for User Story 3 ### Implementation for User Story 3
+40 -37
View File
@@ -26,34 +26,35 @@ Detect the project's test framework, execute tests, and generate a comprehensive
### Execution Steps ### Execution Steps
1. **Detect Test Framework**: 1. **Detect Test Framework**:
```bash ```bash
# Check package.json for test frameworks # Check package.json for test frameworks
cat package.json 2>/dev/null | grep -E "(jest|vitest|mocha|ava|tap)" cat package.json 2>/dev/null | grep -E "(jest|vitest|mocha|ava|tap)"
# Check for Python test frameworks # Check for Python test frameworks
ls pytest.ini setup.cfg pyproject.toml 2>/dev/null ls pytest.ini setup.cfg pyproject.toml 2>/dev/null
# Check for Go tests # Check for Go tests
find . -name "*_test.go" -maxdepth 3 2>/dev/null | head -1 find . -name "*_test.go" -maxdepth 3 2>/dev/null | head -1
``` ```
| Indicator | Framework | | Indicator | Framework |
|-----------|-----------| | ------------------------------- | ---------- |
| `jest` in package.json | Jest | | `jest` in package.json | Jest |
| `vitest` in package.json | Vitest | | `vitest` in package.json | Vitest |
| `pytest.ini` or `[tool.pytest]` | Pytest | | `pytest.ini` or `[tool.pytest]` | Pytest |
| `*_test.go` files | Go test | | `*_test.go` files | Go test |
| `Cargo.toml` + `#[test]` | Cargo test | | `Cargo.toml` + `#[test]` | Cargo test |
2. **Run Tests with Coverage**: 2. **Run Tests with Coverage**:
| Framework | Command | | Framework | Command |
|-----------|---------| | --------- | -------------------------------------------------------------------- |
| Jest | `npx jest --coverage --json --outputFile=coverage/test-results.json` | | Jest | `npx jest --coverage --json --outputFile=coverage/test-results.json` |
| Vitest | `npx vitest run --coverage --reporter=json` | | Vitest | `npx vitest run --coverage --reporter=json` |
| Pytest | `pytest --cov --cov-report=json --json-report` | | Pytest | `pytest --cov --cov-report=json --json-report` |
| Go | `go test -v -cover -coverprofile=coverage.out ./...` | | Go | `go test -v -cover -coverprofile=coverage.out ./...` |
| Cargo | `cargo test -- --test-threads=1` | | Cargo | `cargo test -- --test-threads=1` |
3. **Parse Test Results**: 3. **Parse Test Results**:
Extract from test output: Extract from test output:
@@ -70,39 +71,41 @@ Detect the project's test framework, execute tests, and generate a comprehensive
- Suggested fix (if pattern is recognizable) - Suggested fix (if pattern is recognizable)
5. **Generate Report**: 5. **Generate Report**:
```markdown ```markdown
# Test Report # Test Report
**Date**: [timestamp] **Date**: [timestamp]
**Framework**: [detected] **Framework**: [detected]
**Status**: PASS | FAIL **Status**: PASS | FAIL
## Summary ## Summary
| Metric | Value | | Metric | Value |
|--------|-------| | ----------- | ----- |
| Total Tests | X | | Total Tests | X |
| Passed | X | | Passed | X |
| Failed | X | | Failed | X |
| Skipped | X | | Skipped | X |
| Duration | X.Xs | | Duration | X.Xs |
| Coverage | X% | | Coverage | X% |
## Failed Tests ## Failed Tests
### [test name] ### [test name]
**File**: `path/to/test.ts:42` **File**: `path/to/test.ts:42`
**Error**: Expected X but received Y **Error**: Expected X but received Y
**Suggestion**: Check mock setup for... **Suggestion**: Check mock setup for...
## Coverage by File ## Coverage by File
| File | Lines | Branches | Functions | | File | Lines | Branches | Functions |
|------|-------|----------|-----------| | ----------- | ----- | -------- | --------- |
| src/auth.ts | 85% | 70% | 90% | | src/auth.ts | 85% | 70% | 90% |
## Next Actions ## Next Actions
1. Fix failing test: [name] 1. Fix failing test: [name]
2. Increase coverage in: [low coverage files] 2. Increase coverage in: [low coverage files]
``` ```
+22 -21
View File
@@ -46,37 +46,38 @@ Post-implementation validation that compares code against spec requirements.
4. **Validation Checks**: 4. **Validation Checks**:
| Check | Method | | Check | Method |
|-------|--------| | -------------------- | ------------------------------------------------ |
| Requirement Coverage | Each requirement has ≥1 implementation reference | | Requirement Coverage | Each requirement has ≥1 implementation reference |
| Acceptance Criteria | Each criterion is testable in code | | Acceptance Criteria | Each criterion is testable in code |
| Edge Case Handling | Each edge case has explicit handling code | | Edge Case Handling | Each edge case has explicit handling code |
| Test Coverage | Each requirement has ≥1 test | | Test Coverage | Each requirement has ≥1 test |
5. **Generate Validation Report**: 5. **Generate Validation Report**:
```markdown ```markdown
# Validation Report: [Feature Name] # Validation Report: [Feature Name]
**Date**: [timestamp] **Date**: [timestamp]
**Status**: PASS | PARTIAL | FAIL **Status**: PASS | PARTIAL | FAIL
## Coverage Summary ## Coverage Summary
| Metric | Count | Percentage | | Metric | Count | Percentage |
|--------|-------|------------| | ----------------------- | ----- | ---------- |
| Requirements Covered | X/Y | Z% | | Requirements Covered | X/Y | Z% |
| Acceptance Criteria Met | X/Y | Z% | | Acceptance Criteria Met | X/Y | Z% |
| Edge Cases Handled | X/Y | Z% | | Edge Cases Handled | X/Y | Z% |
| Tests Present | X/Y | Z% | | Tests Present | X/Y | Z% |
## Uncovered Requirements ## Uncovered Requirements
| Requirement | Status | Notes | | Requirement | Status | Notes |
|-------------|--------|-------| | ----------- | ------- | ----------------------- |
| [REQ-001] | Missing | No implementation found | | [REQ-001] | Missing | No implementation found |
## Recommendations ## Recommendations
1. [Action item for gaps] 1. [Action item for gaps]
``` ```
+1 -1
View File
@@ -12,4 +12,4 @@
"ui": { "ui": {
"showStatusInTitle": true "showStatusInTitle": true
} }
} }
+23
View File
@@ -1,50 +1,73 @@
# [PROJECT_NAME] Constitution # [PROJECT_NAME] Constitution
<!-- Example: Spec Constitution, TaskFlow Constitution, etc. --> <!-- Example: Spec Constitution, TaskFlow Constitution, etc. -->
## Core Principles ## Core Principles
### [PRINCIPLE_1_NAME] ### [PRINCIPLE_1_NAME]
<!-- Example: I. Library-First --> <!-- Example: I. Library-First -->
[PRINCIPLE_1_DESCRIPTION] [PRINCIPLE_1_DESCRIPTION]
<!-- Example: Every feature starts as a standalone library; Libraries must be self-contained, independently testable, documented; Clear purpose required - no organizational-only libraries --> <!-- Example: Every feature starts as a standalone library; Libraries must be self-contained, independently testable, documented; Clear purpose required - no organizational-only libraries -->
### [PRINCIPLE_2_NAME] ### [PRINCIPLE_2_NAME]
<!-- Example: II. CLI Interface --> <!-- Example: II. CLI Interface -->
[PRINCIPLE_2_DESCRIPTION] [PRINCIPLE_2_DESCRIPTION]
<!-- Example: Every library exposes functionality via CLI; Text in/out protocol: stdin/args → stdout, errors → stderr; Support JSON + human-readable formats --> <!-- Example: Every library exposes functionality via CLI; Text in/out protocol: stdin/args → stdout, errors → stderr; Support JSON + human-readable formats -->
### [PRINCIPLE_3_NAME] ### [PRINCIPLE_3_NAME]
<!-- Example: III. Test-First (NON-NEGOTIABLE) --> <!-- Example: III. Test-First (NON-NEGOTIABLE) -->
[PRINCIPLE_3_DESCRIPTION] [PRINCIPLE_3_DESCRIPTION]
<!-- Example: TDD mandatory: Tests written → User approved → Tests fail → Then implement; Red-Green-Refactor cycle strictly enforced --> <!-- Example: TDD mandatory: Tests written → User approved → Tests fail → Then implement; Red-Green-Refactor cycle strictly enforced -->
### [PRINCIPLE_4_NAME] ### [PRINCIPLE_4_NAME]
<!-- Example: IV. Integration Testing --> <!-- Example: IV. Integration Testing -->
[PRINCIPLE_4_DESCRIPTION] [PRINCIPLE_4_DESCRIPTION]
<!-- Example: Focus areas requiring integration tests: New library contract tests, Contract changes, Inter-service communication, Shared schemas --> <!-- Example: Focus areas requiring integration tests: New library contract tests, Contract changes, Inter-service communication, Shared schemas -->
### [PRINCIPLE_5_NAME] ### [PRINCIPLE_5_NAME]
<!-- Example: V. Observability, VI. Versioning & Breaking Changes, VII. Simplicity --> <!-- Example: V. Observability, VI. Versioning & Breaking Changes, VII. Simplicity -->
[PRINCIPLE_5_DESCRIPTION] [PRINCIPLE_5_DESCRIPTION]
<!-- Example: Text I/O ensures debuggability; Structured logging required; Or: MAJOR.MINOR.BUILD format; Or: Start simple, YAGNI principles --> <!-- Example: Text I/O ensures debuggability; Structured logging required; Or: MAJOR.MINOR.BUILD format; Or: Start simple, YAGNI principles -->
## [SECTION_2_NAME] ## [SECTION_2_NAME]
<!-- Example: Additional Constraints, Security Requirements, Performance Standards, etc. --> <!-- Example: Additional Constraints, Security Requirements, Performance Standards, etc. -->
[SECTION_2_CONTENT] [SECTION_2_CONTENT]
<!-- Example: Technology stack requirements, compliance standards, deployment policies, etc. --> <!-- Example: Technology stack requirements, compliance standards, deployment policies, etc. -->
## [SECTION_3_NAME] ## [SECTION_3_NAME]
<!-- Example: Development Workflow, Review Process, Quality Gates, etc. --> <!-- Example: Development Workflow, Review Process, Quality Gates, etc. -->
[SECTION_3_CONTENT] [SECTION_3_CONTENT]
<!-- Example: Code review requirements, testing gates, deployment approval process, etc. --> <!-- Example: Code review requirements, testing gates, deployment approval process, etc. -->
## Governance ## Governance
<!-- Example: Constitution supersedes all other practices; Amendments require documentation, approval, migration plan --> <!-- Example: Constitution supersedes all other practices; Amendments require documentation, approval, migration plan -->
[GOVERNANCE_RULES] [GOVERNANCE_RULES]
<!-- Example: All PRs/reviews must verify compliance; Complexity must be justified; Use [GUIDANCE_FILE] for runtime development guidance --> <!-- Example: All PRs/reviews must verify compliance; Complexity must be justified; Use [GUIDANCE_FILE] for runtime development guidance -->
**Version**: [CONSTITUTION_VERSION] | **Ratified**: [RATIFICATION_DATE] | **Last Amended**: [LAST_AMENDED_DATE] **Version**: [CONSTITUTION_VERSION] | **Ratified**: [RATIFICATION_DATE] | **Last Amended**: [LAST_AMENDED_DATE]
<!-- Example: Version: 2.1.1 | Ratified: 2025-06-13 | Last Amended: 2025-07-16 --> <!-- Example: Version: 2.1.1 | Ratified: 2025-06-13 | Last Amended: 2025-07-16 -->
+3 -3
View File
@@ -6,16 +6,16 @@
**Note**: This checklist is generated by the `/speckit.checklist` command based on feature context and requirements. **Note**: This checklist is generated by the `/speckit.checklist` command based on feature context and requirements.
<!-- <!--
============================================================================ ============================================================================
IMPORTANT: The checklist items below are SAMPLE ITEMS for illustration only. IMPORTANT: The checklist items below are SAMPLE ITEMS for illustration only.
The /speckit.checklist command MUST replace these with actual items based on: The /speckit.checklist command MUST replace these with actual items based on:
- User's specific checklist request - User's specific checklist request
- Feature requirements from spec.md - Feature requirements from spec.md
- Technical context from plan.md - Technical context from plan.md
- Implementation details from tasks.md - Implementation details from tasks.md
DO NOT keep these sample items in the generated checklist file. DO NOT keep these sample items in the generated checklist file.
============================================================================ ============================================================================
--> -->
+6 -5
View File
@@ -29,7 +29,7 @@
## Constitution Check ## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* _GATE: Must pass before Phase 0 research. Re-check after Phase 1 design._
[Gates determined based on constitution file] [Gates determined based on constitution file]
@@ -48,6 +48,7 @@ specs/[###-feature]/
``` ```
### Source Code (repository root) ### Source Code (repository root)
<!-- <!--
ACTION REQUIRED: Replace the placeholder tree below with the concrete layout ACTION REQUIRED: Replace the placeholder tree below with the concrete layout
for this feature. Delete unused options and expand the chosen structure with for this feature. Delete unused options and expand the chosen structure with
@@ -98,7 +99,7 @@ directories captured above]
> **Fill ONLY if Constitution Check has violations that must be justified** > **Fill ONLY if Constitution Check has violations that must be justified**
| Violation | Why Needed | Simpler Alternative Rejected Because | | Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|-------------------------------------| | -------------------------- | ------------------ | ------------------------------------ |
| [e.g., 4th project] | [current need] | [why 3 projects insufficient] | | [e.g., 4th project] | [current need] | [why 3 projects insufficient] |
| [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] | | [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] |
+7 -7
View File
@@ -5,13 +5,13 @@
**Status**: Draft **Status**: Draft
**Input**: User description: "$ARGUMENTS" **Input**: User description: "$ARGUMENTS"
## User Scenarios & Testing *(mandatory)* ## User Scenarios & Testing _(mandatory)_
<!-- <!--
IMPORTANT: User stories should be PRIORITIZED as user journeys ordered by importance. IMPORTANT: User stories should be PRIORITIZED as user journeys ordered by importance.
Each user story/journey must be INDEPENDENTLY TESTABLE - meaning if you implement just ONE of them, Each user story/journey must be INDEPENDENTLY TESTABLE - meaning if you implement just ONE of them,
you should still have a viable MVP (Minimum Viable Product) that delivers value. you should still have a viable MVP (Minimum Viable Product) that delivers value.
Assign priorities (P1, P2, P3, etc.) to each story, where P1 is the most critical. Assign priorities (P1, P2, P3, etc.) to each story, where P1 is the most critical.
Think of each story as a standalone slice of functionality that can be: Think of each story as a standalone slice of functionality that can be:
- Developed independently - Developed independently
@@ -75,7 +75,7 @@
- What happens when [boundary condition]? - What happens when [boundary condition]?
- How does system handle [error scenario]? - How does system handle [error scenario]?
## Requirements *(mandatory)* ## Requirements _(mandatory)_
<!-- <!--
ACTION REQUIRED: The content in this section represents placeholders. ACTION REQUIRED: The content in this section represents placeholders.
@@ -85,22 +85,22 @@
### Functional Requirements ### Functional Requirements
- **FR-001**: System MUST [specific capability, e.g., "allow users to create accounts"] - **FR-001**: System MUST [specific capability, e.g., "allow users to create accounts"]
- **FR-002**: System MUST [specific capability, e.g., "validate email addresses"] - **FR-002**: System MUST [specific capability, e.g., "validate email addresses"]
- **FR-003**: Users MUST be able to [key interaction, e.g., "reset their password"] - **FR-003**: Users MUST be able to [key interaction, e.g., "reset their password"]
- **FR-004**: System MUST [data requirement, e.g., "persist user preferences"] - **FR-004**: System MUST [data requirement, e.g., "persist user preferences"]
- **FR-005**: System MUST [behavior, e.g., "log all security events"] - **FR-005**: System MUST [behavior, e.g., "log all security events"]
*Example of marking unclear requirements:* _Example of marking unclear requirements:_
- **FR-006**: System MUST authenticate users via [NEEDS CLARIFICATION: auth method not specified - email/password, SSO, OAuth?] - **FR-006**: System MUST authenticate users via [NEEDS CLARIFICATION: auth method not specified - email/password, SSO, OAuth?]
- **FR-007**: System MUST retain user data for [NEEDS CLARIFICATION: retention period not specified] - **FR-007**: System MUST retain user data for [NEEDS CLARIFICATION: retention period not specified]
### Key Entities *(include if feature involves data)* ### Key Entities _(include if feature involves data)_
- **[Entity 1]**: [What it represents, key attributes without implementation] - **[Entity 1]**: [What it represents, key attributes without implementation]
- **[Entity 2]**: [What it represents, relationships to other entities] - **[Entity 2]**: [What it represents, relationships to other entities]
## Success Criteria *(mandatory)* ## Success Criteria _(mandatory)_
<!-- <!--
ACTION REQUIRED: Define measurable success criteria. ACTION REQUIRED: Define measurable success criteria.
+11 -12
View File
@@ -1,6 +1,5 @@
--- ---
description: 'Task list template for feature implementation'
description: "Task list template for feature implementation"
--- ---
# Tasks: [FEATURE NAME] # Tasks: [FEATURE NAME]
@@ -25,21 +24,21 @@ description: "Task list template for feature implementation"
- **Mobile**: `api/src/`, `ios/src/` or `android/src/` - **Mobile**: `api/src/`, `ios/src/` or `android/src/`
- Paths shown below assume single project - adjust based on plan.md structure - Paths shown below assume single project - adjust based on plan.md structure
<!-- <!--
============================================================================ ============================================================================
IMPORTANT: The tasks below are SAMPLE TASKS for illustration purposes only. IMPORTANT: The tasks below are SAMPLE TASKS for illustration purposes only.
The /speckit.tasks command MUST replace these with actual tasks based on: The /speckit.tasks command MUST replace these with actual tasks based on:
- User stories from spec.md (with their priorities P1, P2, P3...) - User stories from spec.md (with their priorities P1, P2, P3...)
- Feature requirements from plan.md - Feature requirements from plan.md
- Entities from data-model.md - Entities from data-model.md
- Endpoints from contracts/ - Endpoints from contracts/
Tasks MUST be organized by user story so each story can be: Tasks MUST be organized by user story so each story can be:
- Implemented independently - Implemented independently
- Tested independently - Tested independently
- Delivered as an MVP increment - Delivered as an MVP increment
DO NOT keep these sample tasks in the generated tasks.md file. DO NOT keep these sample tasks in the generated tasks.md file.
============================================================================ ============================================================================
--> -->
@@ -83,8 +82,8 @@ Examples of foundational tasks (adjust based on your project):
> **NOTE: Write these tests FIRST, ensure they FAIL before implementation** > **NOTE: Write these tests FIRST, ensure they FAIL before implementation**
- [ ] T010 [P] [US1] Contract test for [endpoint] in tests/contract/test_[name].py - [ ] T010 [P] [US1] Contract test for [endpoint] in tests/contract/test\_[name].py
- [ ] T011 [P] [US1] Integration test for [user journey] in tests/integration/test_[name].py - [ ] T011 [P] [US1] Integration test for [user journey] in tests/integration/test\_[name].py
### Implementation for User Story 1 ### Implementation for User Story 1
@@ -107,8 +106,8 @@ Examples of foundational tasks (adjust based on your project):
### Tests for User Story 2 (OPTIONAL - only if tests requested) ⚠️ ### Tests for User Story 2 (OPTIONAL - only if tests requested) ⚠️
- [ ] T018 [P] [US2] Contract test for [endpoint] in tests/contract/test_[name].py - [ ] T018 [P] [US2] Contract test for [endpoint] in tests/contract/test\_[name].py
- [ ] T019 [P] [US2] Integration test for [user journey] in tests/integration/test_[name].py - [ ] T019 [P] [US2] Integration test for [user journey] in tests/integration/test\_[name].py
### Implementation for User Story 2 ### Implementation for User Story 2
@@ -129,8 +128,8 @@ Examples of foundational tasks (adjust based on your project):
### Tests for User Story 3 (OPTIONAL - only if tests requested) ⚠️ ### Tests for User Story 3 (OPTIONAL - only if tests requested) ⚠️
- [ ] T024 [P] [US3] Contract test for [endpoint] in tests/contract/test_[name].py - [ ] T024 [P] [US3] Contract test for [endpoint] in tests/contract/test\_[name].py
- [ ] T025 [P] [US3] Integration test for [user journey] in tests/integration/test_[name].py - [ ] T025 [P] [US3] Integration test for [user journey] in tests/integration/test\_[name].py
### Implementation for User Story 3 ### Implementation for User Story 3
+4 -1
View File
@@ -2,9 +2,11 @@
auto_execution_mode: 0 auto_execution_mode: 0
description: Review code changes for bugs, security issues, and improvements description: Review code changes for bugs, security issues, and improvements
--- ---
You are a senior software engineer performing a thorough code review to identify potential bugs. You are a senior software engineer performing a thorough code review to identify potential bugs.
Your task is to find all potential bugs and code improvements in the code changes. Focus on: Your task is to find all potential bugs and code improvements in the code changes. Focus on:
1. Logic errors and incorrect behavior 1. Logic errors and incorrect behavior
2. Edge cases that aren't handled 2. Edge cases that aren't handled
3. Null/undefined reference issues 3. Null/undefined reference issues
@@ -16,7 +18,8 @@ Your task is to find all potential bugs and code improvements in the code change
9. Violations of existing code patterns or conventions 9. Violations of existing code patterns or conventions
Make sure to: Make sure to:
1. If exploring the codebase, call multiple tools in parallel for increased efficiency. Do not spend too much time exploring. 1. If exploring the codebase, call multiple tools in parallel for increased efficiency. Do not spend too much time exploring.
2. If you find any pre-existing bugs in the code, you should also report those since it's important for us to maintain general code quality for the user. 2. If you find any pre-existing bugs in the code, you should also report those since it's important for us to maintain general code quality for the user.
3. Do NOT report issues that are speculative or low-confidence. All your conclusions should be based on a complete understanding of the codebase. 3. Do NOT report issues that are speculative or low-confidence. All your conclusions should be based on a complete understanding of the codebase.
4. Remember that if you were given a specific git commit, it may not be checked out and local code states may be different. 4. Remember that if you were given a specific git commit, it may not be checked out and local code states may be different.
+1
View File
@@ -5,6 +5,7 @@
### Document Numbering System Fixes (2026-03-21) ### Document Numbering System Fixes (2026-03-21)
#### 🔢 **Template Management Hardening** #### 🔢 **Template Management Hardening**
- **Issue**: Save/Edit functionality failing due to missing fields and data complexity. - **Issue**: Save/Edit functionality failing due to missing fields and data complexity.
- **Fix (Backend)**: Added `disciplineId` and `isActive` to `DocumentNumberFormat` entity. - **Fix (Backend)**: Added `disciplineId` and `isActive` to `DocumentNumberFormat` entity.
- **Fix (Backend)**: Implemented automated "Upsert" logic in `DocumentNumberingService` to handle business keys (Project + Type + Discipline). - **Fix (Backend)**: Implemented automated "Upsert" logic in `DocumentNumberingService` to handle business keys (Project + Type + Discipline).
+5 -5
View File
@@ -536,11 +536,11 @@ graph LR
**Document History**: **Document History**:
| Version | Date | Author | Changes | | Version | Date | Author | Changes |
| ------- | ---------- | ---------- | -------------------------------------- | | ------- | ---------- | ---------- | ------------------------------------------------------- |
| 1.0.0 | 2025-01-15 | John Doe | Initial version | | 1.0.0 | 2025-01-15 | John Doe | Initial version |
| 1.1.0 | 2025-02-20 | Jane Smith | Add CC support | | 1.1.0 | 2025-02-20 | Jane Smith | Add CC support |
| 1.2.0 | 2025-03-10 | John Doe | Update workflow | | 1.2.0 | 2025-03-10 | John Doe | Update workflow |
| 1.8.1 | 2026-03-21 | Tech Lead | Security hardening, numbering fixes, dependency updates | | 1.8.1 | 2026-03-21 | Tech Lead | Security hardening, numbering fixes, dependency updates |
**Current Version**: 1.8.1 **Current Version**: 1.8.1
+30
View File
@@ -0,0 +1,30 @@
const fs = require('fs');
const report = fs.readFileSync('eslint_report_v7.txt', 'utf8');
const lines = report.split('\n');
const files = {};
let currentFile = null;
lines.forEach((line) => {
const trimmed = line.trim();
if (!trimmed) return;
// Check if line is a filename (starts with D:\)
if (trimmed.startsWith('D:\\')) {
currentFile = trimmed;
return;
}
// Check if line is an error with "any"
if (currentFile && (trimmed.includes('no-unsafe') || trimmed.includes('no-explicit-any'))) {
files[currentFile] = (files[currentFile] || 0) + 1;
}
});
const sorted = Object.entries(files).sort((a, b) => b[1] - a[1]);
console.log('Top 20 files with "any" issues:');
console.log(
sorted
.slice(0, 20)
.map(([file, count]) => `${count} - ${file}`)
.join('\n')
);
+21
View File
@@ -0,0 +1,21 @@
Top 20 files with "any" issues:
29 - D:\nap-dms.lcbp3\backend\src\modules\correspondence\correspondence.service.spec.ts
13 - D:\nap-dms.lcbp3\backend\src\modules\migration\migration.service.ts
10 - D:\nap-dms.lcbp3\backend\src\modules\dashboard\dashboard.service.ts
8 - D:\nap-dms.lcbp3\backend\src\modules\document-numbering\document-numbering.service.spec.ts
8 - D:\nap-dms.lcbp3\backend\test\phase3-workflow.e2e-spec.ts
6 - D:\nap-dms.lcbp3\backend\src\common\decorators\retry.decorator.ts
6 - D:\nap-dms.lcbp3\backend\src\modules\workflow-engine\workflow-engine.service.ts
5 - D:\nap-dms.lcbp3\backend\src\common\auth\auth.service.spec.ts
5 - D:\nap-dms.lcbp3\backend\src\common\auth\guards\permissions.guard.ts
5 - D:\nap-dms.lcbp3\backend\src\modules\user\user.service.ts
3 - D:\nap-dms.lcbp3\backend\src\common\decorators\circuit-breaker.decorator.ts
3 - D:\nap-dms.lcbp3\backend\src\common\decorators\current-user.decorator.ts
3 - D:\nap-dms.lcbp3\backend\src\common\guards\rbac.guard.ts
3 - D:\nap-dms.lcbp3\backend\src\modules\json-schema\dto\create-json-schema.dto.ts
3 - D:\nap-dms.lcbp3\backend\src\modules\search\search.service.ts
2 - D:\nap-dms.lcbp3\backend\src\common\guards\maintenance-mode.guard.ts
2 - D:\nap-dms.lcbp3\backend\src\common\services\crypto.service.ts
2 - D:\nap-dms.lcbp3\backend\src\common\services\request-context.service.ts
2 - D:\nap-dms.lcbp3\backend\src\database\seeds\run-seed.ts
2 - D:\nap-dms.lcbp3\backend\src\database\seeds\user.seed.ts
+21
View File
@@ -0,0 +1,21 @@
Top 20 files with "any" issues:
29 - D:\nap-dms.lcbp3\backend\src\modules\correspondence\correspondence.service.spec.ts
8 - D:\nap-dms.lcbp3\backend\src\modules\document-numbering\document-numbering.service.spec.ts
8 - D:\nap-dms.lcbp3\backend\test\phase3-workflow.e2e-spec.ts
5 - D:\nap-dms.lcbp3\backend\src\common\auth\auth.service.spec.ts
3 - D:\nap-dms.lcbp3\backend\src\common\decorators\circuit-breaker.decorator.ts
3 - D:\nap-dms.lcbp3\backend\src\common\decorators\current-user.decorator.ts
3 - D:\nap-dms.lcbp3\backend\src\common\guards\rbac.guard.ts
3 - D:\nap-dms.lcbp3\backend\src\modules\json-schema\dto\create-json-schema.dto.ts
3 - D:\nap-dms.lcbp3\backend\src\modules\search\search.service.ts
2 - D:\nap-dms.lcbp3\backend\src\common\guards\maintenance-mode.guard.ts
2 - D:\nap-dms.lcbp3\backend\src\common\services\crypto.service.ts
2 - D:\nap-dms.lcbp3\backend\src\common\services\request-context.service.ts
2 - D:\nap-dms.lcbp3\backend\src\database\seeds\run-seed.ts
2 - D:\nap-dms.lcbp3\backend\src\database\seeds\user.seed.ts
2 - D:\nap-dms.lcbp3\backend\src\modules\json-schema\services\virtual-column.service.ts
2 - D:\nap-dms.lcbp3\backend\src\modules\project\project.service.spec.ts
2 - D:\nap-dms.lcbp3\backend\src\modules\rfa\rfa-workflow.service.ts
2 - D:\nap-dms.lcbp3\backend\src\modules\workflow-engine\dsl\parser.service.ts
2 - D:\nap-dms.lcbp3\backend\src\scripts\migrate-storage-v2.ts
1 - D:\nap-dms.lcbp3\backend\src\common\interceptors\audit-log.interceptor.ts
+2
View File
@@ -0,0 +1,2 @@
Top 20 files with "any" issues:
+21
View File
@@ -0,0 +1,21 @@
Top 20 files with "any" issues:
29 - D:\nap-dms.lcbp3\backend\src\modules\correspondence\correspondence.service.spec.ts
8 - D:\nap-dms.lcbp3\backend\src\modules\document-numbering\document-numbering.service.spec.ts
8 - D:\nap-dms.lcbp3\backend\test\phase3-workflow.e2e-spec.ts
5 - D:\nap-dms.lcbp3\backend\src\common\auth\auth.service.spec.ts
3 - D:\nap-dms.lcbp3\backend\src\modules\json-schema\dto\create-json-schema.dto.ts
3 - D:\nap-dms.lcbp3\backend\src\modules\search\search.service.ts
2 - D:\nap-dms.lcbp3\backend\src\common\guards\maintenance-mode.guard.ts
2 - D:\nap-dms.lcbp3\backend\src\common\services\crypto.service.ts
2 - D:\nap-dms.lcbp3\backend\src\common\services\request-context.service.ts
2 - D:\nap-dms.lcbp3\backend\src\database\seeds\run-seed.ts
2 - D:\nap-dms.lcbp3\backend\src\database\seeds\user.seed.ts
2 - D:\nap-dms.lcbp3\backend\src\modules\json-schema\services\virtual-column.service.ts
2 - D:\nap-dms.lcbp3\backend\src\modules\project\project.service.spec.ts
2 - D:\nap-dms.lcbp3\backend\src\modules\workflow-engine\dsl\parser.service.ts
2 - D:\nap-dms.lcbp3\backend\src\scripts\migrate-storage-v2.ts
1 - D:\nap-dms.lcbp3\backend\src\common\interceptors\audit-log.interceptor.ts
1 - D:\nap-dms.lcbp3\backend\src\common\interceptors\idempotency.interceptor.ts
1 - D:\nap-dms.lcbp3\backend\src\modules\json-schema\dto\search-json-schema.dto.ts
1 - D:\nap-dms.lcbp3\backend\src\modules\json-schema\json-schema.service.ts
1 - D:\nap-dms.lcbp3\backend\src\modules\notification\dto\search-notification.dto.ts
+8 -6
View File
@@ -11,13 +11,14 @@ services:
restart: always restart: always
command: redis-server --save 60 1 --loglevel warning --requirepass "${REDIS_PASSWORD:-redis_password}" command: redis-server --save 60 1 --loglevel warning --requirepass "${REDIS_PASSWORD:-redis_password}"
ports: ports:
- "6379:6379" - '6379:6379'
volumes: volumes:
- redis_data:/data - redis_data:/data
networks: networks:
- lcbp3_net - lcbp3_net
healthcheck: healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-redis_password}", "ping"] test:
['CMD', 'redis-cli', '-a', '${REDIS_PASSWORD:-redis_password}', 'ping']
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
@@ -35,7 +36,7 @@ services:
- cluster.name=lcbp3_es_cluster - cluster.name=lcbp3_es_cluster
- discovery.type=single-node # รันแบบ Node เดียวสำหรับ Dev/Phase 0 - discovery.type=single-node # รันแบบ Node เดียวสำหรับ Dev/Phase 0
- bootstrap.memory_lock=true # ล็อคหน่วยความจำเพื่อประสิทธิภาพ - bootstrap.memory_lock=true # ล็อคหน่วยความจำเพื่อประสิทธิภาพ
- "ES_JAVA_OPTS=-Xms512m -Xmx512m" # กำหนด Heap Size (ปรับเพิ่มได้ตาม Resource เครื่อง) - 'ES_JAVA_OPTS=-Xms512m -Xmx512m' # กำหนด Heap Size (ปรับเพิ่มได้ตาม Resource เครื่อง)
- xpack.security.enabled=false # ปิด Security ชั่วคราวสำหรับ Phase 0 (ควรเปิดใน Production) - xpack.security.enabled=false # ปิด Security ชั่วคราวสำหรับ Phase 0 (ควรเปิดใน Production)
- xpack.security.http.ssl.enabled=false - xpack.security.http.ssl.enabled=false
ulimits: ulimits:
@@ -45,11 +46,12 @@ services:
volumes: volumes:
- es_data:/usr/share/elasticsearch/data - es_data:/usr/share/elasticsearch/data
ports: ports:
- "9200:9200" - '9200:9200'
networks: networks:
- lcbp3_net - lcbp3_net
healthcheck: healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:9200/_cluster/health || exit 1"] test:
['CMD-SHELL', 'curl -f http://localhost:9200/_cluster/health || exit 1']
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 5 retries: 5
@@ -73,4 +75,4 @@ volumes:
networks: networks:
lcbp3_net: lcbp3_net:
driver: bridge driver: bridge
name: lcbp3_network name: lcbp3_network
View File
+21
View File
@@ -0,0 +1,21 @@
Top 20 files with "any" issues:
29 - D:\nap-dms.lcbp3\backend\src\modules\correspondence\correspondence.service.spec.ts
8 - D:\nap-dms.lcbp3\backend\src\modules\document-numbering\document-numbering.service.spec.ts
8 - D:\nap-dms.lcbp3\backend\test\phase3-workflow.e2e-spec.ts
5 - D:\nap-dms.lcbp3\backend\src\common\auth\auth.service.spec.ts
3 - D:\nap-dms.lcbp3\backend\src\modules\json-schema\dto\create-json-schema.dto.ts
2 - D:\nap-dms.lcbp3\backend\src\database\seeds\run-seed.ts
2 - D:\nap-dms.lcbp3\backend\src\database\seeds\user.seed.ts
2 - D:\nap-dms.lcbp3\backend\src\modules\json-schema\services\virtual-column.service.ts
2 - D:\nap-dms.lcbp3\backend\src\modules\project\project.service.spec.ts
2 - D:\nap-dms.lcbp3\backend\src\modules\workflow-engine\dsl\parser.service.ts
2 - D:\nap-dms.lcbp3\backend\src\scripts\migrate-storage-v2.ts
1 - D:\nap-dms.lcbp3\backend\src\common\interceptors\audit-log.interceptor.ts
1 - D:\nap-dms.lcbp3\backend\src\modules\json-schema\dto\search-json-schema.dto.ts
1 - D:\nap-dms.lcbp3\backend\src\modules\json-schema\json-schema.service.ts
1 - D:\nap-dms.lcbp3\backend\src\modules\notification\dto\search-notification.dto.ts
1 - D:\nap-dms.lcbp3\backend\src\modules\project\dto\search-project.dto.ts
1 - D:\nap-dms.lcbp3\backend\src\modules\rfa\entities\rfa-workflow-template.entity.ts
1 - D:\nap-dms.lcbp3\backend\src\modules\rfa\entities\rfa-workflow.entity.ts
1 - D:\nap-dms.lcbp3\backend\src\modules\workflow-engine\dto\evaluate-workflow.dto.ts
1 - D:\nap-dms.lcbp3\backend\src\modules\workflow-engine\entities\workflow-history.entity.ts
+14
View File
@@ -29,6 +29,14 @@ export default tseslint.config(
'@typescript-eslint/no-explicit-any': 'error', '@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-floating-promises': 'error', '@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-unsafe-argument': 'error', '@typescript-eslint/no-unsafe-argument': 'error',
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
},
],
'no-console': 'error', 'no-console': 'error',
'prettier/prettier': ['error', { endOfLine: 'auto' }], 'prettier/prettier': ['error', { endOfLine: 'auto' }],
'no-restricted-syntax': [ 'no-restricted-syntax': [
@@ -44,4 +52,10 @@ export default tseslint.config(
], ],
}, },
}, },
{
files: ['**/*.spec.ts', '**/*.e2e-spec.ts'],
rules: {
'@typescript-eslint/unbound-method': 'off',
},
}
); );
+344
View File
@@ -0,0 +1,344 @@
> backend@1.8.0 lint D:\nap-dms.lcbp3\backend
> eslint "{src,apps,libs,test}/**/*.ts" --fix
D:\nap-dms.lcbp3\backend\src\common\auth\auth.controller.spec.ts
81:13 error 'result' is assigned a value but never used @typescript-eslint/no-unused-vars
D:\nap-dms.lcbp3\backend\src\common\auth\auth.service.spec.ts
21:7 error Unsafe assignment of an `any` value @typescript-eslint/no-unsafe-assignment
26:7 error 'jwtService' is assigned a value but never used @typescript-eslint/no-unused-vars
27:7 error 'tokenRepo' is assigned a value but never used @typescript-eslint/no-unused-vars
56:5 error Unsafe call of an `any` typed value @typescript-eslint/no-unsafe-call
56:12 error Unsafe member access .compare on an `any` value @typescript-eslint/no-unsafe-member-access
131:7 error Unsafe call of an `any` typed value @typescript-eslint/no-unsafe-call
131:14 error Unsafe member access .compare on an `any` value @typescript-eslint/no-unsafe-member-access
165:14 error A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.
Consider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value.
If a function does not access `this`, it can be annotated with `this: void` @typescript-eslint/unbound-method
D:\nap-dms.lcbp3\backend\src\common\auth\strategies\jwt-refresh.strategy.ts
27:3 error Async method 'validate' has no 'await' expression @typescript-eslint/require-await
D:\nap-dms.lcbp3\backend\src\common\file-storage\file-storage.controller.spec.ts
51:13 error 'result' is assigned a value but never used @typescript-eslint/no-unused-vars
D:\nap-dms.lcbp3\backend\src\common\file-storage\file-storage.service.spec.ts
88:14 error A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.
Consider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value.
If a function does not access `this`, it can be annotated with `this: void` @typescript-eslint/unbound-method
89:14 error A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.
Consider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value.
If a function does not access `this`, it can be annotated with `this: void` @typescript-eslint/unbound-method
121:14 error A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.
Consider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value.
If a function does not access `this`, it can be annotated with `this: void` @typescript-eslint/unbound-method
139:14 error A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.
Consider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value.
If a function does not access `this`, it can be annotated with `this: void` @typescript-eslint/unbound-method
D:\nap-dms.lcbp3\backend\src\common\interceptors\audit-log.interceptor.ts
60:11 error Unsafe assignment of an `any` value @typescript-eslint/no-unsafe-assignment
D:\nap-dms.lcbp3\backend\src\database\seeds\run-seed.ts
7:37 error Unsafe argument of type `any` assigned to a parameter of type `DataSourceOptions` @typescript-eslint/no-unsafe-argument
7:55 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
11:5 error Unexpected console statement no-console
16:5 error Unexpected console statement no-console
18:5 error Unexpected console statement no-console
24:1 error Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator @typescript-eslint/no-floating-promises
D:\nap-dms.lcbp3\backend\src\database\seeds\user.seed.ts
61:7 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment
115:13 error Unsafe assignment of an `any` value @typescript-eslint/no-unsafe-assignment
119:11 error Unsafe assignment of an `any` value @typescript-eslint/no-unsafe-assignment
D:\nap-dms.lcbp3\backend\src\database\seeds\workflow-definitions.seed.ts
133:9 error Unexpected console statement no-console
135:9 error Unexpected console statement no-console
138:7 error Unexpected console statement no-console
D:\nap-dms.lcbp3\backend\src\modules\circulation\circulation-workflow.service.ts
89:57 error Invalid type "unknown" of template literal expression @typescript-eslint/restrict-template-expressions
D:\nap-dms.lcbp3\backend\src\modules\circulation\circulation.service.ts
8:34 error 'Not' is defined but never used @typescript-eslint/no-unused-vars
98:13 error 'search' is assigned a value but never used @typescript-eslint/no-unused-vars
D:\nap-dms.lcbp3\backend\src\modules\contract\contract.controller.ts
16:3 error 'ApiQuery' is defined but never used @typescript-eslint/no-unused-vars
D:\nap-dms.lcbp3\backend\src\modules\correspondence\correspondence-workflow.service.ts
91:55 error Invalid type "unknown" of template literal expression @typescript-eslint/restrict-template-expressions
D:\nap-dms.lcbp3\backend\src\modules\correspondence\correspondence.controller.spec.ts
82:13 error 'result' is assigned a value but never used @typescript-eslint/no-unused-vars
D:\nap-dms.lcbp3\backend\src\modules\correspondence\correspondence.service.spec.ts
11:10 error 'CorrespondenceRecipient' is defined but never used @typescript-eslint/no-unused-vars
22:27 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
23:21 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
24:7 error 'dataSource' is assigned a value but never used @typescript-eslint/no-unused-vars
24:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
133:5 error Unsafe assignment of an `any` value @typescript-eslint/no-unsafe-assignment
134:5 error Unsafe assignment of an `any` value @typescript-eslint/no-unsafe-assignment
135:5 error Unsafe assignment of an `any` value @typescript-eslint/no-unsafe-assignment
144:13 error Unsafe assignment of an `any` value @typescript-eslint/no-unsafe-assignment
144:69 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
154:13 error 'mockStatus' is assigned a value but never used @typescript-eslint/no-unused-vars
177:31 error Unsafe argument of type `any` assigned to a parameter of type `UpdateCorrespondenceDto` @typescript-eslint/no-unsafe-argument
177:44 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
177:49 error Unsafe argument of type `any` assigned to a parameter of type `User` @typescript-eslint/no-unsafe-argument
180:14 error A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.
Consider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value.
If a function does not access `this`, it can be annotated with `this: void` @typescript-eslint/unbound-method
184:13 error Unsafe assignment of an `any` value @typescript-eslint/no-unsafe-assignment
184:69 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
208:31 error Unsafe argument of type `any` assigned to a parameter of type `UpdateCorrespondenceDto` @typescript-eslint/no-unsafe-argument
208:44 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
208:49 error Unsafe argument of type `any` assigned to a parameter of type `User` @typescript-eslint/no-unsafe-argument
210:14 error A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.
Consider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value.
If a function does not access `this`, it can be annotated with `this: void` @typescript-eslint/unbound-method
213:13 error Unsafe assignment of an `any` value @typescript-eslint/no-unsafe-assignment
213:69 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
237:31 error Unsafe argument of type `any` assigned to a parameter of type `UpdateCorrespondenceDto` @typescript-eslint/no-unsafe-argument
237:44 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
237:49 error Unsafe argument of type `any` assigned to a parameter of type `User` @typescript-eslint/no-unsafe-argument
239:14 error A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.
Consider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value.
If a function does not access `this`, it can be annotated with `this: void` @typescript-eslint/unbound-method
243:13 error Unsafe assignment of an `any` value @typescript-eslint/no-unsafe-assignment
243:69 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
262:7 error Unsafe call of a type that could not be resolved @typescript-eslint/no-unsafe-call
264:10 error Unsafe member access .mockResolvedValue on a type that cannot be resolved @typescript-eslint/no-unsafe-member-access
264:71 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
270:31 error Unsafe argument of type `any` assigned to a parameter of type `UpdateCorrespondenceDto` @typescript-eslint/no-unsafe-argument
270:44 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
270:49 error Unsafe argument of type `any` assigned to a parameter of type `User` @typescript-eslint/no-unsafe-argument
272:14 error A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.
Consider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value.
If a function does not access `this`, it can be annotated with `this: void` @typescript-eslint/unbound-method
D:\nap-dms.lcbp3\backend\src\modules\correspondence\dto\create-routing-template.dto.ts
9:3 error 'IsEnum' is defined but never used @typescript-eslint/no-unused-vars
D:\nap-dms.lcbp3\backend\src\modules\correspondence\entities\correspondence-recipient.entity.ts
1:18 error 'Column' is defined but never used @typescript-eslint/no-unused-vars
D:\nap-dms.lcbp3\backend\src\modules\document-numbering\controllers\document-numbering.controller.ts
119:5 error Unexpected console statement no-console
D:\nap-dms.lcbp3\backend\src\modules\document-numbering\controllers\numbering-metrics.controller.ts
1:27 error 'UseGuards' is defined but never used @typescript-eslint/no-unused-vars
13:3 error Async method 'getMetrics' has no 'await' expression @typescript-eslint/require-await
D:\nap-dms.lcbp3\backend\src\modules\document-numbering\document-numbering.service.spec.ts
127:14 error A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.
Consider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value.
If a function does not access `this`, it can be annotated with `this: void` @typescript-eslint/unbound-method
128:14 error A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.
Consider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value.
If a function does not access `this`, it can be annotated with `this: void` @typescript-eslint/unbound-method
145:13 error Unsafe assignment of an `any` value @typescript-eslint/no-unsafe-assignment
146:18 error Unsafe member access .findOne on an `any` value @typescript-eslint/no-unsafe-member-access
151:18 error Unsafe member access .save on an `any` value @typescript-eslint/no-unsafe-member-access
159:24 error Unsafe member access .save on an `any` value @typescript-eslint/no-unsafe-member-access
163:13 error Unsafe assignment of an `any` value @typescript-eslint/no-unsafe-assignment
164:18 error Unsafe member access .findOne on an `any` value @typescript-eslint/no-unsafe-member-access
168:18 error Unsafe member access .save on an `any` value @typescript-eslint/no-unsafe-member-access
176:24 error Unsafe member access .save on an `any` value @typescript-eslint/no-unsafe-member-access
D:\nap-dms.lcbp3\backend\src\modules\document-numbering\entities\document-number-audit.entity.ts
28:42 error 'unknown' overrides all other types in this union type @typescript-eslint/no-redundant-type-constituents
D:\nap-dms.lcbp3\backend\src\modules\document-numbering\services\format.service.ts
54:5 error Unexpected console statement no-console
D:\nap-dms.lcbp3\backend\src\modules\document-numbering\services\manual-override.service.spec.ts
56:12 error A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.
Consider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value.
If a function does not access `this`, it can be annotated with `this: void` @typescript-eslint/unbound-method
57:12 error A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.
Consider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value.
If a function does not access `this`, it can be annotated with `this: void` @typescript-eslint/unbound-method
D:\nap-dms.lcbp3\backend\src\modules\document-numbering\services\metrics.service.ts
1:22 error 'Logger' is defined but never used @typescript-eslint/no-unused-vars
D:\nap-dms.lcbp3\backend\src\modules\document-numbering\services\reservation.service.ts
76:11 error 'reservation' is assigned a value but never used @typescript-eslint/no-unused-vars
D:\nap-dms.lcbp3\backend\src\modules\document-numbering\services\template.service.ts
1:30 error 'NotFoundException' is defined but never used @typescript-eslint/no-unused-vars
D:\nap-dms.lcbp3\backend\src\modules\json-schema\dto\create-json-schema.dto.ts
51:37 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
55:29 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
65:36 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
D:\nap-dms.lcbp3\backend\src\modules\json-schema\dto\search-json-schema.dto.ts
14:5 error Unsafe return of a value of type `any` @typescript-eslint/no-unsafe-return
D:\nap-dms.lcbp3\backend\src\modules\json-schema\json-schema.service.ts
92:18 error '_schema' is defined but never used @typescript-eslint/no-unused-vars
92:35 error '_data' is defined but never used @typescript-eslint/no-unused-vars
258:5 error 'options' is assigned a value but never used @typescript-eslint/no-unused-vars
265:11 error Unsafe assignment of an `any` value @typescript-eslint/no-unsafe-assignment
D:\nap-dms.lcbp3\backend\src\modules\json-schema\services\virtual-column.service.ts
101:11 error Unsafe assignment of an `any` value @typescript-eslint/no-unsafe-assignment
106:16 error Unsafe member access [0] on an `any` value @typescript-eslint/no-unsafe-member-access
D:\nap-dms.lcbp3\backend\src\modules\master\dto\create-tag.dto.ts
1:44 error 'IsInt' is defined but never used @typescript-eslint/no-unused-vars
D:\nap-dms.lcbp3\backend\src\modules\master\dto\save-number-format.dto.ts
1:39 error 'IsOptional' is defined but never used @typescript-eslint/no-unused-vars
D:\nap-dms.lcbp3\backend\src\modules\master\master.controller.ts
7:3 error 'Put' is defined but never used @typescript-eslint/no-unused-vars
D:\nap-dms.lcbp3\backend\src\modules\migration\migration.controller.spec.ts
53:12 error A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.
Consider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value.
If a function does not access `this`, it can be annotated with `this: void` @typescript-eslint/unbound-method
D:\nap-dms.lcbp3\backend\src\modules\migration\migration.controller.ts
166:3 error Async method 'getStagingFile' has no 'await' expression @typescript-eslint/require-await
D:\nap-dms.lcbp3\backend\src\modules\notification\dto\create-notification.dto.ts
7:3 error 'IsUrl' is defined but never used @typescript-eslint/no-unused-vars
D:\nap-dms.lcbp3\backend\src\modules\notification\dto\search-notification.dto.ts
20:5 error Unsafe return of a value of type `any` @typescript-eslint/no-unsafe-return
D:\nap-dms.lcbp3\backend\src\modules\notification\notification-cleanup.service.ts
4:22 error 'LessThan' is defined but never used @typescript-eslint/no-unused-vars
D:\nap-dms.lcbp3\backend\src\modules\notification\notification.service.ts
12:10 error 'UserPreference' is defined but never used @typescript-eslint/no-unused-vars
D:\nap-dms.lcbp3\backend\src\modules\organization\organization.service.ts
76:5 error Unexpected console statement no-console
D:\nap-dms.lcbp3\backend\src\modules\project\dto\search-project.dto.ts
14:5 error Unsafe return of a value of type `any` @typescript-eslint/no-unsafe-return
D:\nap-dms.lcbp3\backend\src\modules\project\project.controller.spec.ts
49:13 error 'result' is assigned a value but never used @typescript-eslint/no-unused-vars
62:13 error 'result' is assigned a value but never used @typescript-eslint/no-unused-vars
D:\nap-dms.lcbp3\backend\src\modules\project\project.service.spec.ts
64:7 error Unsafe call of an `any` typed value @typescript-eslint/no-unsafe-call
66:10 error Unsafe member access .getManyAndCount on an `any` value @typescript-eslint/no-unsafe-member-access
D:\nap-dms.lcbp3\backend\src\modules\project\project.service.ts
8:22 error 'Like' is defined but never used @typescript-eslint/no-unused-vars
D:\nap-dms.lcbp3\backend\src\modules\rfa\entities\rfa-workflow-template.entity.ts
26:35 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
D:\nap-dms.lcbp3\backend\src\modules\rfa\entities\rfa-workflow.entity.ts
55:33 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
D:\nap-dms.lcbp3\backend\src\modules\search\dto\search-query.dto.ts
1:39 error 'IsNotEmpty' is defined but never used @typescript-eslint/no-unused-vars
D:\nap-dms.lcbp3\backend\src\modules\transmittal\transmittal.controller.ts
49:20 error '_user' is defined but never used @typescript-eslint/no-unused-vars
D:\nap-dms.lcbp3\backend\src\modules\user\dto\assign-role.dto.ts
1:41 error 'ValidateIf' is defined but never used @typescript-eslint/no-unused-vars
D:\nap-dms.lcbp3\backend\src\modules\workflow-engine\dsl\parser.service.ts
24:13 error Unsafe assignment of an `any` value @typescript-eslint/no-unsafe-assignment
185:13 error Unsafe assignment of an `any` value @typescript-eslint/no-unsafe-assignment
D:\nap-dms.lcbp3\backend\src\modules\workflow-engine\dto\evaluate-workflow.dto.ts
24:28 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
D:\nap-dms.lcbp3\backend\src\modules\workflow-engine\entities\workflow-history.entity.ts
57:29 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
D:\nap-dms.lcbp3\backend\src\modules\workflow-engine\entities\workflow-instance.entity.ts
73:28 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
D:\nap-dms.lcbp3\backend\src\modules\workflow-engine\workflow-dsl.service.ts
258:20 error Implied eval. Do not use the Function constructor to create functions @typescript-eslint/no-implied-eval
259:16 error Unsafe call of a `Function` typed value @typescript-eslint/no-unsafe-call
D:\nap-dms.lcbp3\backend\src\modules\workflow-engine\workflow-engine.controller.ts
118:3 error Async method 'getAvailableActions' has no 'await' expression @typescript-eslint/require-await
118:42 error 'instanceId' is defined but never used @typescript-eslint/no-unused-vars
D:\nap-dms.lcbp3\backend\src\modules\workflow-engine\workflow-engine.service.spec.ts
121:14 error A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.
Consider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value.
If a function does not access `this`, it can be annotated with `this: void` @typescript-eslint/unbound-method
122:14 error A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.
Consider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value.
If a function does not access `this`, it can be annotated with `this: void` @typescript-eslint/unbound-method
145:14 error A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.
Consider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value.
If a function does not access `this`, it can be annotated with `this: void` @typescript-eslint/unbound-method
188:14 error A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.
Consider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value.
If a function does not access `this`, it can be annotated with `this: void` @typescript-eslint/unbound-method
D:\nap-dms.lcbp3\backend\src\modules\workflow-engine\workflow-event.service.ts
28:3 error Async method 'dispatchEvents' has no 'await' expression @typescript-eslint/require-await
31:29 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
40:5 error Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator @typescript-eslint/no-floating-promises
73:66 error Invalid type "unknown" of template literal expression @typescript-eslint/restrict-template-expressions
80:3 error Async method 'handleNotify' has no 'await' expression @typescript-eslint/require-await
82:5 error '_context' is defined but never used @typescript-eslint/no-unused-vars
91:3 error Async method 'handleWebhook' has no 'await' expression @typescript-eslint/require-await
93:5 error '_context' is defined but never used @typescript-eslint/no-unused-vars
D:\nap-dms.lcbp3\backend\src\scripts\migrate-storage-v2.ts
10:37 error Unsafe argument of type `any` assigned to a parameter of type `DataSourceOptions` @typescript-eslint/no-unsafe-argument
10:47 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
14:5 error Unexpected console statement no-console
21:5 error Unexpected console statement no-console
33:5 error Unexpected console statement no-console
36:7 error Unexpected console statement no-console
50:9 error Unexpected console statement no-console
57:20 error Unnecessary escape character: \/ no-useless-escape
57:78 error Unnecessary escape character: \/ no-useless-escape
57:89 error Unnecessary escape character: \/ no-useless-escape
68:9 error Unexpected console statement no-console
98:37 error Unexpected console statement no-console
100:9 error Unexpected console statement no-console
108:5 error Unexpected console statement no-console
109:5 error Unexpected console statement no-console
110:5 error Unexpected console statement no-console
111:5 error Unexpected console statement no-console
113:5 error Unexpected console statement no-console
119:1 error Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator @typescript-eslint/no-floating-promises
D:\nap-dms.lcbp3\backend\test\phase3-workflow.e2e-spec.ts
27:7 error 'adminToken' is assigned a value but never used @typescript-eslint/no-unused-vars
57:7 error Unexpected console statement no-console
70:36 error Unsafe argument of type `any` assigned to a parameter of type `App` @typescript-eslint/no-unsafe-argument
83:5 error Unsafe assignment of an `any` value @typescript-eslint/no-unsafe-assignment
83:38 error Unsafe member access .id on an `any` value @typescript-eslint/no-unsafe-member-access
84:5 error Unexpected console statement no-console
88:36 error Unsafe argument of type `any` assigned to a parameter of type `App` @typescript-eslint/no-unsafe-argument
98:5 error Unsafe assignment of an `any` value @typescript-eslint/no-unsafe-assignment
98:40 error Unsafe member access .instanceId on an `any` value @typescript-eslint/no-unsafe-member-access
99:5 error Unexpected console statement no-console
100:5 error Unexpected console statement no-console
100:49 error Unsafe member access .currentState on an `any` value @typescript-eslint/no-unsafe-member-access
106:7 error Unexpected console statement no-console
110:36 error Unsafe argument of type `any` assigned to a parameter of type `App` @typescript-eslint/no-unsafe-argument
122:5 error Unexpected console statement no-console
D:\nap-dms.lcbp3\backend\test\simple.e2e-spec.ts
1:8 error 'request' is defined but never used @typescript-eslint/no-unused-vars
3:10 error 'RoutingTemplate' is defined but never used @typescript-eslint/no-unused-vars
✖ 180 problems (180 errors, 0 warnings)
ELIFECYCLE Command failed with exit code 1.
+122
View File
@@ -0,0 +1,122 @@
> backend@1.8.0 lint D:\nap-dms.lcbp3\backend
> eslint "{src,apps,libs,test}/**/*.ts" --fix
D:\nap-dms.lcbp3\backend\src\common\auth\auth.service.spec.ts
164:14 error A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.
Consider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value.
If a function does not access `this`, it can be annotated with `this: void` @typescript-eslint/unbound-method
D:\nap-dms.lcbp3\backend\src\common\file-storage\file-storage.service.spec.ts
88:14 error A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.
Consider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value.
If a function does not access `this`, it can be annotated with `this: void` @typescript-eslint/unbound-method
89:14 error A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.
Consider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value.
If a function does not access `this`, it can be annotated with `this: void` @typescript-eslint/unbound-method
121:14 error A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.
Consider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value.
If a function does not access `this`, it can be annotated with `this: void` @typescript-eslint/unbound-method
139:14 error A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.
Consider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value.
If a function does not access `this`, it can be annotated with `this: void` @typescript-eslint/unbound-method
D:\nap-dms.lcbp3\backend\src\database\seeds\user.seed.ts
61:7 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment
115:13 error Unsafe assignment of an `any` value @typescript-eslint/no-unsafe-assignment
119:11 error Unsafe assignment of an `any` value @typescript-eslint/no-unsafe-assignment
D:\nap-dms.lcbp3\backend\src\modules\correspondence\correspondence.service.spec.ts
187:9 error A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.
Consider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value.
If a function does not access `this`, it can be annotated with `this: void` @typescript-eslint/unbound-method
223:9 error A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.
Consider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value.
If a function does not access `this`, it can be annotated with `this: void` @typescript-eslint/unbound-method
259:9 error A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.
Consider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value.
If a function does not access `this`, it can be annotated with `this: void` @typescript-eslint/unbound-method
304:9 error A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.
Consider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value.
If a function does not access `this`, it can be annotated with `this: void` @typescript-eslint/unbound-method
D:\nap-dms.lcbp3\backend\src\modules\document-numbering\document-numbering.service.spec.ts
128:14 error A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.
Consider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value.
If a function does not access `this`, it can be annotated with `this: void` @typescript-eslint/unbound-method
129:14 error A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.
Consider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value.
If a function does not access `this`, it can be annotated with `this: void` @typescript-eslint/unbound-method
162:14 error A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.
Consider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value.
If a function does not access `this`, it can be annotated with `this: void` @typescript-eslint/unbound-method
181:14 error A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.
Consider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value.
If a function does not access `this`, it can be annotated with `this: void` @typescript-eslint/unbound-method
D:\nap-dms.lcbp3\backend\src\modules\document-numbering\services\manual-override.service.spec.ts
56:12 error A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.
Consider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value.
If a function does not access `this`, it can be annotated with `this: void` @typescript-eslint/unbound-method
57:12 error A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.
Consider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value.
If a function does not access `this`, it can be annotated with `this: void` @typescript-eslint/unbound-method
D:\nap-dms.lcbp3\backend\src\modules\json-schema\dto\create-json-schema.dto.ts
51:37 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
55:29 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
65:36 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
D:\nap-dms.lcbp3\backend\src\modules\json-schema\dto\search-json-schema.dto.ts
14:5 error Unsafe return of a value of type `any` @typescript-eslint/no-unsafe-return
D:\nap-dms.lcbp3\backend\src\modules\json-schema\json-schema.service.ts
265:11 error Unsafe assignment of an `any` value @typescript-eslint/no-unsafe-assignment
D:\nap-dms.lcbp3\backend\src\modules\json-schema\services\virtual-column.service.ts
101:11 error Unsafe assignment of an `any` value @typescript-eslint/no-unsafe-assignment
106:16 error Unsafe member access [0] on an `any` value @typescript-eslint/no-unsafe-member-access
D:\nap-dms.lcbp3\backend\src\modules\migration\migration.controller.spec.ts
53:12 error A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.
Consider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value.
If a function does not access `this`, it can be annotated with `this: void` @typescript-eslint/unbound-method
D:\nap-dms.lcbp3\backend\src\modules\notification\dto\search-notification.dto.ts
20:5 error Unsafe return of a value of type `any` @typescript-eslint/no-unsafe-return
D:\nap-dms.lcbp3\backend\src\modules\project\dto\search-project.dto.ts
14:5 error Unsafe return of a value of type `any` @typescript-eslint/no-unsafe-return
D:\nap-dms.lcbp3\backend\src\modules\project\project.service.spec.ts
64:7 error Unsafe call of an `any` typed value @typescript-eslint/no-unsafe-call
66:10 error Unsafe member access .getManyAndCount on an `any` value @typescript-eslint/no-unsafe-member-access
D:\nap-dms.lcbp3\backend\src\modules\workflow-engine\dsl\parser.service.ts
24:13 error Unsafe assignment of an `any` value @typescript-eslint/no-unsafe-assignment
185:13 error Unsafe assignment of an `any` value @typescript-eslint/no-unsafe-assignment
D:\nap-dms.lcbp3\backend\src\modules\workflow-engine\entities\workflow-history.entity.ts
57:29 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
D:\nap-dms.lcbp3\backend\src\modules\workflow-engine\entities\workflow-instance.entity.ts
73:28 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
D:\nap-dms.lcbp3\backend\src\modules\workflow-engine\workflow-engine.service.spec.ts
121:14 error A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.
Consider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value.
If a function does not access `this`, it can be annotated with `this: void` @typescript-eslint/unbound-method
122:14 error A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.
Consider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value.
If a function does not access `this`, it can be annotated with `this: void` @typescript-eslint/unbound-method
145:14 error A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.
Consider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value.
If a function does not access `this`, it can be annotated with `this: void` @typescript-eslint/unbound-method
188:14 error A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.
Consider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value.
If a function does not access `this`, it can be annotated with `this: void` @typescript-eslint/unbound-method
✖ 38 problems (38 errors, 0 warnings)
ELIFECYCLE Command failed with exit code 1.
+28
View File
@@ -0,0 +1,28 @@
> backend@1.8.0 lint D:\nap-dms.lcbp3\backend
> eslint "{src,apps,libs,test}/**/*.ts" --fix
D:\nap-dms.lcbp3\backend\src\database\seeds\user.seed.ts
61:7 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment
115:13 error Unsafe assignment of an `any` value @typescript-eslint/no-unsafe-assignment
119:11 error Unsafe assignment of an `any` value @typescript-eslint/no-unsafe-assignment
D:\nap-dms.lcbp3\backend\src\modules\json-schema\json-schema.service.ts
265:11 error Unsafe assignment of an `any` value @typescript-eslint/no-unsafe-assignment
D:\nap-dms.lcbp3\backend\src\modules\json-schema\services\virtual-column.service.ts
101:11 error Unsafe assignment of an `any` value @typescript-eslint/no-unsafe-assignment
106:16 error Unsafe member access [0] on an `any` value @typescript-eslint/no-unsafe-member-access
D:\nap-dms.lcbp3\backend\src\modules\project\project.service.spec.ts
64:7 error Unsafe call of an `any` typed value @typescript-eslint/no-unsafe-call
66:10 error Unsafe member access .getManyAndCount on an `any` value @typescript-eslint/no-unsafe-member-access
D:\nap-dms.lcbp3\backend\src\modules\workflow-engine\dsl\parser.service.ts
24:13 error Unsafe assignment of an `any` value @typescript-eslint/no-unsafe-assignment
185:13 error Unsafe assignment of an `any` value @typescript-eslint/no-unsafe-assignment
✖ 10 problems (10 errors, 0 warnings)
ELIFECYCLE Command failed with exit code 1.
+4
View File
@@ -0,0 +1,4 @@
> backend@1.8.0 lint D:\nap-dms.lcbp3\backend
> eslint "{src,apps,libs,test}/**/*.ts" --fix
+32 -31
View File
@@ -4,49 +4,50 @@ import * as fs from 'fs';
// Read .env to get DB config // Read .env to get DB config
const envFile = fs.readFileSync('.env', 'utf8'); const envFile = fs.readFileSync('.env', 'utf8');
const getEnv = (key: string) => { const getEnv = (key: string) => {
const line = envFile.split('\n').find(l => l.startsWith(key + '=')); const line = envFile.split('\n').find((l) => l.startsWith(key + '='));
return line ? line.split('=')[1].trim() : ''; return line ? line.split('=')[1].trim() : '';
}; };
const dataSource = new DataSource({ const dataSource = new DataSource({
type: 'mariadb', type: 'mariadb',
host: getEnv('DB_HOST') || 'localhost', host: getEnv('DB_HOST') || 'localhost',
port: parseInt(getEnv('DB_PORT') || '3306'), port: parseInt(getEnv('DB_PORT') || '3306'),
username: getEnv('DB_USERNAME') || 'admin', username: getEnv('DB_USERNAME') || 'admin',
password: getEnv('DB_PASSWORD') || 'Center2025', password: getEnv('DB_PASSWORD') || 'Center2025',
database: getEnv('DB_DATABASE') || 'lcbp3_dev', database: getEnv('DB_DATABASE') || 'lcbp3_dev',
entities: [], entities: [],
synchronize: false, synchronize: false,
}); });
async function main() { async function main() {
await dataSource.initialize(); await dataSource.initialize();
console.log('Connected to DB'); console.log('Connected to DB');
try { try {
const assignments = await dataSource.query('SELECT * FROM user_assignments'); const assignments = await dataSource.query(
console.log('All Assignments:', assignments); 'SELECT * FROM user_assignments'
);
console.log('All Assignments:', assignments);
// Check if User 3 has any assignment // Check if User 3 has any assignment
const user3Assign = assignments.find((a: any) => a.user_id === 3); const user3Assign = assignments.find((a: any) => a.user_id === 3);
if (!user3Assign) { if (!user3Assign) {
console.log('User 3 has NO assignments.'); console.log('User 3 has NO assignments.');
// Try to insert assignment for User 3 (Editor) // Try to insert assignment for User 3 (Editor)
console.log('Inserting assignment for User 3 (Role 4, Org 41)...'); console.log('Inserting assignment for User 3 (Role 4, Org 41)...');
await dataSource.query(` await dataSource.query(`
INSERT INTO user_assignments (user_id, role_id, organization_id, assigned_by_user_id) INSERT INTO user_assignments (user_id, role_id, organization_id, assigned_by_user_id)
VALUES (3, 4, 41, 1) VALUES (3, 4, 41, 1)
`); `);
console.log('Inserted assignment for User 3.'); console.log('Inserted assignment for User 3.');
} else { } else {
console.log('User 3 Assignment:', user3Assign); console.log('User 3 Assignment:', user3Assign);
}
} catch (err) {
console.error(err);
} finally {
await dataSource.destroy();
} }
} catch (err) {
console.error(err);
} finally {
await dataSource.destroy();
}
} }
main(); main();
+8 -8
View File
@@ -9,10 +9,10 @@ const API_URL = 'http://localhost:3000/api';
function signJwt(payload: any) { function signJwt(payload: any) {
const header = { alg: 'HS256', typ: 'JWT' }; const header = { alg: 'HS256', typ: 'JWT' };
const encodedHeader = Buffer.from(JSON.stringify(header)).toString( const encodedHeader = Buffer.from(JSON.stringify(header)).toString(
'base64url', 'base64url'
); );
const encodedPayload = Buffer.from(JSON.stringify(payload)).toString( const encodedPayload = Buffer.from(JSON.stringify(payload)).toString(
'base64url', 'base64url'
); );
const signature = crypto const signature = crypto
@@ -44,7 +44,7 @@ async function main() {
console.error( console.error(
'Failed to get permissions:', 'Failed to get permissions:',
permRes.status, permRes.status,
await permRes.text(), await permRes.text()
); );
} }
@@ -64,7 +64,7 @@ async function main() {
if (!createRes.ok) { if (!createRes.ok) {
throw new Error( throw new Error(
`Create failed: ${createRes.status} ${await createRes.text()}`, `Create failed: ${createRes.status} ${await createRes.text()}`
); );
} }
@@ -81,7 +81,7 @@ async function main() {
body: JSON.stringify({ body: JSON.stringify({
templateId: 1, // Assuming Template ID 1 exists templateId: 1, // Assuming Template ID 1 exists
}), }),
}, }
); );
if (!submitRes.ok) { if (!submitRes.ok) {
@@ -89,7 +89,7 @@ async function main() {
console.error(`Submit failed: ${submitRes.status} ${text}`); console.error(`Submit failed: ${submitRes.status} ${text}`);
if (text.includes('template')) { if (text.includes('template')) {
console.warn( console.warn(
'⚠️ Template ID 1 not found. Please ensure a Routing Template exists.', '⚠️ Template ID 1 not found. Please ensure a Routing Template exists.'
); );
} }
return; return;
@@ -108,12 +108,12 @@ async function main() {
action: 'APPROVE', action: 'APPROVE',
comment: 'Approved via script', comment: 'Approved via script',
}), }),
}, }
); );
if (!approveRes.ok) { if (!approveRes.ok) {
throw new Error( throw new Error(
`Approve failed: ${approveRes.status} ${await approveRes.text()}`, `Approve failed: ${approveRes.status} ${await approveRes.text()}`
); );
} }
@@ -78,7 +78,7 @@ describe('AuthController', () => {
(mockAuthService.register as jest.Mock).mockResolvedValue(mockUser); (mockAuthService.register as jest.Mock).mockResolvedValue(mockUser);
const result = await controller.register(registerDto); const _result = await controller.register(registerDto);
expect(mockAuthService.register).toHaveBeenCalledWith(registerDto); expect(mockAuthService.register).toHaveBeenCalledWith(registerDto);
}); });
+5 -2
View File
@@ -27,7 +27,10 @@ import {
ApiResponse, ApiResponse,
ApiBody, ApiBody,
} from '@nestjs/swagger'; } from '@nestjs/swagger';
import type { RequestWithUser, RequestWithRefreshUser } from '../interfaces/request-with-user.interface'; import type {
RequestWithUser,
RequestWithRefreshUser,
} from '../interfaces/request-with-user.interface';
@ApiTags('Authentication') @ApiTags('Authentication')
@Controller('auth') @Controller('auth')
@@ -143,6 +146,6 @@ export class AuthController {
@ApiOperation({ summary: 'Revoke session' }) @ApiOperation({ summary: 'Revoke session' })
@ApiResponse({ status: 200, description: 'Session revoked' }) @ApiResponse({ status: 200, description: 'Session revoked' })
async revokeSession(@Param('id') id: string) { async revokeSession(@Param('id') id: string) {
return this.authService.revokeSession(parseInt(id)); return this.authService.revokeSession(Number(id));
} }
} }
+49 -11
View File
@@ -17,14 +17,13 @@ jest.mock('bcrypt', () => ({
genSalt: jest.fn().mockResolvedValue('salt'), genSalt: jest.fn().mockResolvedValue('salt'),
})); }));
// eslint-disable-next-line @typescript-eslint/no-require-imports import * as bcrypt from 'bcrypt';
const bcrypt = require('bcrypt');
describe('AuthService', () => { describe('AuthService', () => {
let service: AuthService; let service: AuthService;
let userService: UserService; let userService: UserService;
let jwtService: JwtService; let _jwtService: JwtService;
let tokenRepo: Repository<RefreshToken>; let _tokenRepo: Repository<RefreshToken>;
const mockUser = { const mockUser = {
user_id: 1, user_id: 1,
@@ -53,7 +52,7 @@ describe('AuthService', () => {
beforeEach(async () => { beforeEach(async () => {
// Reset bcrypt mocks // Reset bcrypt mocks
bcrypt.compare.mockResolvedValue(true); (bcrypt.compare as jest.Mock).mockResolvedValue(true);
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
providers: [ providers: [
@@ -101,8 +100,8 @@ describe('AuthService', () => {
service = module.get<AuthService>(AuthService); service = module.get<AuthService>(AuthService);
userService = module.get<UserService>(UserService); userService = module.get<UserService>(UserService);
jwtService = module.get<JwtService>(JwtService); _jwtService = module.get<JwtService>(JwtService);
tokenRepo = module.get(getRepositoryToken(RefreshToken)); _tokenRepo = module.get(getRepositoryToken(RefreshToken));
}); });
afterEach(() => { afterEach(() => {
@@ -118,7 +117,7 @@ describe('AuthService', () => {
const result = await service.validateUser('testuser', 'password'); const result = await service.validateUser('testuser', 'password');
expect(result).toBeDefined(); expect(result).toBeDefined();
expect(result).not.toHaveProperty('password'); expect(result).not.toHaveProperty('password');
expect(result.username).toBe('testuser'); expect(result!.username).toBe('testuser');
}); });
it('should return null if user not found', async () => { it('should return null if user not found', async () => {
@@ -128,7 +127,7 @@ describe('AuthService', () => {
}); });
it('should return null if password mismatch', async () => { it('should return null if password mismatch', async () => {
bcrypt.compare.mockResolvedValueOnce(false); (bcrypt.compare as jest.Mock).mockResolvedValueOnce(false);
const result = await service.validateUser('testuser', 'wrongpassword'); const result = await service.validateUser('testuser', 'wrongpassword');
expect(result).toBeNull(); expect(result).toBeNull();
}); });
@@ -139,7 +138,7 @@ describe('AuthService', () => {
mockTokenRepo.create.mockReturnValue({ id: 1 }); mockTokenRepo.create.mockReturnValue({ id: 1 });
mockTokenRepo.save.mockResolvedValue({ id: 1 }); mockTokenRepo.save.mockResolvedValue({ id: 1 });
const result = await service.login(mockUser); const result = await service.login(mockUser as User);
expect(result).toHaveProperty('access_token'); expect(result).toHaveProperty('access_token');
expect(result).toHaveProperty('refresh_token'); expect(result).toHaveProperty('refresh_token');
@@ -161,8 +160,9 @@ describe('AuthService', () => {
}; };
const result = await service.register(dto); const result = await service.register(dto);
const createMock = userService.create as jest.Mock;
expect(result).toBeDefined(); expect(result).toBeDefined();
expect(userService.create).toHaveBeenCalled(); expect(createMock).toHaveBeenCalled();
}); });
}); });
@@ -198,5 +198,43 @@ describe('AuthService', () => {
UnauthorizedException UnauthorizedException
); );
}); });
it('should allow refresh within 30s grace period if already revoked', async () => {
const updatedAt = new Date(Date.now() - 5000); // 5 seconds ago
const mockStoredToken = {
tokenHash: 'somehash',
isRevoked: true,
updatedAt: updatedAt,
replacedByToken: 'new_token_hash',
expiresAt: new Date(Date.now() + 10000),
};
mockTokenRepo.findOne.mockResolvedValue(mockStoredToken);
(userService.findOne as jest.Mock).mockResolvedValue(mockUser);
mockTokenRepo.create.mockReturnValue({ token_id: 2 });
mockTokenRepo.save.mockResolvedValue({ token_id: 2 });
const result = await service.refreshToken(1, 'valid_refresh_token');
expect(result.access_token).toBeDefined();
expect(result.refresh_token).toBeDefined();
// Should not call revokeAllUserTokens
expect(mockTokenRepo.update).not.toHaveBeenCalled();
});
it('should throw UnauthorizedException if token revoked more than 30s ago', async () => {
const updatedAt = new Date(Date.now() - 35000); // 35 seconds ago
const mockStoredToken = {
tokenHash: 'somehash',
isRevoked: true,
updatedAt: updatedAt,
replacedByToken: 'new_token_hash',
expiresAt: new Date(Date.now() + 10000),
};
mockTokenRepo.findOne.mockResolvedValue(mockStoredToken);
await expect(service.refreshToken(1, 'revoked_token')).rejects.toThrow(
UnauthorizedException
);
});
}); });
}); });
+50 -12
View File
@@ -9,6 +9,7 @@ import {
UnauthorizedException, UnauthorizedException,
Inject, Inject,
BadRequestException, BadRequestException,
Logger,
} from '@nestjs/common'; } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt'; import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
@@ -27,6 +28,8 @@ import { RefreshToken } from './entities/refresh-token.entity'; // [P2-2]
@Injectable() @Injectable()
export class AuthService { export class AuthService {
private readonly logger = new Logger(AuthService.name);
constructor( constructor(
private userService: UserService, private userService: UserService,
private jwtService: JwtService, private jwtService: JwtService,
@@ -40,8 +43,8 @@ export class AuthService {
) {} ) {}
// 1. ตรวจสอบ Username/Password // 1. ตรวจสอบ Username/Password
async validateUser(username: string, pass: string): Promise<any> { async validateUser(username: string, pass: string): Promise<User | null> {
console.log(`🔍 Checking login for: ${username}`); this.logger.log(`🔍 Checking login for: ${username}`);
const user = await this.usersRepository const user = await this.usersRepository
.createQueryBuilder('user') .createQueryBuilder('user')
.addSelect('user.password') .addSelect('user.password')
@@ -51,7 +54,7 @@ export class AuthService {
.getOne(); .getOne();
if (!user) { if (!user) {
console.log('❌ User not found in database'); this.logger.warn('❌ User not found in database');
return null; return null;
} }
@@ -75,7 +78,6 @@ export class AuthService {
derivedRole = 'DC'; derivedRole = 'DC';
} }
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const { password, ...result } = user; const { password, ...result } = user;
return { ...result, role: derivedRole }; return { ...result, role: derivedRole };
@@ -121,7 +123,10 @@ export class AuthService {
} }
// [P2-2] Store Refresh Token Logic // [P2-2] Store Refresh Token Logic
private async storeRefreshToken(userId: number, token: string) { private async storeRefreshToken(
userId: number,
token: string
): Promise<void> {
// Hash token before storing for security // Hash token before storing for security
const hash = crypto.createHash('sha256').update(token).digest('hex'); const hash = crypto.createHash('sha256').update(token).digest('hex');
const expiresInDays = 7; // Should match JWT_REFRESH_EXPIRATION const expiresInDays = 7; // Should match JWT_REFRESH_EXPIRATION
@@ -157,7 +162,10 @@ export class AuthService {
} }
// 4. Refresh Token: ตรวจสอบและออก Token ใหม่ (Rotation) // 4. Refresh Token: ตรวจสอบและออก Token ใหม่ (Rotation)
async refreshToken(userId: number, refreshToken: string) { async refreshToken(
userId: number,
refreshToken: string
): Promise<{ access_token: string; refresh_token: string }> {
// Hash incoming token to match with DB // Hash incoming token to match with DB
const hash = crypto.createHash('sha256').update(refreshToken).digest('hex'); const hash = crypto.createHash('sha256').update(refreshToken).digest('hex');
@@ -171,9 +179,37 @@ export class AuthService {
} }
if (storedToken.isRevoked) { if (storedToken.isRevoked) {
// Possible token theft! Invalidate all user tokens family // [P2-2.1] Grace period for Token Rotation (30 seconds)
await this.revokeAllUserTokens(userId); // ป้องกัน Race Condition เมื่อ Frontend ส่ง Refresh Request ซ้อนกันในชั่วพริบตา
throw new UnauthorizedException('Refresh token revoked - Security alert'); const now = new Date();
const revokedAt = new Date(storedToken.updatedAt);
const diffMs = now.getTime() - revokedAt.getTime();
this.logger.debug(`[DEBUG-TOKEN] user=${userId}`);
this.logger.debug(`[DEBUG-TOKEN] now=${now.toISOString()}`);
this.logger.debug(
`[DEBUG-TOKEN] updatedAt=${storedToken.updatedAt ? new Date(storedToken.updatedAt).toISOString() : 'NULL'}`
);
this.logger.debug(`[DEBUG-TOKEN] diffMs=${diffMs}`);
this.logger.debug(
`[DEBUG-TOKEN] replacedBy=${storedToken.replacedByToken ? 'YES(HASHED)' : 'NULL'}`
);
if (diffMs <= 30000 && storedToken.replacedByToken) {
this.logger.warn(
`Refresh token reuse detected within grace period (${diffMs}ms) for user ${userId}. Allowing another rotation.`
);
// ไม่ต้อง revokeAllUserTokens และอนุญาตให้ทำงานต่อด้านล่างเพื่อออก Token ชุดใหม่
} else {
// Possible token theft! Invalidate all user tokens family
await this.revokeAllUserTokens(userId);
this.logger.error(
`Refresh token revoked - Security alert for user ${userId}. All tokens invalidated.`
);
throw new UnauthorizedException(
'Refresh token revoked - Security alert'
);
}
} }
if (storedToken.expiresAt < new Date()) { if (storedToken.expiresAt < new Date()) {
@@ -205,8 +241,10 @@ export class AuthService {
.update(newRefreshToken) .update(newRefreshToken)
.digest('hex'); .digest('hex');
// [P2-2] Mark old token as revoked and rotated
storedToken.isRevoked = true; storedToken.isRevoked = true;
storedToken.replacedByToken = newHash; storedToken.replacedByToken = newHash;
storedToken.updatedAt = new Date(); // Fallback: Manually update instead of relying solely on @UpdateDateColumn
await this.refreshTokenRepository.save(storedToken); await this.refreshTokenRepository.save(storedToken);
// Save NEW token // Save NEW token
@@ -219,7 +257,7 @@ export class AuthService {
} }
// [P2-2] Helper: Revoke all tokens for a user (Security Measure) // [P2-2] Helper: Revoke all tokens for a user (Security Measure)
private async revokeAllUserTokens(userId: number) { private async revokeAllUserTokens(userId: number): Promise<void> {
await this.refreshTokenRepository.update( await this.refreshTokenRepository.update(
{ userId, isRevoked: false }, { userId, isRevoked: false },
{ isRevoked: true } { isRevoked: true }
@@ -230,7 +268,7 @@ export class AuthService {
async logout(userId: number, accessToken: string, refreshToken?: string) { async logout(userId: number, accessToken: string, refreshToken?: string) {
// Blacklist Access Token // Blacklist Access Token
try { try {
const decoded = this.jwtService.decode(accessToken); const decoded = this.jwtService.decode<{ exp: number }>(accessToken);
if (decoded && decoded.exp) { if (decoded && decoded.exp) {
const ttl = decoded.exp - Math.floor(Date.now() / 1000); const ttl = decoded.exp - Math.floor(Date.now() / 1000);
if (ttl > 0) { if (ttl > 0) {
@@ -241,7 +279,7 @@ export class AuthService {
); );
} }
} }
} catch (error) { } catch {
// Ignore decoding error // Ignore decoding error
} }
+3 -2
View File
@@ -17,7 +17,6 @@ import { RequirePermission } from '../common/decorators/require-permission.decor
@Controller('correspondences') @Controller('correspondences')
@UseGuards(JwtAuthGuard) // Step 1: Authenticate user @UseGuards(JwtAuthGuard) // Step 1: Authenticate user
export class CorrespondenceController { export class CorrespondenceController {
// ตัวอย่าง 1: Single Permission // ตัวอย่าง 1: Single Permission
@Post() @Post()
@UseGuards(PermissionsGuard) // Step 2: Check permissions @UseGuards(PermissionsGuard) // Step 2: Check permissions
@@ -63,7 +62,6 @@ Permissions guard จะ extract scope จาก request params/body/query:
@Controller('projects/:projectId/correspondences') @Controller('projects/:projectId/correspondences')
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
export class ProjectCorrespondenceController { export class ProjectCorrespondenceController {
@Post() @Post()
@UseGuards(PermissionsGuard) @UseGuards(PermissionsGuard)
@RequirePermission('correspondence.create') @RequirePermission('correspondence.create')
@@ -99,6 +97,7 @@ export class ProjectCorrespondenceController {
Permission ใน database ต้องเป็นรูปแบบ: `{subject}.{action}` Permission ใน database ต้องเป็นรูปแบบ: `{subject}.{action}`
ตัวอย่าง: ตัวอย่าง:
- `correspondence.create` - `correspondence.create`
- `correspondence.view` - `correspondence.view`
- `correspondence.edit` - `correspondence.edit`
@@ -110,11 +109,13 @@ Permission ใน database ต้องเป็นรูปแบบ: `{subject
## Testing ## Testing
Run unit tests: Run unit tests:
```bash ```bash
npm run test -- ability.factory.spec npm run test -- ability.factory.spec
``` ```
Expected output: Expected output:
``` ```
✓ should grant all permissions for global admin ✓ should grant all permissions for global admin
✓ should grant permissions for matching organization ✓ should grant permissions for matching organization
@@ -3,6 +3,7 @@ import {
Column, Column,
PrimaryGeneratedColumn, PrimaryGeneratedColumn,
CreateDateColumn, CreateDateColumn,
UpdateDateColumn,
ManyToOne, ManyToOne,
JoinColumn, JoinColumn,
} from 'typeorm'; } from 'typeorm';
@@ -28,6 +29,9 @@ export class RefreshToken {
@CreateDateColumn({ name: 'created_at' }) @CreateDateColumn({ name: 'created_at' })
createdAt!: Date; createdAt!: Date;
@UpdateDateColumn({ name: 'updated_at' })
updatedAt!: Date;
@Column({ name: 'replaced_by_token', nullable: true, length: 255 }) @Column({ name: 'replaced_by_token', nullable: true, length: 255 })
replacedByToken?: string; // For rotation support replacedByToken?: string; // For rotation support

Some files were not shown because too many files have changed in this diff Show More