260322:1648 Correct Coresspondence / Doing RFA / Correct CI
This commit is contained in:
+25
-25
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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_
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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"
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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} />;
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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?
|
||||||
|
|||||||
@@ -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 |
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
============================================================================
|
============================================================================
|
||||||
-->
|
-->
|
||||||
|
|||||||
@@ -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 multiple‑choice selection (2–5 distinct, mutually exclusive options), OR
|
- A short multiple‑choice selection (2–5 distinct, mutually exclusive options), OR
|
||||||
- A one-word / short‑phrase answer (explicitly constrain: "Answer in <=5 words").
|
- A one-word / short‑phrase 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 multiple‑choice questions:
|
- For multiple‑choice 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 short‑answer style (no meaningful discrete options):
|
||||||
- For short‑answer 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).
|
||||||
|
|||||||
@@ -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]
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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]
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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] |
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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%
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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]
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -12,4 +12,4 @@
|
|||||||
"ui": {
|
"ui": {
|
||||||
"showStatusInTitle": true
|
"showStatusInTitle": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 -->
|
||||||
|
|||||||
@@ -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.
|
||||||
============================================================================
|
============================================================================
|
||||||
-->
|
-->
|
||||||
|
|||||||
@@ -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] |
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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')
|
||||||
|
);
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Top 20 files with "any" issues:
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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',
|
||||||
|
},
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
> backend@1.8.0 lint D:\nap-dms.lcbp3\backend
|
||||||
|
> eslint "{src,apps,libs,test}/**/*.ts" --fix
|
||||||
|
|
||||||
+32
-31
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user