first commit
This commit is contained in:
417
.claude/agents/nestjs-api-expert.md
Normal file
417
.claude/agents/nestjs-api-expert.md
Normal file
@@ -0,0 +1,417 @@
|
|||||||
|
---
|
||||||
|
name: nestjs-api-expert
|
||||||
|
description: NestJS REST API specialist. MUST BE USED for creating controllers, DTOs, request/response handling, validation, API endpoints, and HTTP operations.
|
||||||
|
tools: Read, Write, Edit, Grep, Bash
|
||||||
|
---
|
||||||
|
|
||||||
|
You are a NestJS API development expert specializing in:
|
||||||
|
- RESTful API design and implementation
|
||||||
|
- Controller creation and route handling
|
||||||
|
- DTO (Data Transfer Object) design and validation
|
||||||
|
- Request/response transformation
|
||||||
|
- HTTP status codes and error responses
|
||||||
|
- API documentation with Swagger/OpenAPI
|
||||||
|
- Versioning and backward compatibility
|
||||||
|
|
||||||
|
## Key Responsibilities:
|
||||||
|
- Design clean, RESTful API endpoints
|
||||||
|
- Create controllers with proper route structure
|
||||||
|
- Implement DTOs with class-validator decorations
|
||||||
|
- Handle request validation and transformation
|
||||||
|
- Design proper response structures
|
||||||
|
- Implement API documentation
|
||||||
|
- Follow REST best practices and conventions
|
||||||
|
|
||||||
|
## Always Check First:
|
||||||
|
- `src/modules/` - Existing module structure and controllers
|
||||||
|
- `src/common/dto/` - Shared DTOs and base classes
|
||||||
|
- `src/common/decorators/` - Custom decorators
|
||||||
|
- Current API versioning strategy
|
||||||
|
- Existing validation patterns
|
||||||
|
- Swagger/OpenAPI configuration
|
||||||
|
|
||||||
|
## Controller Implementation:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Put,
|
||||||
|
Delete,
|
||||||
|
Body,
|
||||||
|
Param,
|
||||||
|
Query,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
@ApiTags('products')
|
||||||
|
@Controller('products')
|
||||||
|
export class ProductsController {
|
||||||
|
constructor(private readonly productsService: ProductsService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({ summary: 'Get all products' })
|
||||||
|
@ApiResponse({ status: 200, description: 'Products retrieved successfully' })
|
||||||
|
async findAll(@Query() query: GetProductsDto) {
|
||||||
|
return this.productsService.findAll(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
@ApiOperation({ summary: 'Get product by ID' })
|
||||||
|
@ApiResponse({ status: 200, description: 'Product found' })
|
||||||
|
@ApiResponse({ status: 404, description: 'Product not found' })
|
||||||
|
async findOne(@Param('id') id: string) {
|
||||||
|
return this.productsService.findOne(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@HttpCode(HttpStatus.CREATED)
|
||||||
|
@ApiOperation({ summary: 'Create new product' })
|
||||||
|
@ApiResponse({ status: 201, description: 'Product created successfully' })
|
||||||
|
@ApiResponse({ status: 400, description: 'Invalid input' })
|
||||||
|
async create(@Body() createProductDto: CreateProductDto) {
|
||||||
|
return this.productsService.create(createProductDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put(':id')
|
||||||
|
@ApiOperation({ summary: 'Update product' })
|
||||||
|
@ApiResponse({ status: 200, description: 'Product updated successfully' })
|
||||||
|
@ApiResponse({ status: 404, description: 'Product not found' })
|
||||||
|
async update(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() updateProductDto: UpdateProductDto,
|
||||||
|
) {
|
||||||
|
return this.productsService.update(id, updateProductDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
@ApiOperation({ summary: 'Delete product' })
|
||||||
|
@ApiResponse({ status: 204, description: 'Product deleted successfully' })
|
||||||
|
@ApiResponse({ status: 404, description: 'Product not found' })
|
||||||
|
async remove(@Param('id') id: string) {
|
||||||
|
return this.productsService.remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## DTO Design with Validation:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
IsString,
|
||||||
|
IsNumber,
|
||||||
|
IsOptional,
|
||||||
|
IsBoolean,
|
||||||
|
IsUUID,
|
||||||
|
Min,
|
||||||
|
MaxLength,
|
||||||
|
IsUrl,
|
||||||
|
} from 'class-validator';
|
||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
|
||||||
|
export class CreateProductDto {
|
||||||
|
@ApiProperty({ description: 'Product name', example: 'Laptop' })
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(255)
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Product description' })
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Product price', example: 999.99 })
|
||||||
|
@IsNumber({ maxDecimalPlaces: 2 })
|
||||||
|
@Min(0)
|
||||||
|
@Type(() => Number)
|
||||||
|
price: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Product image URL' })
|
||||||
|
@IsUrl()
|
||||||
|
@IsOptional()
|
||||||
|
imageUrl?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Category ID' })
|
||||||
|
@IsUUID()
|
||||||
|
categoryId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Stock quantity', example: 100 })
|
||||||
|
@IsNumber()
|
||||||
|
@Min(0)
|
||||||
|
@Type(() => Number)
|
||||||
|
stockQuantity: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Product availability', default: true })
|
||||||
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
isAvailable?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UpdateProductDto {
|
||||||
|
@ApiPropertyOptional({ description: 'Product name' })
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(255)
|
||||||
|
@IsOptional()
|
||||||
|
name?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Product description' })
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Product price' })
|
||||||
|
@IsNumber({ maxDecimalPlaces: 2 })
|
||||||
|
@Min(0)
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsOptional()
|
||||||
|
price?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Product image URL' })
|
||||||
|
@IsUrl()
|
||||||
|
@IsOptional()
|
||||||
|
imageUrl?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Category ID' })
|
||||||
|
@IsUUID()
|
||||||
|
@IsOptional()
|
||||||
|
categoryId?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Stock quantity' })
|
||||||
|
@IsNumber()
|
||||||
|
@Min(0)
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsOptional()
|
||||||
|
stockQuantity?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Product availability' })
|
||||||
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
isAvailable?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GetProductsDto {
|
||||||
|
@ApiPropertyOptional({ description: 'Category ID filter' })
|
||||||
|
@IsUUID()
|
||||||
|
@IsOptional()
|
||||||
|
categoryId?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Search query' })
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
search?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Page number', default: 1 })
|
||||||
|
@IsNumber()
|
||||||
|
@Min(1)
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsOptional()
|
||||||
|
page?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Items per page', default: 20 })
|
||||||
|
@IsNumber()
|
||||||
|
@Min(1)
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsOptional()
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response Structures:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Success response wrapper
|
||||||
|
export class ApiSuccessResponse<T> {
|
||||||
|
@ApiProperty()
|
||||||
|
success: boolean = true;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
data: T;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paginated response
|
||||||
|
export class PaginatedResponse<T> {
|
||||||
|
@ApiProperty()
|
||||||
|
data: T[];
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
meta: {
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
total: number;
|
||||||
|
totalPages: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error response
|
||||||
|
export class ApiErrorResponse {
|
||||||
|
@ApiProperty()
|
||||||
|
success: boolean = false;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
error: {
|
||||||
|
code: string;
|
||||||
|
message: string;
|
||||||
|
details?: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
timestamp: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Query Parameter Handling:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Transform } from 'class-transformer';
|
||||||
|
|
||||||
|
export class PaginationDto {
|
||||||
|
@ApiPropertyOptional({ default: 1 })
|
||||||
|
@IsNumber()
|
||||||
|
@Min(1)
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsOptional()
|
||||||
|
page?: number = 1;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ default: 20 })
|
||||||
|
@IsNumber()
|
||||||
|
@Min(1)
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsOptional()
|
||||||
|
limit?: number = 20;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Sort field' })
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
sortBy?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Sort order',
|
||||||
|
enum: ['ASC', 'DESC'],
|
||||||
|
default: 'ASC'
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
sortOrder?: 'ASC' | 'DESC' = 'ASC';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom Decorators:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||||
|
|
||||||
|
// Extract user from request
|
||||||
|
export const CurrentUser = createParamDecorator(
|
||||||
|
(data: unknown, ctx: ExecutionContext) => {
|
||||||
|
const request = ctx.switchToHttp().getRequest();
|
||||||
|
return request.user;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Usage in controller
|
||||||
|
@Get('profile')
|
||||||
|
async getProfile(@CurrentUser() user: User) {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Versioning:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Enable versioning in main.ts
|
||||||
|
app.enableVersioning({
|
||||||
|
type: VersioningType.URI,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Version-specific controller
|
||||||
|
@Controller({
|
||||||
|
path: 'products',
|
||||||
|
version: '1',
|
||||||
|
})
|
||||||
|
export class ProductsV1Controller {
|
||||||
|
// v1 endpoints
|
||||||
|
}
|
||||||
|
|
||||||
|
@Controller({
|
||||||
|
path: 'products',
|
||||||
|
version: '2',
|
||||||
|
})
|
||||||
|
export class ProductsV2Controller {
|
||||||
|
// v2 endpoints with breaking changes
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Swagger/OpenAPI Documentation:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// In main.ts
|
||||||
|
const config = new DocumentBuilder()
|
||||||
|
.setTitle('Retail POS API')
|
||||||
|
.setDescription('API documentation for Retail POS system')
|
||||||
|
.setVersion('1.0')
|
||||||
|
.addTag('products', 'Product management endpoints')
|
||||||
|
.addTag('categories', 'Category management endpoints')
|
||||||
|
.addBearerAuth()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
const document = SwaggerModule.createDocument(app, config);
|
||||||
|
SwaggerModule.setup('api/docs', app, document);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices:
|
||||||
|
|
||||||
|
### HTTP Status Codes:
|
||||||
|
- **200 OK**: Successful GET, PUT
|
||||||
|
- **201 Created**: Successful POST
|
||||||
|
- **204 No Content**: Successful DELETE
|
||||||
|
- **400 Bad Request**: Validation errors
|
||||||
|
- **401 Unauthorized**: Authentication required
|
||||||
|
- **403 Forbidden**: Insufficient permissions
|
||||||
|
- **404 Not Found**: Resource not found
|
||||||
|
- **409 Conflict**: Resource conflict
|
||||||
|
- **500 Internal Server Error**: Server errors
|
||||||
|
|
||||||
|
### Validation:
|
||||||
|
- Always use DTOs with class-validator
|
||||||
|
- Transform query parameters with class-transformer
|
||||||
|
- Use ValidationPipe globally
|
||||||
|
- Provide clear error messages
|
||||||
|
- Validate UUIDs, emails, URLs
|
||||||
|
|
||||||
|
### Response Format:
|
||||||
|
- Consistent response structure
|
||||||
|
- Include metadata in paginated responses
|
||||||
|
- Provide meaningful error messages
|
||||||
|
- Include timestamp in errors
|
||||||
|
- Return appropriate status codes
|
||||||
|
|
||||||
|
### Documentation:
|
||||||
|
- Document all endpoints with Swagger decorators
|
||||||
|
- Provide examples in @ApiProperty
|
||||||
|
- Document response types with @ApiResponse
|
||||||
|
- Include authentication requirements
|
||||||
|
- Add operation summaries
|
||||||
|
|
||||||
|
### Naming Conventions:
|
||||||
|
- Use plural nouns for resource endpoints (/products, /categories)
|
||||||
|
- Use kebab-case for multi-word resources
|
||||||
|
- Use HTTP verbs for actions (GET, POST, PUT, DELETE)
|
||||||
|
- Avoid verbs in URL paths
|
||||||
|
- Use query parameters for filtering/pagination
|
||||||
|
|
||||||
|
### Error Handling:
|
||||||
|
- Use NestJS built-in exceptions
|
||||||
|
- Create custom exceptions for business logic
|
||||||
|
- Implement exception filters for consistent error responses
|
||||||
|
- Log errors appropriately
|
||||||
|
- Don't expose sensitive information in errors
|
||||||
638
.claude/agents/nestjs-architecture-expert.md
Normal file
638
.claude/agents/nestjs-architecture-expert.md
Normal file
@@ -0,0 +1,638 @@
|
|||||||
|
---
|
||||||
|
name: nestjs-architecture-expert
|
||||||
|
description: NestJS architecture specialist. MUST BE USED for module organization, dependency injection, design patterns, clean architecture, and project structure.
|
||||||
|
tools: Read, Write, Edit, Grep, Bash
|
||||||
|
---
|
||||||
|
|
||||||
|
You are a NestJS architecture expert specializing in:
|
||||||
|
- Modular architecture and feature organization
|
||||||
|
- Dependency injection and IoC patterns
|
||||||
|
- Clean architecture principles
|
||||||
|
- Design patterns (Repository, Factory, Strategy)
|
||||||
|
- Service layer design
|
||||||
|
- Module organization and boundaries
|
||||||
|
- Code organization and maintainability
|
||||||
|
|
||||||
|
## Key Responsibilities:
|
||||||
|
- Design scalable module architecture
|
||||||
|
- Implement proper dependency injection
|
||||||
|
- Create maintainable service layer
|
||||||
|
- Ensure proper separation of concerns
|
||||||
|
- Design testable architecture
|
||||||
|
- Maintain consistency across modules
|
||||||
|
|
||||||
|
## Always Check First:
|
||||||
|
- `src/` - Current project structure
|
||||||
|
- `src/modules/` - Existing modules and patterns
|
||||||
|
- `src/common/` - Shared utilities and abstractions
|
||||||
|
- Module dependencies and relationships
|
||||||
|
- Existing architectural patterns
|
||||||
|
|
||||||
|
## NestJS Module Structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
common/
|
||||||
|
decorators/
|
||||||
|
current-user.decorator.ts
|
||||||
|
roles.decorator.ts
|
||||||
|
dto/
|
||||||
|
pagination.dto.ts
|
||||||
|
api-response.dto.ts
|
||||||
|
filters/
|
||||||
|
http-exception.filter.ts
|
||||||
|
all-exceptions.filter.ts
|
||||||
|
guards/
|
||||||
|
jwt-auth.guard.ts
|
||||||
|
roles.guard.ts
|
||||||
|
interceptors/
|
||||||
|
logging.interceptor.ts
|
||||||
|
transform.interceptor.ts
|
||||||
|
pipes/
|
||||||
|
validation.pipe.ts
|
||||||
|
interfaces/
|
||||||
|
pagination.interface.ts
|
||||||
|
utils/
|
||||||
|
helpers.ts
|
||||||
|
|
||||||
|
config/
|
||||||
|
app.config.ts
|
||||||
|
database.config.ts
|
||||||
|
jwt.config.ts
|
||||||
|
|
||||||
|
database/
|
||||||
|
migrations/
|
||||||
|
data-source.ts
|
||||||
|
|
||||||
|
modules/
|
||||||
|
products/
|
||||||
|
dto/
|
||||||
|
create-product.dto.ts
|
||||||
|
update-product.dto.ts
|
||||||
|
get-products.dto.ts
|
||||||
|
entities/
|
||||||
|
product.entity.ts
|
||||||
|
interfaces/
|
||||||
|
products-repository.interface.ts
|
||||||
|
products.controller.ts
|
||||||
|
products.service.ts
|
||||||
|
products.repository.ts
|
||||||
|
products.module.ts
|
||||||
|
|
||||||
|
categories/
|
||||||
|
dto/
|
||||||
|
entities/
|
||||||
|
categories.controller.ts
|
||||||
|
categories.service.ts
|
||||||
|
categories.repository.ts
|
||||||
|
categories.module.ts
|
||||||
|
|
||||||
|
transactions/
|
||||||
|
dto/
|
||||||
|
entities/
|
||||||
|
transactions.controller.ts
|
||||||
|
transactions.service.ts
|
||||||
|
transactions.repository.ts
|
||||||
|
transactions.module.ts
|
||||||
|
|
||||||
|
auth/
|
||||||
|
dto/
|
||||||
|
strategies/
|
||||||
|
jwt.strategy.ts
|
||||||
|
auth.controller.ts
|
||||||
|
auth.service.ts
|
||||||
|
auth.module.ts
|
||||||
|
|
||||||
|
app.module.ts
|
||||||
|
main.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Module Design:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// products.module.ts
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { ProductsController } from './products.controller';
|
||||||
|
import { ProductsService } from './products.service';
|
||||||
|
import { ProductsRepository } from './products.repository';
|
||||||
|
import { Product } from './entities/product.entity';
|
||||||
|
import { CategoriesModule } from '../categories/categories.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([Product]),
|
||||||
|
CategoriesModule, // Import when you need CategoryService
|
||||||
|
],
|
||||||
|
controllers: [ProductsController],
|
||||||
|
providers: [
|
||||||
|
ProductsService,
|
||||||
|
ProductsRepository,
|
||||||
|
],
|
||||||
|
exports: [ProductsService], // Export to use in other modules
|
||||||
|
})
|
||||||
|
export class ProductsModule {}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Service Layer Pattern:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// products.service.ts
|
||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { ProductsRepository } from './products.repository';
|
||||||
|
import { CategoriesService } from '../categories/categories.service';
|
||||||
|
import { CreateProductDto, UpdateProductDto, GetProductsDto } from './dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ProductsService {
|
||||||
|
constructor(
|
||||||
|
private readonly productsRepository: ProductsRepository,
|
||||||
|
private readonly categoriesService: CategoriesService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async findAll(query: GetProductsDto) {
|
||||||
|
const [products, total] = await this.productsRepository.findAll(query);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: products,
|
||||||
|
meta: {
|
||||||
|
page: query.page || 1,
|
||||||
|
limit: query.limit || 20,
|
||||||
|
total,
|
||||||
|
totalPages: Math.ceil(total / (query.limit || 20)),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(id: string) {
|
||||||
|
const product = await this.productsRepository.findOne(id);
|
||||||
|
|
||||||
|
if (!product) {
|
||||||
|
throw new NotFoundException(`Product with ID ${id} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return product;
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(createProductDto: CreateProductDto) {
|
||||||
|
// Validate category exists
|
||||||
|
await this.categoriesService.findOne(createProductDto.categoryId);
|
||||||
|
|
||||||
|
return this.productsRepository.create(createProductDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, updateProductDto: UpdateProductDto) {
|
||||||
|
await this.findOne(id); // Ensure exists
|
||||||
|
|
||||||
|
if (updateProductDto.categoryId) {
|
||||||
|
await this.categoriesService.findOne(updateProductDto.categoryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.productsRepository.update(id, updateProductDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(id: string) {
|
||||||
|
await this.findOne(id); // Ensure exists
|
||||||
|
return this.productsRepository.remove(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Business logic methods
|
||||||
|
async updateStock(id: string, quantity: number) {
|
||||||
|
const product = await this.findOne(id);
|
||||||
|
|
||||||
|
if (product.stockQuantity < quantity) {
|
||||||
|
throw new BadRequestException('Insufficient stock');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.productsRepository.updateStock(id, quantity);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getProductsByCategory(categoryId: string) {
|
||||||
|
await this.categoriesService.findOne(categoryId);
|
||||||
|
return this.productsRepository.findAll({ categoryId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Repository Pattern:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// products.repository.ts
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { Product } from './entities/product.entity';
|
||||||
|
import { IProductsRepository } from './interfaces/products-repository.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ProductsRepository implements IProductsRepository {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(Product)
|
||||||
|
private readonly repository: Repository<Product>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async findAll(query: any): Promise<[Product[], number]> {
|
||||||
|
// Implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(id: string): Promise<Product | null> {
|
||||||
|
return this.repository.findOne({
|
||||||
|
where: { id },
|
||||||
|
relations: ['category'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(data: any): Promise<Product> {
|
||||||
|
const product = this.repository.create(data);
|
||||||
|
return this.repository.save(product);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, data: any): Promise<Product> {
|
||||||
|
await this.repository.update(id, data);
|
||||||
|
return this.findOne(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(id: string): Promise<void> {
|
||||||
|
await this.repository.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repository interface for testing
|
||||||
|
export interface IProductsRepository {
|
||||||
|
findAll(query: any): Promise<[Product[], number]>;
|
||||||
|
findOne(id: string): Promise<Product | null>;
|
||||||
|
create(data: any): Promise<Product>;
|
||||||
|
update(id: string, data: any): Promise<Product>;
|
||||||
|
remove(id: string): Promise<void>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependency Injection Patterns:
|
||||||
|
|
||||||
|
### Constructor Injection (Recommended):
|
||||||
|
```typescript
|
||||||
|
@Injectable()
|
||||||
|
export class ProductsService {
|
||||||
|
constructor(
|
||||||
|
private readonly productsRepository: ProductsRepository,
|
||||||
|
private readonly categoriesService: CategoriesService,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Providers:
|
||||||
|
```typescript
|
||||||
|
// app.module.ts
|
||||||
|
@Module({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: 'CONFIG',
|
||||||
|
useValue: {
|
||||||
|
apiUrl: process.env.API_URL,
|
||||||
|
apiKey: process.env.API_KEY,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: 'PRODUCTS_REPOSITORY',
|
||||||
|
useClass: ProductsRepository,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: 'CACHE_MANAGER',
|
||||||
|
useFactory: (configService: ConfigService) => {
|
||||||
|
return createCacheManager(configService.get('REDIS_URL'));
|
||||||
|
},
|
||||||
|
inject: [ConfigService],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Custom Providers:
|
||||||
|
```typescript
|
||||||
|
@Injectable()
|
||||||
|
export class ProductsService {
|
||||||
|
constructor(
|
||||||
|
@Inject('CONFIG') private readonly config: any,
|
||||||
|
@Inject('PRODUCTS_REPOSITORY')
|
||||||
|
private readonly repository: IProductsRepository,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exception Handling:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// common/filters/http-exception.filter.ts
|
||||||
|
import {
|
||||||
|
ExceptionFilter,
|
||||||
|
Catch,
|
||||||
|
ArgumentsHost,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
|
||||||
|
@Catch()
|
||||||
|
export class AllExceptionsFilter implements ExceptionFilter {
|
||||||
|
catch(exception: unknown, host: ArgumentsHost) {
|
||||||
|
const ctx = host.switchToHttp();
|
||||||
|
const response = ctx.getResponse();
|
||||||
|
const request = ctx.getRequest();
|
||||||
|
|
||||||
|
const status =
|
||||||
|
exception instanceof HttpException
|
||||||
|
? exception.getStatus()
|
||||||
|
: HttpStatus.INTERNAL_SERVER_ERROR;
|
||||||
|
|
||||||
|
const message =
|
||||||
|
exception instanceof HttpException
|
||||||
|
? exception.getResponse()
|
||||||
|
: 'Internal server error';
|
||||||
|
|
||||||
|
response.status(status).json({
|
||||||
|
success: false,
|
||||||
|
error: {
|
||||||
|
statusCode: status,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
path: request.url,
|
||||||
|
message,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply globally in main.ts
|
||||||
|
app.useGlobalFilters(new AllExceptionsFilter());
|
||||||
|
```
|
||||||
|
|
||||||
|
## Interceptors:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// common/interceptors/transform.interceptor.ts
|
||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
NestInterceptor,
|
||||||
|
ExecutionContext,
|
||||||
|
CallHandler,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
|
export interface Response<T> {
|
||||||
|
success: boolean;
|
||||||
|
data: T;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TransformInterceptor<T>
|
||||||
|
implements NestInterceptor<T, Response<T>>
|
||||||
|
{
|
||||||
|
intercept(
|
||||||
|
context: ExecutionContext,
|
||||||
|
next: CallHandler,
|
||||||
|
): Observable<Response<T>> {
|
||||||
|
return next.handle().pipe(
|
||||||
|
map((data) => ({
|
||||||
|
success: true,
|
||||||
|
data,
|
||||||
|
message: 'Operation successful',
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply globally
|
||||||
|
app.useGlobalInterceptors(new TransformInterceptor());
|
||||||
|
```
|
||||||
|
|
||||||
|
## Guards:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// common/guards/roles.guard.ts
|
||||||
|
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
||||||
|
import { Reflector } from '@nestjs/core';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RolesGuard implements CanActivate {
|
||||||
|
constructor(private reflector: Reflector) {}
|
||||||
|
|
||||||
|
canActivate(context: ExecutionContext): boolean {
|
||||||
|
const requiredRoles = this.reflector.getAllAndOverride<string[]>('roles', [
|
||||||
|
context.getHandler(),
|
||||||
|
context.getClass(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!requiredRoles) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { user } = context.switchToHttp().getRequest();
|
||||||
|
return requiredRoles.some((role) => user.roles?.includes(role));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage with decorator
|
||||||
|
@Post()
|
||||||
|
@Roles('admin')
|
||||||
|
async create(@Body() dto: CreateProductDto) {
|
||||||
|
return this.productsService.create(dto);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration Management:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// config/app.config.ts
|
||||||
|
import { registerAs } from '@nestjs/config';
|
||||||
|
|
||||||
|
export default registerAs('app', () => ({
|
||||||
|
port: parseInt(process.env.PORT, 10) || 3000,
|
||||||
|
environment: process.env.NODE_ENV || 'development',
|
||||||
|
apiPrefix: process.env.API_PREFIX || 'api',
|
||||||
|
}));
|
||||||
|
|
||||||
|
// config/database.config.ts
|
||||||
|
import { registerAs } from '@nestjs/config';
|
||||||
|
|
||||||
|
export default registerAs('database', () => ({
|
||||||
|
host: process.env.DB_HOST || 'localhost',
|
||||||
|
port: parseInt(process.env.DB_PORT, 10) || 5432,
|
||||||
|
username: process.env.DB_USERNAME || 'postgres',
|
||||||
|
password: process.env.DB_PASSWORD || 'postgres',
|
||||||
|
database: process.env.DB_DATABASE || 'retail_pos',
|
||||||
|
}));
|
||||||
|
|
||||||
|
// app.module.ts
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
ConfigModule.forRoot({
|
||||||
|
isGlobal: true,
|
||||||
|
load: [appConfig, databaseConfig],
|
||||||
|
envFilePath: ['.env.local', '.env'],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
|
|
||||||
|
// Usage in service
|
||||||
|
@Injectable()
|
||||||
|
export class AppService {
|
||||||
|
constructor(
|
||||||
|
@Inject(appConfig.KEY)
|
||||||
|
private readonly config: ConfigType<typeof appConfig>,
|
||||||
|
) {
|
||||||
|
console.log(this.config.port); // Type-safe access
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Main Application Setup:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// main.ts
|
||||||
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
import { ValidationPipe, VersioningType } from '@nestjs/common';
|
||||||
|
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||||||
|
import { AppModule } from './app.module';
|
||||||
|
import { AllExceptionsFilter } from './common/filters/all-exceptions.filter';
|
||||||
|
import { TransformInterceptor } from './common/interceptors/transform.interceptor';
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
const app = await NestFactory.create(AppModule);
|
||||||
|
|
||||||
|
// Global prefix
|
||||||
|
app.setGlobalPrefix('api');
|
||||||
|
|
||||||
|
// Versioning
|
||||||
|
app.enableVersioning({
|
||||||
|
type: VersioningType.URI,
|
||||||
|
defaultVersion: '1',
|
||||||
|
});
|
||||||
|
|
||||||
|
// CORS
|
||||||
|
app.enableCors({
|
||||||
|
origin: process.env.CORS_ORIGIN || '*',
|
||||||
|
credentials: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Global pipes
|
||||||
|
app.useGlobalPipes(
|
||||||
|
new ValidationPipe({
|
||||||
|
whitelist: true,
|
||||||
|
transform: true,
|
||||||
|
forbidNonWhitelisted: true,
|
||||||
|
transformOptions: {
|
||||||
|
enableImplicitConversion: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Global filters
|
||||||
|
app.useGlobalFilters(new AllExceptionsFilter());
|
||||||
|
|
||||||
|
// Global interceptors
|
||||||
|
app.useGlobalInterceptors(new TransformInterceptor());
|
||||||
|
|
||||||
|
// Swagger documentation
|
||||||
|
const config = new DocumentBuilder()
|
||||||
|
.setTitle('Retail POS API')
|
||||||
|
.setDescription('API for Retail POS application')
|
||||||
|
.setVersion('1.0')
|
||||||
|
.addTag('products')
|
||||||
|
.addTag('categories')
|
||||||
|
.addTag('transactions')
|
||||||
|
.addBearerAuth()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
const document = SwaggerModule.createDocument(app, config);
|
||||||
|
SwaggerModule.setup('api/docs', app, document);
|
||||||
|
|
||||||
|
const port = process.env.PORT || 3000;
|
||||||
|
await app.listen(port);
|
||||||
|
|
||||||
|
console.log(`Application is running on: http://localhost:${port}`);
|
||||||
|
console.log(`Swagger docs: http://localhost:${port}/api/docs`);
|
||||||
|
}
|
||||||
|
|
||||||
|
bootstrap();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Architecture:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// products.service.spec.ts
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { ProductsService } from './products.service';
|
||||||
|
import { ProductsRepository } from './products.repository';
|
||||||
|
import { CategoriesService } from '../categories/categories.service';
|
||||||
|
|
||||||
|
describe('ProductsService', () => {
|
||||||
|
let service: ProductsService;
|
||||||
|
let repository: ProductsRepository;
|
||||||
|
|
||||||
|
const mockProductsRepository = {
|
||||||
|
findAll: jest.fn(),
|
||||||
|
findOne: jest.fn(),
|
||||||
|
create: jest.fn(),
|
||||||
|
update: jest.fn(),
|
||||||
|
remove: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockCategoriesService = {
|
||||||
|
findOne: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
ProductsService,
|
||||||
|
{
|
||||||
|
provide: ProductsRepository,
|
||||||
|
useValue: mockProductsRepository,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: CategoriesService,
|
||||||
|
useValue: mockCategoriesService,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<ProductsService>(ProductsService);
|
||||||
|
repository = module.get<ProductsRepository>(ProductsRepository);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('findOne', () => {
|
||||||
|
it('should return a product', async () => {
|
||||||
|
const product = { id: '1', name: 'Test' };
|
||||||
|
mockProductsRepository.findOne.mockResolvedValue(product);
|
||||||
|
|
||||||
|
const result = await service.findOne('1');
|
||||||
|
expect(result).toEqual(product);
|
||||||
|
expect(repository.findOne).toHaveBeenCalledWith('1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw NotFoundException when product not found', async () => {
|
||||||
|
mockProductsRepository.findOne.mockResolvedValue(null);
|
||||||
|
|
||||||
|
await expect(service.findOne('1')).rejects.toThrow(NotFoundException);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices:
|
||||||
|
|
||||||
|
1. **Module Organization**: One feature per module, export only what's needed
|
||||||
|
2. **Single Responsibility**: Each service should have one clear purpose
|
||||||
|
3. **Dependency Injection**: Use constructor injection, avoid circular dependencies
|
||||||
|
4. **Interface Segregation**: Define interfaces for repositories
|
||||||
|
5. **Error Handling**: Use NestJS exceptions, implement global filters
|
||||||
|
6. **Configuration**: Use ConfigModule, never hardcode values
|
||||||
|
7. **Validation**: Use DTOs with class-validator globally
|
||||||
|
8. **Documentation**: Document all endpoints with Swagger
|
||||||
|
9. **Testing**: Write unit tests for services, e2e tests for flows
|
||||||
|
10. **Separation of Concerns**: Controller → Service → Repository pattern
|
||||||
636
.claude/agents/nestjs-auth-expert.md
Normal file
636
.claude/agents/nestjs-auth-expert.md
Normal file
@@ -0,0 +1,636 @@
|
|||||||
|
---
|
||||||
|
name: nestjs-auth-expert
|
||||||
|
description: NestJS authentication and security specialist. MUST BE USED for JWT authentication, guards, security strategies, authorization, and API protection.
|
||||||
|
tools: Read, Write, Edit, Grep, Bash
|
||||||
|
---
|
||||||
|
|
||||||
|
You are a NestJS authentication and security expert specializing in:
|
||||||
|
- JWT authentication implementation
|
||||||
|
- Passport strategies (JWT, Local)
|
||||||
|
- Guards and authorization
|
||||||
|
- Role-based access control (RBAC)
|
||||||
|
- API security best practices
|
||||||
|
- Token management and refresh
|
||||||
|
- Password hashing and validation
|
||||||
|
|
||||||
|
## Key Responsibilities:
|
||||||
|
- Implement secure authentication flows
|
||||||
|
- Create JWT strategies and guards
|
||||||
|
- Design role-based authorization
|
||||||
|
- Handle token generation and validation
|
||||||
|
- Implement password security
|
||||||
|
- Protect API endpoints
|
||||||
|
- Manage user sessions
|
||||||
|
|
||||||
|
## Always Check First:
|
||||||
|
- `src/modules/auth/` - Existing auth implementation
|
||||||
|
- `src/common/guards/` - Guard implementations
|
||||||
|
- `src/common/decorators/` - Auth decorators
|
||||||
|
- JWT configuration and secrets
|
||||||
|
- Existing authentication strategy
|
||||||
|
|
||||||
|
## JWT Authentication Implementation:
|
||||||
|
|
||||||
|
### Installation:
|
||||||
|
```bash
|
||||||
|
npm install @nestjs/jwt @nestjs/passport passport passport-jwt
|
||||||
|
npm install -D @types/passport-jwt
|
||||||
|
npm install bcrypt
|
||||||
|
npm install -D @types/bcrypt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Auth Module:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// auth.module.ts
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
|
import { PassportModule } from '@nestjs/passport';
|
||||||
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
import { AuthController } from './auth.controller';
|
||||||
|
import { JwtStrategy } from './strategies/jwt.strategy';
|
||||||
|
import { LocalStrategy } from './strategies/local.strategy';
|
||||||
|
import { UsersModule } from '../users/users.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
PassportModule,
|
||||||
|
UsersModule,
|
||||||
|
JwtModule.registerAsync({
|
||||||
|
imports: [ConfigModule],
|
||||||
|
useFactory: async (configService: ConfigService) => ({
|
||||||
|
secret: configService.get<string>('JWT_SECRET'),
|
||||||
|
signOptions: {
|
||||||
|
expiresIn: configService.get<string>('JWT_EXPIRES_IN', '1d'),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
inject: [ConfigService],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
controllers: [AuthController],
|
||||||
|
providers: [AuthService, JwtStrategy, LocalStrategy],
|
||||||
|
exports: [AuthService],
|
||||||
|
})
|
||||||
|
export class AuthModule {}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Auth Service:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// auth.service.ts
|
||||||
|
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||||
|
import { JwtService } from '@nestjs/jwt';
|
||||||
|
import { UsersService } from '../users/users.service';
|
||||||
|
import * as bcrypt from 'bcrypt';
|
||||||
|
|
||||||
|
export interface JwtPayload {
|
||||||
|
sub: string;
|
||||||
|
email: string;
|
||||||
|
roles: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthService {
|
||||||
|
constructor(
|
||||||
|
private readonly usersService: UsersService,
|
||||||
|
private readonly jwtService: JwtService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async validateUser(email: string, password: string): Promise<any> {
|
||||||
|
const user = await this.usersService.findByEmail(email);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new UnauthorizedException('Invalid credentials');
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPasswordValid = await bcrypt.compare(password, user.password);
|
||||||
|
|
||||||
|
if (!isPasswordValid) {
|
||||||
|
throw new UnauthorizedException('Invalid credentials');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { password: _, ...result } = user;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async login(user: any) {
|
||||||
|
const payload: JwtPayload = {
|
||||||
|
sub: user.id,
|
||||||
|
email: user.email,
|
||||||
|
roles: user.roles || [],
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
access_token: this.jwtService.sign(payload),
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
roles: user.roles,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async register(registerDto: RegisterDto) {
|
||||||
|
// Check if user exists
|
||||||
|
const existingUser = await this.usersService.findByEmail(registerDto.email);
|
||||||
|
|
||||||
|
if (existingUser) {
|
||||||
|
throw new BadRequestException('Email already registered');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash password
|
||||||
|
const hashedPassword = await bcrypt.hash(registerDto.password, 10);
|
||||||
|
|
||||||
|
// Create user
|
||||||
|
const user = await this.usersService.create({
|
||||||
|
...registerDto,
|
||||||
|
password: hashedPassword,
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.login(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateToken(token: string): Promise<any> {
|
||||||
|
try {
|
||||||
|
const payload = this.jwtService.verify(token);
|
||||||
|
return payload;
|
||||||
|
} catch (error) {
|
||||||
|
throw new UnauthorizedException('Invalid token');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async refreshToken(userId: string) {
|
||||||
|
const user = await this.usersService.findOne(userId);
|
||||||
|
return this.login(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### JWT Strategy:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// strategies/jwt.strategy.ts
|
||||||
|
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||||
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
|
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { UsersService } from '../../users/users.service';
|
||||||
|
import { JwtPayload } from '../auth.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||||
|
constructor(
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
private readonly usersService: UsersService,
|
||||||
|
) {
|
||||||
|
super({
|
||||||
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||||
|
ignoreExpiration: false,
|
||||||
|
secretOrKey: configService.get<string>('JWT_SECRET'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async validate(payload: JwtPayload) {
|
||||||
|
const user = await this.usersService.findOne(payload.sub);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new UnauthorizedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
roles: user.roles,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Local Strategy (for login):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// strategies/local.strategy.ts
|
||||||
|
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||||
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
|
import { Strategy } from 'passport-local';
|
||||||
|
import { AuthService } from '../auth.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LocalStrategy extends PassportStrategy(Strategy) {
|
||||||
|
constructor(private authService: AuthService) {
|
||||||
|
super({
|
||||||
|
usernameField: 'email',
|
||||||
|
passwordField: 'password',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async validate(email: string, password: string): Promise<any> {
|
||||||
|
const user = await this.authService.validateUser(email, password);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new UnauthorizedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Auth Controller:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// auth.controller.ts
|
||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
UseGuards,
|
||||||
|
Request,
|
||||||
|
Get,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
import { LocalAuthGuard } from './guards/local-auth.guard';
|
||||||
|
import { JwtAuthGuard } from './guards/jwt-auth.guard';
|
||||||
|
import { LoginDto, RegisterDto } from './dto';
|
||||||
|
|
||||||
|
@ApiTags('auth')
|
||||||
|
@Controller('auth')
|
||||||
|
export class AuthController {
|
||||||
|
constructor(private readonly authService: AuthService) {}
|
||||||
|
|
||||||
|
@Post('register')
|
||||||
|
@ApiOperation({ summary: 'Register new user' })
|
||||||
|
@ApiResponse({ status: 201, description: 'User registered successfully' })
|
||||||
|
@ApiResponse({ status: 400, description: 'Email already registered' })
|
||||||
|
async register(@Body() registerDto: RegisterDto) {
|
||||||
|
return this.authService.register(registerDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('login')
|
||||||
|
@UseGuards(LocalAuthGuard)
|
||||||
|
@ApiOperation({ summary: 'Login user' })
|
||||||
|
@ApiResponse({ status: 200, description: 'Login successful' })
|
||||||
|
@ApiResponse({ status: 401, description: 'Invalid credentials' })
|
||||||
|
async login(@Body() loginDto: LoginDto, @Request() req) {
|
||||||
|
return this.authService.login(req.user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('profile')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@ApiOperation({ summary: 'Get current user profile' })
|
||||||
|
@ApiResponse({ status: 200, description: 'Profile retrieved' })
|
||||||
|
@ApiResponse({ status: 401, description: 'Unauthorized' })
|
||||||
|
async getProfile(@Request() req) {
|
||||||
|
return req.user;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('refresh')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@ApiOperation({ summary: 'Refresh access token' })
|
||||||
|
async refreshToken(@Request() req) {
|
||||||
|
return this.authService.refreshToken(req.user.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Auth Guards:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// guards/jwt-auth.guard.ts
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JwtAuthGuard extends AuthGuard('jwt') {}
|
||||||
|
|
||||||
|
// guards/local-auth.guard.ts
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LocalAuthGuard extends AuthGuard('local') {}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Roles Guard:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// guards/roles.guard.ts
|
||||||
|
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
||||||
|
import { Reflector } from '@nestjs/core';
|
||||||
|
import { ROLES_KEY } from '../decorators/roles.decorator';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RolesGuard implements CanActivate {
|
||||||
|
constructor(private reflector: Reflector) {}
|
||||||
|
|
||||||
|
canActivate(context: ExecutionContext): boolean {
|
||||||
|
const requiredRoles = this.reflector.getAllAndOverride<string[]>(
|
||||||
|
ROLES_KEY,
|
||||||
|
[context.getHandler(), context.getClass()],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!requiredRoles) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { user } = context.switchToHttp().getRequest();
|
||||||
|
|
||||||
|
return requiredRoles.some((role) => user.roles?.includes(role));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Decorators:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// decorators/roles.decorator.ts
|
||||||
|
import { SetMetadata } from '@nestjs/common';
|
||||||
|
|
||||||
|
export const ROLES_KEY = 'roles';
|
||||||
|
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
|
||||||
|
|
||||||
|
// decorators/current-user.decorator.ts
|
||||||
|
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||||
|
|
||||||
|
export const CurrentUser = createParamDecorator(
|
||||||
|
(data: unknown, ctx: ExecutionContext) => {
|
||||||
|
const request = ctx.switchToHttp().getRequest();
|
||||||
|
return request.user;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// decorators/public.decorator.ts
|
||||||
|
import { SetMetadata } from '@nestjs/common';
|
||||||
|
|
||||||
|
export const IS_PUBLIC_KEY = 'isPublic';
|
||||||
|
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
|
||||||
|
```
|
||||||
|
|
||||||
|
### DTOs:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// dto/login.dto.ts
|
||||||
|
import { IsEmail, IsString, MinLength } from 'class-validator';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class LoginDto {
|
||||||
|
@ApiProperty({ example: 'user@example.com' })
|
||||||
|
@IsEmail()
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'Password123!' })
|
||||||
|
@IsString()
|
||||||
|
@MinLength(8)
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// dto/register.dto.ts
|
||||||
|
import { IsEmail, IsString, MinLength, MaxLength } from 'class-validator';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class RegisterDto {
|
||||||
|
@ApiProperty({ example: 'John Doe' })
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(255)
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'user@example.com' })
|
||||||
|
@IsEmail()
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'Password123!' })
|
||||||
|
@IsString()
|
||||||
|
@MinLength(8)
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Protecting Routes:
|
||||||
|
|
||||||
|
### Using Guards:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Protect single endpoint
|
||||||
|
@Get('admin')
|
||||||
|
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||||
|
@Roles('admin')
|
||||||
|
async adminOnly() {
|
||||||
|
return 'Admin only content';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Protect entire controller
|
||||||
|
@Controller('products')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
export class ProductsController {
|
||||||
|
// All routes protected by JWT
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public route in protected controller
|
||||||
|
@Get('public')
|
||||||
|
@Public()
|
||||||
|
async publicRoute() {
|
||||||
|
return 'This is public';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Global JWT Guard:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// app.module.ts
|
||||||
|
import { APP_GUARD } from '@nestjs/core';
|
||||||
|
import { JwtAuthGuard } from './common/guards/jwt-auth.guard';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: APP_GUARD,
|
||||||
|
useClass: JwtAuthGuard,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
|
|
||||||
|
// Enhanced JWT guard to respect @Public decorator
|
||||||
|
@Injectable()
|
||||||
|
export class JwtAuthGuard extends AuthGuard('jwt') {
|
||||||
|
constructor(private reflector: Reflector) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
canActivate(context: ExecutionContext) {
|
||||||
|
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||||
|
context.getHandler(),
|
||||||
|
context.getClass(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (isPublic) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.canActivate(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Refresh Token Pattern:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// entities/refresh-token.entity.ts
|
||||||
|
@Entity('refresh_tokens')
|
||||||
|
export class RefreshToken {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ type: 'uuid' })
|
||||||
|
userId: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar' })
|
||||||
|
token: string;
|
||||||
|
|
||||||
|
@Column({ type: 'timestamp' })
|
||||||
|
expiresAt: Date;
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@ManyToOne(() => User)
|
||||||
|
@JoinColumn({ name: 'userId' })
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
// auth.service.ts - Extended
|
||||||
|
async login(user: any) {
|
||||||
|
const payload: JwtPayload = {
|
||||||
|
sub: user.id,
|
||||||
|
email: user.email,
|
||||||
|
roles: user.roles,
|
||||||
|
};
|
||||||
|
|
||||||
|
const accessToken = this.jwtService.sign(payload);
|
||||||
|
const refreshToken = await this.createRefreshToken(user.id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
access_token: accessToken,
|
||||||
|
refresh_token: refreshToken,
|
||||||
|
user: { /* user data */ },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async createRefreshToken(userId: string): Promise<string> {
|
||||||
|
const token = randomBytes(32).toString('hex');
|
||||||
|
const expiresAt = new Date();
|
||||||
|
expiresAt.setDate(expiresAt.getDate() + 7); // 7 days
|
||||||
|
|
||||||
|
await this.refreshTokenRepository.save({
|
||||||
|
userId,
|
||||||
|
token,
|
||||||
|
expiresAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
async refreshAccessToken(refreshToken: string) {
|
||||||
|
const token = await this.refreshTokenRepository.findOne({
|
||||||
|
where: { token: refreshToken },
|
||||||
|
relations: ['user'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!token || token.expiresAt < new Date()) {
|
||||||
|
throw new UnauthorizedException('Invalid refresh token');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.login(token.user);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Best Practices:
|
||||||
|
|
||||||
|
### Environment Variables:
|
||||||
|
```bash
|
||||||
|
# .env
|
||||||
|
JWT_SECRET=your-super-secret-key-change-this-in-production
|
||||||
|
JWT_EXPIRES_IN=1d
|
||||||
|
REFRESH_TOKEN_EXPIRES_IN=7d
|
||||||
|
BCRYPT_ROUNDS=10
|
||||||
|
```
|
||||||
|
|
||||||
|
### Password Hashing:
|
||||||
|
```typescript
|
||||||
|
// Always hash passwords
|
||||||
|
const hashedPassword = await bcrypt.hash(password, 10);
|
||||||
|
|
||||||
|
// Never return password in responses
|
||||||
|
const { password, ...user } = foundUser;
|
||||||
|
return user;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rate Limiting:
|
||||||
|
```bash
|
||||||
|
npm install @nestjs/throttler
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// app.module.ts
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
ThrottlerModule.forRoot({
|
||||||
|
ttl: 60,
|
||||||
|
limit: 10,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: APP_GUARD,
|
||||||
|
useClass: ThrottlerGuard,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
|
|
||||||
|
// Customize per route
|
||||||
|
@Throttle(3, 60) // 3 requests per 60 seconds
|
||||||
|
@Post('login')
|
||||||
|
async login() { }
|
||||||
|
```
|
||||||
|
|
||||||
|
### CORS Configuration:
|
||||||
|
```typescript
|
||||||
|
// main.ts
|
||||||
|
app.enableCors({
|
||||||
|
origin: process.env.CORS_ORIGIN || 'http://localhost:3000',
|
||||||
|
credentials: true,
|
||||||
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
|
||||||
|
allowedHeaders: ['Content-Type', 'Authorization'],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Helmet for Security Headers:
|
||||||
|
```bash
|
||||||
|
npm install helmet
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// main.ts
|
||||||
|
import helmet from 'helmet';
|
||||||
|
|
||||||
|
app.use(helmet());
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices:
|
||||||
|
|
||||||
|
1. **Never store passwords in plain text** - Always hash with bcrypt
|
||||||
|
2. **Use strong JWT secrets** - Generate random, long secrets
|
||||||
|
3. **Set appropriate token expiration** - Short for access, longer for refresh
|
||||||
|
4. **Validate all inputs** - Use DTOs with class-validator
|
||||||
|
5. **Implement rate limiting** - Prevent brute force attacks
|
||||||
|
6. **Use HTTPS in production** - Never send tokens over HTTP
|
||||||
|
7. **Implement refresh tokens** - For better security and UX
|
||||||
|
8. **Log authentication events** - For security auditing
|
||||||
|
9. **Handle token expiration gracefully** - Return clear error messages
|
||||||
|
10. **Use role-based access control** - Implement granular permissions
|
||||||
717
.claude/agents/nestjs-database-expert.md
Normal file
717
.claude/agents/nestjs-database-expert.md
Normal file
@@ -0,0 +1,717 @@
|
|||||||
|
---
|
||||||
|
name: nestjs-database-expert
|
||||||
|
description: NestJS database specialist. MUST BE USED for TypeORM/Prisma entities, database operations, migrations, queries, relationships, and data persistence.
|
||||||
|
tools: Read, Write, Edit, Grep, Bash
|
||||||
|
---
|
||||||
|
|
||||||
|
You are a NestJS database expert specializing in:
|
||||||
|
- TypeORM and Prisma ORM
|
||||||
|
- Database schema design and entity modeling
|
||||||
|
- Database migrations and versioning
|
||||||
|
- Complex queries and optimization
|
||||||
|
- Relationship management (one-to-many, many-to-many)
|
||||||
|
- Transaction handling
|
||||||
|
- Database performance tuning
|
||||||
|
|
||||||
|
## Key Responsibilities:
|
||||||
|
- Design efficient database schemas
|
||||||
|
- Create TypeORM entities or Prisma models
|
||||||
|
- Implement repository patterns
|
||||||
|
- Write optimized database queries
|
||||||
|
- Handle database transactions
|
||||||
|
- Design proper indexing strategies
|
||||||
|
- Manage database migrations
|
||||||
|
|
||||||
|
## Always Check First:
|
||||||
|
- `src/database/` - Database configuration and migrations
|
||||||
|
- `src/modules/*/entities/` - Existing entity definitions
|
||||||
|
- `ormconfig.json` or `data-source.ts` - TypeORM configuration
|
||||||
|
- `prisma/schema.prisma` - Prisma schema file
|
||||||
|
- Current database relationships
|
||||||
|
- Existing migration files
|
||||||
|
|
||||||
|
## Database Choice:
|
||||||
|
This guide covers both **TypeORM** (default) and **Prisma** patterns. TypeORM is more commonly used with NestJS, but Prisma is gaining popularity.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# TypeORM Implementation
|
||||||
|
|
||||||
|
## Entity Definition:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
ManyToOne,
|
||||||
|
OneToMany,
|
||||||
|
JoinColumn,
|
||||||
|
Index,
|
||||||
|
} from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('products')
|
||||||
|
@Index(['name', 'categoryId']) // Composite index for filtering
|
||||||
|
export class Product {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 255 })
|
||||||
|
@Index() // Index for search
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
@Column({ type: 'decimal', precision: 10, scale: 2 })
|
||||||
|
price: number;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', nullable: true })
|
||||||
|
imageUrl: string;
|
||||||
|
|
||||||
|
@Column({ type: 'uuid' })
|
||||||
|
categoryId: string;
|
||||||
|
|
||||||
|
@Column({ type: 'int', default: 0 })
|
||||||
|
stockQuantity: number;
|
||||||
|
|
||||||
|
@Column({ type: 'boolean', default: true })
|
||||||
|
isAvailable: boolean;
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn()
|
||||||
|
updatedAt: Date;
|
||||||
|
|
||||||
|
// Relationships
|
||||||
|
@ManyToOne(() => Category, category => category.products, {
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
})
|
||||||
|
@JoinColumn({ name: 'categoryId' })
|
||||||
|
category: Category;
|
||||||
|
|
||||||
|
@OneToMany(() => TransactionItem, item => item.product)
|
||||||
|
transactionItems: TransactionItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity('categories')
|
||||||
|
export class Category {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 255, unique: true })
|
||||||
|
@Index()
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', nullable: true })
|
||||||
|
iconPath: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', nullable: true })
|
||||||
|
color: string;
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn()
|
||||||
|
updatedAt: Date;
|
||||||
|
|
||||||
|
// Virtual field (not stored in DB)
|
||||||
|
@Column({ type: 'int', default: 0 })
|
||||||
|
productCount: number;
|
||||||
|
|
||||||
|
// Relationships
|
||||||
|
@OneToMany(() => Product, product => product.category)
|
||||||
|
products: Product[];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity('transactions')
|
||||||
|
export class Transaction {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ type: 'decimal', precision: 10, scale: 2 })
|
||||||
|
subtotal: number;
|
||||||
|
|
||||||
|
@Column({ type: 'decimal', precision: 10, scale: 2, default: 0 })
|
||||||
|
tax: number;
|
||||||
|
|
||||||
|
@Column({ type: 'decimal', precision: 10, scale: 2, default: 0 })
|
||||||
|
discount: number;
|
||||||
|
|
||||||
|
@Column({ type: 'decimal', precision: 10, scale: 2 })
|
||||||
|
total: number;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 50 })
|
||||||
|
paymentMethod: string;
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
completedAt: Date;
|
||||||
|
|
||||||
|
// Relationships
|
||||||
|
@OneToMany(() => TransactionItem, item => item.transaction, {
|
||||||
|
cascade: true,
|
||||||
|
})
|
||||||
|
items: TransactionItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity('transaction_items')
|
||||||
|
export class TransactionItem {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ type: 'uuid' })
|
||||||
|
transactionId: string;
|
||||||
|
|
||||||
|
@Column({ type: 'uuid' })
|
||||||
|
productId: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 255 })
|
||||||
|
productName: string;
|
||||||
|
|
||||||
|
@Column({ type: 'decimal', precision: 10, scale: 2 })
|
||||||
|
price: number;
|
||||||
|
|
||||||
|
@Column({ type: 'int' })
|
||||||
|
quantity: number;
|
||||||
|
|
||||||
|
@Column({ type: 'decimal', precision: 10, scale: 2 })
|
||||||
|
lineTotal: number;
|
||||||
|
|
||||||
|
// Relationships
|
||||||
|
@ManyToOne(() => Transaction, transaction => transaction.items, {
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
})
|
||||||
|
@JoinColumn({ name: 'transactionId' })
|
||||||
|
transaction: Transaction;
|
||||||
|
|
||||||
|
@ManyToOne(() => Product, product => product.transactionItems)
|
||||||
|
@JoinColumn({ name: 'productId' })
|
||||||
|
product: Product;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Repository Implementation:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository, Like, Between } from 'typeorm';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ProductsRepository {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(Product)
|
||||||
|
private readonly productRepository: Repository<Product>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async findAll(query: GetProductsDto): Promise<[Product[], number]> {
|
||||||
|
const {
|
||||||
|
page = 1,
|
||||||
|
limit = 20,
|
||||||
|
categoryId,
|
||||||
|
search,
|
||||||
|
minPrice,
|
||||||
|
maxPrice,
|
||||||
|
} = query;
|
||||||
|
|
||||||
|
const queryBuilder = this.productRepository
|
||||||
|
.createQueryBuilder('product')
|
||||||
|
.leftJoinAndSelect('product.category', 'category');
|
||||||
|
|
||||||
|
// Filtering
|
||||||
|
if (categoryId) {
|
||||||
|
queryBuilder.andWhere('product.categoryId = :categoryId', { categoryId });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
queryBuilder.andWhere(
|
||||||
|
'(product.name ILIKE :search OR product.description ILIKE :search)',
|
||||||
|
{ search: `%${search}%` },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minPrice !== undefined || maxPrice !== undefined) {
|
||||||
|
if (minPrice) {
|
||||||
|
queryBuilder.andWhere('product.price >= :minPrice', { minPrice });
|
||||||
|
}
|
||||||
|
if (maxPrice) {
|
||||||
|
queryBuilder.andWhere('product.price <= :maxPrice', { maxPrice });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
const skip = (page - 1) * limit;
|
||||||
|
queryBuilder.skip(skip).take(limit);
|
||||||
|
|
||||||
|
// Sorting
|
||||||
|
queryBuilder.orderBy('product.name', 'ASC');
|
||||||
|
|
||||||
|
return queryBuilder.getManyAndCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(id: string): Promise<Product> {
|
||||||
|
return this.productRepository.findOne({
|
||||||
|
where: { id },
|
||||||
|
relations: ['category'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(createProductDto: CreateProductDto): Promise<Product> {
|
||||||
|
const product = this.productRepository.create(createProductDto);
|
||||||
|
return this.productRepository.save(product);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(
|
||||||
|
id: string,
|
||||||
|
updateProductDto: UpdateProductDto,
|
||||||
|
): Promise<Product> {
|
||||||
|
await this.productRepository.update(id, updateProductDto);
|
||||||
|
return this.findOne(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(id: string): Promise<void> {
|
||||||
|
await this.productRepository.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateStock(id: string, quantity: number): Promise<void> {
|
||||||
|
await this.productRepository.decrement(
|
||||||
|
{ id },
|
||||||
|
'stockQuantity',
|
||||||
|
quantity,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async bulkCreate(products: CreateProductDto[]): Promise<Product[]> {
|
||||||
|
const entities = this.productRepository.create(products);
|
||||||
|
return this.productRepository.save(entities);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Transaction Handling:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { DataSource } from 'typeorm';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TransactionsService {
|
||||||
|
constructor(
|
||||||
|
private readonly dataSource: DataSource,
|
||||||
|
@InjectRepository(Transaction)
|
||||||
|
private readonly transactionRepo: Repository<Transaction>,
|
||||||
|
@InjectRepository(Product)
|
||||||
|
private readonly productRepo: Repository<Product>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async createTransaction(
|
||||||
|
createTransactionDto: CreateTransactionDto,
|
||||||
|
): Promise<Transaction> {
|
||||||
|
const queryRunner = this.dataSource.createQueryRunner();
|
||||||
|
await queryRunner.connect();
|
||||||
|
await queryRunner.startTransaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create transaction
|
||||||
|
const transaction = queryRunner.manager.create(Transaction, {
|
||||||
|
subtotal: createTransactionDto.subtotal,
|
||||||
|
tax: createTransactionDto.tax,
|
||||||
|
discount: createTransactionDto.discount,
|
||||||
|
total: createTransactionDto.total,
|
||||||
|
paymentMethod: createTransactionDto.paymentMethod,
|
||||||
|
});
|
||||||
|
await queryRunner.manager.save(transaction);
|
||||||
|
|
||||||
|
// Create transaction items and update stock
|
||||||
|
for (const item of createTransactionDto.items) {
|
||||||
|
// Create transaction item
|
||||||
|
const transactionItem = queryRunner.manager.create(TransactionItem, {
|
||||||
|
transactionId: transaction.id,
|
||||||
|
productId: item.productId,
|
||||||
|
productName: item.productName,
|
||||||
|
price: item.price,
|
||||||
|
quantity: item.quantity,
|
||||||
|
lineTotal: item.price * item.quantity,
|
||||||
|
});
|
||||||
|
await queryRunner.manager.save(transactionItem);
|
||||||
|
|
||||||
|
// Update product stock
|
||||||
|
await queryRunner.manager.decrement(
|
||||||
|
Product,
|
||||||
|
{ id: item.productId },
|
||||||
|
'stockQuantity',
|
||||||
|
item.quantity,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await queryRunner.commitTransaction();
|
||||||
|
|
||||||
|
// Return transaction with items
|
||||||
|
return this.transactionRepo.findOne({
|
||||||
|
where: { id: transaction.id },
|
||||||
|
relations: ['items'],
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
await queryRunner.rollbackTransaction();
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
await queryRunner.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Database Configuration:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/database/data-source.ts
|
||||||
|
import { DataSource } from 'typeorm';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
|
export const AppDataSource = new DataSource({
|
||||||
|
type: 'postgres',
|
||||||
|
host: process.env.DB_HOST || 'localhost',
|
||||||
|
port: parseInt(process.env.DB_PORT) || 5432,
|
||||||
|
username: process.env.DB_USERNAME || 'postgres',
|
||||||
|
password: process.env.DB_PASSWORD || 'postgres',
|
||||||
|
database: process.env.DB_DATABASE || 'retail_pos',
|
||||||
|
entities: ['dist/**/*.entity.js'],
|
||||||
|
migrations: ['dist/database/migrations/*.js'],
|
||||||
|
synchronize: false, // Never use true in production
|
||||||
|
logging: process.env.NODE_ENV === 'development',
|
||||||
|
});
|
||||||
|
|
||||||
|
// In app.module.ts
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forRootAsync({
|
||||||
|
imports: [ConfigModule],
|
||||||
|
useFactory: (configService: ConfigService) => ({
|
||||||
|
type: 'postgres',
|
||||||
|
host: configService.get('DB_HOST'),
|
||||||
|
port: configService.get('DB_PORT'),
|
||||||
|
username: configService.get('DB_USERNAME'),
|
||||||
|
password: configService.get('DB_PASSWORD'),
|
||||||
|
database: configService.get('DB_DATABASE'),
|
||||||
|
entities: [__dirname + '/**/*.entity{.ts,.js}'],
|
||||||
|
synchronize: false,
|
||||||
|
logging: configService.get('NODE_ENV') === 'development',
|
||||||
|
}),
|
||||||
|
inject: [ConfigService],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migrations:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate migration
|
||||||
|
npm run typeorm migration:generate -- -n CreateProductsTable
|
||||||
|
|
||||||
|
# Run migrations
|
||||||
|
npm run typeorm migration:run
|
||||||
|
|
||||||
|
# Revert migration
|
||||||
|
npm run typeorm migration:revert
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Migration example
|
||||||
|
import { MigrationInterface, QueryRunner, Table } from 'typeorm';
|
||||||
|
|
||||||
|
export class CreateProductsTable1234567890 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.createTable(
|
||||||
|
new Table({
|
||||||
|
name: 'products',
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
type: 'uuid',
|
||||||
|
isPrimary: true,
|
||||||
|
generationStrategy: 'uuid',
|
||||||
|
default: 'uuid_generate_v4()',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
type: 'varchar',
|
||||||
|
length: '255',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'description',
|
||||||
|
type: 'text',
|
||||||
|
isNullable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'price',
|
||||||
|
type: 'decimal',
|
||||||
|
precision: 10,
|
||||||
|
scale: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'imageUrl',
|
||||||
|
type: 'varchar',
|
||||||
|
isNullable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'categoryId',
|
||||||
|
type: 'uuid',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'stockQuantity',
|
||||||
|
type: 'int',
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'isAvailable',
|
||||||
|
type: 'boolean',
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'createdAt',
|
||||||
|
type: 'timestamp',
|
||||||
|
default: 'CURRENT_TIMESTAMP',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'updatedAt',
|
||||||
|
type: 'timestamp',
|
||||||
|
default: 'CURRENT_TIMESTAMP',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
indices: [
|
||||||
|
{
|
||||||
|
columnNames: ['name'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
columnNames: ['categoryId'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
foreignKeys: [
|
||||||
|
{
|
||||||
|
columnNames: ['categoryId'],
|
||||||
|
referencedTableName: 'categories',
|
||||||
|
referencedColumnNames: ['id'],
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.dropTable('products');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Prisma Implementation (Alternative)
|
||||||
|
|
||||||
|
## Prisma Schema:
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
// prisma/schema.prisma
|
||||||
|
|
||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "postgresql"
|
||||||
|
url = env("DATABASE_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Category {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
name String @unique @db.VarChar(255)
|
||||||
|
description String? @db.Text
|
||||||
|
iconPath String? @db.VarChar(255)
|
||||||
|
color String? @db.VarChar(50)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
products Product[]
|
||||||
|
|
||||||
|
@@map("categories")
|
||||||
|
@@index([name])
|
||||||
|
}
|
||||||
|
|
||||||
|
model Product {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
name String @db.VarChar(255)
|
||||||
|
description String? @db.Text
|
||||||
|
price Decimal @db.Decimal(10, 2)
|
||||||
|
imageUrl String? @db.VarChar(255)
|
||||||
|
categoryId String
|
||||||
|
stockQuantity Int @default(0)
|
||||||
|
isAvailable Boolean @default(true)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
category Category @relation(fields: [categoryId], references: [id], onDelete: Cascade)
|
||||||
|
transactionItems TransactionItem[]
|
||||||
|
|
||||||
|
@@map("products")
|
||||||
|
@@index([name])
|
||||||
|
@@index([categoryId])
|
||||||
|
@@index([name, categoryId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model Transaction {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
subtotal Decimal @db.Decimal(10, 2)
|
||||||
|
tax Decimal @default(0) @db.Decimal(10, 2)
|
||||||
|
discount Decimal @default(0) @db.Decimal(10, 2)
|
||||||
|
total Decimal @db.Decimal(10, 2)
|
||||||
|
paymentMethod String @db.VarChar(50)
|
||||||
|
completedAt DateTime @default(now())
|
||||||
|
|
||||||
|
items TransactionItem[]
|
||||||
|
|
||||||
|
@@map("transactions")
|
||||||
|
}
|
||||||
|
|
||||||
|
model TransactionItem {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
transactionId String
|
||||||
|
productId String
|
||||||
|
productName String @db.VarChar(255)
|
||||||
|
price Decimal @db.Decimal(10, 2)
|
||||||
|
quantity Int
|
||||||
|
lineTotal Decimal @db.Decimal(10, 2)
|
||||||
|
|
||||||
|
transaction Transaction @relation(fields: [transactionId], references: [id], onDelete: Cascade)
|
||||||
|
product Product @relation(fields: [productId], references: [id])
|
||||||
|
|
||||||
|
@@map("transaction_items")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prisma Service:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PrismaService extends PrismaClient implements OnModuleInit {
|
||||||
|
async onModuleInit() {
|
||||||
|
await this.$connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
async onModuleDestroy() {
|
||||||
|
await this.$disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prisma Repository:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@Injectable()
|
||||||
|
export class ProductsRepository {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async findAll(query: GetProductsDto) {
|
||||||
|
const { page = 1, limit = 20, categoryId, search } = query;
|
||||||
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
|
const where = {
|
||||||
|
...(categoryId && { categoryId }),
|
||||||
|
...(search && {
|
||||||
|
OR: [
|
||||||
|
{ name: { contains: search, mode: 'insensitive' } },
|
||||||
|
{ description: { contains: search, mode: 'insensitive' } },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const [data, total] = await Promise.all([
|
||||||
|
this.prisma.product.findMany({
|
||||||
|
where,
|
||||||
|
include: { category: true },
|
||||||
|
skip,
|
||||||
|
take: limit,
|
||||||
|
orderBy: { name: 'asc' },
|
||||||
|
}),
|
||||||
|
this.prisma.product.count({ where }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { data, total };
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(createProductDto: CreateProductDto) {
|
||||||
|
return this.prisma.product.create({
|
||||||
|
data: createProductDto,
|
||||||
|
include: { category: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Performance Optimization:
|
||||||
|
|
||||||
|
### Indexing Strategy:
|
||||||
|
```typescript
|
||||||
|
// Add indexes for frequently queried fields
|
||||||
|
@Index(['name']) // Single column index
|
||||||
|
@Index(['name', 'categoryId']) // Composite index
|
||||||
|
@Index(['createdAt']) // Date range queries
|
||||||
|
```
|
||||||
|
|
||||||
|
### Query Optimization:
|
||||||
|
```typescript
|
||||||
|
// Bad - N+1 problem
|
||||||
|
const products = await productRepo.find();
|
||||||
|
for (const product of products) {
|
||||||
|
product.category = await categoryRepo.findOne(product.categoryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Good - Use joins/relations
|
||||||
|
const products = await productRepo.find({
|
||||||
|
relations: ['category'],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Better - Use query builder for complex queries
|
||||||
|
const products = await productRepo
|
||||||
|
.createQueryBuilder('product')
|
||||||
|
.leftJoinAndSelect('product.category', 'category')
|
||||||
|
.where('product.isAvailable = :available', { available: true })
|
||||||
|
.getMany();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bulk Operations:
|
||||||
|
```typescript
|
||||||
|
// Insert multiple records efficiently
|
||||||
|
await productRepo.insert(products);
|
||||||
|
|
||||||
|
// Update multiple records
|
||||||
|
await productRepo.update(
|
||||||
|
{ categoryId: oldCategoryId },
|
||||||
|
{ categoryId: newCategoryId },
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices:
|
||||||
|
|
||||||
|
1. **Always use migrations** - Never use `synchronize: true` in production
|
||||||
|
2. **Index frequently queried columns** - name, foreign keys, dates
|
||||||
|
3. **Use transactions** for operations affecting multiple tables
|
||||||
|
4. **Implement soft deletes** when data history is important
|
||||||
|
5. **Use query builders** for complex queries
|
||||||
|
6. **Avoid N+1 queries** - use eager loading or joins
|
||||||
|
7. **Implement connection pooling** for better performance
|
||||||
|
8. **Use database constraints** for data integrity
|
||||||
|
9. **Monitor slow queries** and optimize them
|
||||||
|
10. **Use prepared statements** to prevent SQL injection
|
||||||
566
.claude/agents/nestjs-performance-expert.md
Normal file
566
.claude/agents/nestjs-performance-expert.md
Normal file
@@ -0,0 +1,566 @@
|
|||||||
|
---
|
||||||
|
name: nestjs-performance-expert
|
||||||
|
description: NestJS performance optimization specialist. MUST BE USED for caching, query optimization, rate limiting, compression, and API performance improvements.
|
||||||
|
tools: Read, Write, Edit, Grep, Bash
|
||||||
|
---
|
||||||
|
|
||||||
|
You are a NestJS performance optimization expert specializing in:
|
||||||
|
- Caching strategies (Redis, in-memory)
|
||||||
|
- Database query optimization
|
||||||
|
- API response compression
|
||||||
|
- Rate limiting and throttling
|
||||||
|
- Request/response optimization
|
||||||
|
- Memory management
|
||||||
|
- Load balancing and scaling
|
||||||
|
|
||||||
|
## Key Responsibilities:
|
||||||
|
- Implement efficient caching layers
|
||||||
|
- Optimize database queries
|
||||||
|
- Configure response compression
|
||||||
|
- Set up rate limiting
|
||||||
|
- Monitor and improve API performance
|
||||||
|
- Reduce response times
|
||||||
|
- Optimize resource usage
|
||||||
|
|
||||||
|
## Always Check First:
|
||||||
|
- Current caching implementation
|
||||||
|
- Database query patterns
|
||||||
|
- API response times
|
||||||
|
- Memory usage patterns
|
||||||
|
- Existing performance optimizations
|
||||||
|
|
||||||
|
## Caching with Redis:
|
||||||
|
|
||||||
|
### Installation:
|
||||||
|
```bash
|
||||||
|
npm install cache-manager cache-manager-redis-store
|
||||||
|
npm install @nestjs/cache-manager
|
||||||
|
npm install -D @types/cache-manager
|
||||||
|
```
|
||||||
|
|
||||||
|
### Redis Configuration:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// app.module.ts
|
||||||
|
import { CacheModule } from '@nestjs/cache-manager';
|
||||||
|
import { redisStore } from 'cache-manager-redis-store';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
CacheModule.registerAsync({
|
||||||
|
isGlobal: true,
|
||||||
|
imports: [ConfigModule],
|
||||||
|
useFactory: async (configService: ConfigService) => ({
|
||||||
|
store: redisStore,
|
||||||
|
host: configService.get('REDIS_HOST', 'localhost'),
|
||||||
|
port: configService.get('REDIS_PORT', 6379),
|
||||||
|
ttl: configService.get('CACHE_TTL', 300), // 5 minutes default
|
||||||
|
}),
|
||||||
|
inject: [ConfigService],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Cache in Service:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
|
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||||
|
import { Cache } from 'cache-manager';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ProductsService {
|
||||||
|
constructor(
|
||||||
|
@Inject(CACHE_MANAGER) private cacheManager: Cache,
|
||||||
|
private readonly productsRepository: ProductsRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async findAll(query: GetProductsDto) {
|
||||||
|
const cacheKey = `products:${JSON.stringify(query)}`;
|
||||||
|
|
||||||
|
// Try to get from cache
|
||||||
|
const cached = await this.cacheManager.get(cacheKey);
|
||||||
|
if (cached) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not in cache, fetch from database
|
||||||
|
const products = await this.productsRepository.findAll(query);
|
||||||
|
|
||||||
|
// Store in cache for 5 minutes
|
||||||
|
await this.cacheManager.set(cacheKey, products, 300);
|
||||||
|
|
||||||
|
return products;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(id: string) {
|
||||||
|
const cacheKey = `product:${id}`;
|
||||||
|
|
||||||
|
let product = await this.cacheManager.get(cacheKey);
|
||||||
|
|
||||||
|
if (!product) {
|
||||||
|
product = await this.productsRepository.findOne(id);
|
||||||
|
await this.cacheManager.set(cacheKey, product, 600); // 10 minutes
|
||||||
|
}
|
||||||
|
|
||||||
|
return product;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, updateProductDto: UpdateProductDto) {
|
||||||
|
const product = await this.productsRepository.update(id, updateProductDto);
|
||||||
|
|
||||||
|
// Invalidate cache
|
||||||
|
await this.cacheManager.del(`product:${id}`);
|
||||||
|
await this.cacheManager.reset(); // Clear all product list caches
|
||||||
|
|
||||||
|
return product;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cache Interceptor:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Use built-in cache interceptor
|
||||||
|
@Controller('products')
|
||||||
|
@UseInterceptors(CacheInterceptor)
|
||||||
|
export class ProductsController {
|
||||||
|
@Get()
|
||||||
|
@CacheTTL(300) // Cache for 5 minutes
|
||||||
|
async findAll() {
|
||||||
|
return this.productsService.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
@CacheTTL(600) // Cache for 10 minutes
|
||||||
|
async findOne(@Param('id') id: string) {
|
||||||
|
return this.productsService.findOne(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Cache Interceptor:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
NestInterceptor,
|
||||||
|
ExecutionContext,
|
||||||
|
CallHandler,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Observable, of } from 'rxjs';
|
||||||
|
import { tap } from 'rxjs/operators';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class HttpCacheInterceptor implements NestInterceptor {
|
||||||
|
constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}
|
||||||
|
|
||||||
|
async intercept(
|
||||||
|
context: ExecutionContext,
|
||||||
|
next: CallHandler,
|
||||||
|
): Promise<Observable<any>> {
|
||||||
|
const request = context.switchToHttp().getRequest();
|
||||||
|
const cacheKey = this.generateCacheKey(request);
|
||||||
|
|
||||||
|
const cachedResponse = await this.cacheManager.get(cacheKey);
|
||||||
|
|
||||||
|
if (cachedResponse) {
|
||||||
|
return of(cachedResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
return next.handle().pipe(
|
||||||
|
tap(async (response) => {
|
||||||
|
await this.cacheManager.set(cacheKey, response, 300);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateCacheKey(request: any): string {
|
||||||
|
return `${request.method}:${request.url}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Database Query Optimization:
|
||||||
|
|
||||||
|
### Use Query Builder for Complex Queries:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Bad - Multiple queries (N+1 problem)
|
||||||
|
async getProductsWithCategory() {
|
||||||
|
const products = await this.productRepository.find();
|
||||||
|
|
||||||
|
for (const product of products) {
|
||||||
|
product.category = await this.categoryRepository.findOne(
|
||||||
|
product.categoryId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return products;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Good - Single query with join
|
||||||
|
async getProductsWithCategory() {
|
||||||
|
return this.productRepository
|
||||||
|
.createQueryBuilder('product')
|
||||||
|
.leftJoinAndSelect('product.category', 'category')
|
||||||
|
.getMany();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pagination for Large Datasets:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async findAll(query: GetProductsDto) {
|
||||||
|
const { page = 1, limit = 20 } = query;
|
||||||
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
|
const [data, total] = await this.productRepository.findAndCount({
|
||||||
|
take: limit,
|
||||||
|
skip: skip,
|
||||||
|
order: { createdAt: 'DESC' },
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
meta: {
|
||||||
|
total,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
totalPages: Math.ceil(total / limit),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Select Only Needed Fields:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Bad - Fetches all fields
|
||||||
|
const products = await this.productRepository.find();
|
||||||
|
|
||||||
|
// Good - Select specific fields
|
||||||
|
const products = await this.productRepository.find({
|
||||||
|
select: ['id', 'name', 'price', 'imageUrl'],
|
||||||
|
});
|
||||||
|
|
||||||
|
// With query builder
|
||||||
|
const products = await this.productRepository
|
||||||
|
.createQueryBuilder('product')
|
||||||
|
.select(['product.id', 'product.name', 'product.price'])
|
||||||
|
.getMany();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Indexing:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Add indexes to frequently queried fields
|
||||||
|
@Entity('products')
|
||||||
|
export class Product {
|
||||||
|
@Column()
|
||||||
|
@Index() // Single column index
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
@Index() // Index for foreign key
|
||||||
|
categoryId: string;
|
||||||
|
|
||||||
|
// Composite index for multiple columns
|
||||||
|
@Index(['name', 'categoryId'])
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Eager vs Lazy Loading:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Eager loading (always loads relations)
|
||||||
|
@ManyToOne(() => Category, { eager: true })
|
||||||
|
category: Category;
|
||||||
|
|
||||||
|
// Lazy loading (load only when accessed)
|
||||||
|
@ManyToOne(() => Category)
|
||||||
|
category: Promise<Category>;
|
||||||
|
|
||||||
|
// Best practice: Load relations when needed
|
||||||
|
const product = await this.productRepository.findOne({
|
||||||
|
where: { id },
|
||||||
|
relations: ['category'], // Explicit relation loading
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response Compression:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install compression
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// main.ts
|
||||||
|
import * as compression from 'compression';
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
const app = await NestFactory.create(AppModule);
|
||||||
|
|
||||||
|
// Enable compression
|
||||||
|
app.use(compression());
|
||||||
|
|
||||||
|
await app.listen(3000);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rate Limiting:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @nestjs/throttler
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// app.module.ts
|
||||||
|
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
|
||||||
|
import { APP_GUARD } from '@nestjs/core';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
ThrottlerModule.forRoot({
|
||||||
|
ttl: 60, // Time window in seconds
|
||||||
|
limit: 10, // Max requests per ttl
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: APP_GUARD,
|
||||||
|
useClass: ThrottlerGuard,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
|
|
||||||
|
// Custom rate limiting per endpoint
|
||||||
|
@Controller('products')
|
||||||
|
export class ProductsController {
|
||||||
|
@Get()
|
||||||
|
@Throttle(100, 60) // 100 requests per minute
|
||||||
|
async findAll() {
|
||||||
|
return this.productsService.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@Throttle(10, 60) // 10 requests per minute for writes
|
||||||
|
async create(@Body() dto: CreateProductDto) {
|
||||||
|
return this.productsService.create(dto);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Request/Response Optimization:
|
||||||
|
|
||||||
|
### Streaming Large Responses:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { StreamableFile } from '@nestjs/common';
|
||||||
|
import { createReadStream } from 'fs';
|
||||||
|
|
||||||
|
@Get('export')
|
||||||
|
async exportProducts(): Promise<StreamableFile> {
|
||||||
|
const file = createReadStream('./products-export.csv');
|
||||||
|
|
||||||
|
return new StreamableFile(file, {
|
||||||
|
type: 'text/csv',
|
||||||
|
disposition: 'attachment; filename="products.csv"',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response Transformation:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Transform and exclude sensitive data
|
||||||
|
export class ProductResponseDto {
|
||||||
|
@Exclude()
|
||||||
|
internalId: string;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
@Transform(({ value }) => parseFloat(value).toFixed(2))
|
||||||
|
price: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In controller
|
||||||
|
@Get()
|
||||||
|
@UseInterceptors(ClassSerializerInterceptor)
|
||||||
|
async findAll(): Promise<ProductResponseDto[]> {
|
||||||
|
return this.productsService.findAll();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Connection Pooling:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Database connection pooling
|
||||||
|
TypeOrmModule.forRoot({
|
||||||
|
type: 'postgres',
|
||||||
|
// ... other config
|
||||||
|
extra: {
|
||||||
|
max: 20, // Maximum connections
|
||||||
|
min: 5, // Minimum connections
|
||||||
|
idle: 10000, // Idle timeout
|
||||||
|
acquire: 30000, // Acquire timeout
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
```
|
||||||
|
|
||||||
|
## Async Processing with Bull Queue:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @nestjs/bull bull
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// app.module.ts
|
||||||
|
import { BullModule } from '@nestjs/bull';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
BullModule.forRoot({
|
||||||
|
redis: {
|
||||||
|
host: 'localhost',
|
||||||
|
port: 6379,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
BullModule.registerQueue({
|
||||||
|
name: 'products-sync',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
|
|
||||||
|
// products.service.ts
|
||||||
|
@Injectable()
|
||||||
|
export class ProductsService {
|
||||||
|
constructor(
|
||||||
|
@InjectQueue('products-sync') private productsQueue: Queue,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async syncProducts() {
|
||||||
|
// Add job to queue instead of processing immediately
|
||||||
|
await this.productsQueue.add('sync-all', {
|
||||||
|
timestamp: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return { message: 'Sync initiated' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// products.processor.ts
|
||||||
|
import { Processor, Process } from '@nestjs/bull';
|
||||||
|
import { Job } from 'bull';
|
||||||
|
|
||||||
|
@Processor('products-sync')
|
||||||
|
export class ProductsProcessor {
|
||||||
|
@Process('sync-all')
|
||||||
|
async handleSync(job: Job) {
|
||||||
|
// Heavy processing here
|
||||||
|
const products = await this.fetchFromExternalAPI();
|
||||||
|
await this.saveToDatabase(products);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Monitoring:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Logging interceptor with timing
|
||||||
|
@Injectable()
|
||||||
|
export class LoggingInterceptor implements NestInterceptor {
|
||||||
|
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||||
|
const request = context.switchToHttp().getRequest();
|
||||||
|
const { method, url } = request;
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
return next.handle().pipe(
|
||||||
|
tap(() => {
|
||||||
|
const responseTime = Date.now() - startTime;
|
||||||
|
console.log(`${method} ${url} - ${responseTime}ms`);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Load Testing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install autocannon for load testing
|
||||||
|
npm install -D autocannon
|
||||||
|
|
||||||
|
# Run load test
|
||||||
|
npx autocannon -c 100 -d 30 http://localhost:3000/api/products
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Best Practices:
|
||||||
|
|
||||||
|
### 1. Database Optimization:
|
||||||
|
- Use indexes on frequently queried columns
|
||||||
|
- Implement pagination for large datasets
|
||||||
|
- Use query builders for complex queries
|
||||||
|
- Avoid N+1 query problems
|
||||||
|
- Use connection pooling
|
||||||
|
|
||||||
|
### 2. Caching:
|
||||||
|
- Cache frequently accessed data
|
||||||
|
- Set appropriate TTL values
|
||||||
|
- Invalidate cache on updates
|
||||||
|
- Use Redis for distributed caching
|
||||||
|
- Cache at different layers (API, DB, etc.)
|
||||||
|
|
||||||
|
### 3. API Optimization:
|
||||||
|
- Enable response compression
|
||||||
|
- Implement rate limiting
|
||||||
|
- Use streaming for large responses
|
||||||
|
- Return only necessary fields
|
||||||
|
- Implement proper pagination
|
||||||
|
|
||||||
|
### 4. Code Optimization:
|
||||||
|
- Use async/await properly
|
||||||
|
- Avoid blocking operations
|
||||||
|
- Process heavy tasks in queues
|
||||||
|
- Use lazy loading when appropriate
|
||||||
|
- Implement proper error handling
|
||||||
|
|
||||||
|
### 5. Monitoring:
|
||||||
|
- Log response times
|
||||||
|
- Monitor database query performance
|
||||||
|
- Track cache hit rates
|
||||||
|
- Monitor memory usage
|
||||||
|
- Set up alerts for slow endpoints
|
||||||
|
|
||||||
|
### 6. Scalability:
|
||||||
|
- Use horizontal scaling
|
||||||
|
- Implement load balancing
|
||||||
|
- Use queues for async processing
|
||||||
|
- Separate read/write databases
|
||||||
|
- Use CDN for static assets
|
||||||
|
|
||||||
|
## Common Performance Pitfalls:
|
||||||
|
|
||||||
|
❌ **Don't:**
|
||||||
|
- Fetch all data without pagination
|
||||||
|
- Load unnecessary relations
|
||||||
|
- Block the event loop with heavy computations
|
||||||
|
- Forget to close database connections
|
||||||
|
- Cache everything without expiration
|
||||||
|
|
||||||
|
✅ **Do:**
|
||||||
|
- Implement pagination everywhere
|
||||||
|
- Load relations selectively
|
||||||
|
- Use queues for heavy processing
|
||||||
|
- Use connection pooling
|
||||||
|
- Set appropriate cache TTLs
|
||||||
56
.gitignore
vendored
Normal file
56
.gitignore
vendored
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# compiled output
|
||||||
|
/dist
|
||||||
|
/node_modules
|
||||||
|
/build
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
/coverage
|
||||||
|
/.nyc_output
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
/.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# IDE - VSCode
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# temp directory
|
||||||
|
.temp
|
||||||
|
.tmp
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
4
.prettierrc
Normal file
4
.prettierrc
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
||||||
98
README.md
Normal file
98
README.md
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
<p align="center">
|
||||||
|
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
|
||||||
|
[circleci-url]: https://circleci.com/gh/nestjs/nest
|
||||||
|
|
||||||
|
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
|
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
|
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
|
||||||
|
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
|
||||||
|
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
|
||||||
|
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
|
||||||
|
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
|
||||||
|
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
|
||||||
|
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
|
||||||
|
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
|
||||||
|
</p>
|
||||||
|
<!--[](https://opencollective.com/nest#backer)
|
||||||
|
[](https://opencollective.com/nest#sponsor)-->
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
|
||||||
|
|
||||||
|
## Project setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Compile and run the project
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# development
|
||||||
|
$ npm run start
|
||||||
|
|
||||||
|
# watch mode
|
||||||
|
$ npm run start:dev
|
||||||
|
|
||||||
|
# production mode
|
||||||
|
$ npm run start:prod
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# unit tests
|
||||||
|
$ npm run test
|
||||||
|
|
||||||
|
# e2e tests
|
||||||
|
$ npm run test:e2e
|
||||||
|
|
||||||
|
# test coverage
|
||||||
|
$ npm run test:cov
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information.
|
||||||
|
|
||||||
|
If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ npm install -g @nestjs/mau
|
||||||
|
$ mau deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure.
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
Check out a few resources that may come in handy when working with NestJS:
|
||||||
|
|
||||||
|
- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework.
|
||||||
|
- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
|
||||||
|
- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
|
||||||
|
- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks.
|
||||||
|
- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
|
||||||
|
- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
|
||||||
|
- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
|
||||||
|
- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
|
||||||
|
|
||||||
|
## Stay in touch
|
||||||
|
|
||||||
|
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
|
||||||
|
- Website - [https://nestjs.com](https://nestjs.com/)
|
||||||
|
- Twitter - [@nestframework](https://twitter.com/nestframework)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).
|
||||||
910
claude.md
Normal file
910
claude.md
Normal file
@@ -0,0 +1,910 @@
|
|||||||
|
# NestJS Retail POS Backend API Expert Guidelines
|
||||||
|
|
||||||
|
## 🎯 Backend Overview
|
||||||
|
A robust NestJS-based REST API backend for the Retail POS Flutter application, providing product management, category organization, transaction processing, and user authentication with offline-sync capabilities.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤖 SUBAGENT DELEGATION SYSTEM 🤖
|
||||||
|
**CRITICAL: BE PROACTIVE WITH SUBAGENTS! YOU HAVE SPECIALIZED EXPERTS AVAILABLE!**
|
||||||
|
|
||||||
|
### 🚨 DELEGATION MINDSET
|
||||||
|
**Instead of thinking "I'll handle this myself"** **Think: "Which specialist is BEST suited for this task?"**
|
||||||
|
|
||||||
|
### 📋 AVAILABLE SPECIALISTS
|
||||||
|
You have access to these expert subagents - USE THEM PROACTIVELY:
|
||||||
|
|
||||||
|
#### 🌐 **nestjs-api-expert**
|
||||||
|
- **MUST BE USED for**: Controllers, DTOs, REST endpoints, request/response handling, validation
|
||||||
|
- **Triggers**: "API endpoint", "controller", "DTO", "validation", "REST", "route"
|
||||||
|
|
||||||
|
#### 🗄️ **nestjs-database-expert**
|
||||||
|
- **MUST BE USED for**: TypeORM entities, database schema, migrations, queries, relationships
|
||||||
|
- **Triggers**: "database", "entity", "migration", "query", "TypeORM", "Prisma", "PostgreSQL"
|
||||||
|
|
||||||
|
#### 🏗️ **nestjs-architecture-expert**
|
||||||
|
- **MUST BE USED for**: Module structure, dependency injection, services, clean architecture
|
||||||
|
- **Triggers**: "architecture", "module", "service", "dependency injection", "structure"
|
||||||
|
|
||||||
|
#### 🔐 **nestjs-auth-expert**
|
||||||
|
- **MUST BE USED for**: JWT authentication, guards, security, authorization, user management
|
||||||
|
- **Triggers**: "auth", "JWT", "guard", "security", "login", "authentication"
|
||||||
|
|
||||||
|
#### ⚡ **nestjs-performance-expert**
|
||||||
|
- **MUST BE USED for**: Caching, query optimization, rate limiting, performance tuning
|
||||||
|
- **Triggers**: "performance", "cache", "Redis", "optimization", "slow", "rate limit"
|
||||||
|
|
||||||
|
### 🎯 DELEGATION STRATEGY
|
||||||
|
**BEFORE starting ANY task, ASK YOURSELF:**
|
||||||
|
1. "Which of my specialists could handle this better?"
|
||||||
|
2. "Should I break this into parts for different specialists?"
|
||||||
|
3. "Would a specialist complete this faster and better?"
|
||||||
|
|
||||||
|
### 💼 WORK BALANCE RECOMMENDATION:
|
||||||
|
- **Simple Tasks (20%)**: Handle independently - quick fixes, minor updates
|
||||||
|
- **Complex Tasks (80%)**: Delegate to specialists for expert-level results
|
||||||
|
|
||||||
|
### 🔧 HOW TO DELEGATE
|
||||||
|
```
|
||||||
|
# Explicit delegation examples:
|
||||||
|
> Use the nestjs-api-expert to create the products CRUD endpoints
|
||||||
|
> Have the nestjs-database-expert design the transaction entity relationships
|
||||||
|
> Ask the nestjs-architecture-expert to structure the sync module
|
||||||
|
> Use the nestjs-auth-expert to implement JWT authentication
|
||||||
|
> Have the nestjs-performance-expert optimize product queries with caching
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## NestJS Best Practices
|
||||||
|
- Use TypeScript with strict mode enabled
|
||||||
|
- Implement modular architecture with feature-based organization
|
||||||
|
- Follow dependency injection patterns with NestJS IoC
|
||||||
|
- Use DTOs with class-validator for request validation
|
||||||
|
- Implement proper error handling with exception filters
|
||||||
|
- Use TypeORM for database operations
|
||||||
|
- Follow RESTful API design principles
|
||||||
|
- Implement comprehensive API documentation with Swagger
|
||||||
|
- Use environment variables for configuration
|
||||||
|
- Write unit and e2e tests for all endpoints
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
common/
|
||||||
|
decorators/
|
||||||
|
current-user.decorator.ts
|
||||||
|
roles.decorator.ts
|
||||||
|
public.decorator.ts
|
||||||
|
dto/
|
||||||
|
pagination.dto.ts
|
||||||
|
api-response.dto.ts
|
||||||
|
filters/
|
||||||
|
http-exception.filter.ts
|
||||||
|
all-exceptions.filter.ts
|
||||||
|
guards/
|
||||||
|
jwt-auth.guard.ts
|
||||||
|
roles.guard.ts
|
||||||
|
interceptors/
|
||||||
|
logging.interceptor.ts
|
||||||
|
transform.interceptor.ts
|
||||||
|
cache.interceptor.ts
|
||||||
|
pipes/
|
||||||
|
validation.pipe.ts
|
||||||
|
interfaces/
|
||||||
|
pagination.interface.ts
|
||||||
|
utils/
|
||||||
|
helpers.ts
|
||||||
|
formatters.ts
|
||||||
|
|
||||||
|
config/
|
||||||
|
app.config.ts
|
||||||
|
database.config.ts
|
||||||
|
jwt.config.ts
|
||||||
|
redis.config.ts
|
||||||
|
|
||||||
|
database/
|
||||||
|
migrations/
|
||||||
|
1234567890-CreateProductsTable.ts
|
||||||
|
1234567891-CreateCategoriesTable.ts
|
||||||
|
1234567892-CreateTransactionsTable.ts
|
||||||
|
seeds/
|
||||||
|
categories.seed.ts
|
||||||
|
products.seed.ts
|
||||||
|
data-source.ts
|
||||||
|
|
||||||
|
modules/
|
||||||
|
products/
|
||||||
|
dto/
|
||||||
|
create-product.dto.ts
|
||||||
|
update-product.dto.ts
|
||||||
|
get-products.dto.ts
|
||||||
|
product-response.dto.ts
|
||||||
|
entities/
|
||||||
|
product.entity.ts
|
||||||
|
products.controller.ts
|
||||||
|
products.service.ts
|
||||||
|
products.repository.ts
|
||||||
|
products.module.ts
|
||||||
|
|
||||||
|
categories/
|
||||||
|
dto/
|
||||||
|
create-category.dto.ts
|
||||||
|
update-category.dto.ts
|
||||||
|
category-response.dto.ts
|
||||||
|
entities/
|
||||||
|
category.entity.ts
|
||||||
|
categories.controller.ts
|
||||||
|
categories.service.ts
|
||||||
|
categories.repository.ts
|
||||||
|
categories.module.ts
|
||||||
|
|
||||||
|
transactions/
|
||||||
|
dto/
|
||||||
|
create-transaction.dto.ts
|
||||||
|
get-transactions.dto.ts
|
||||||
|
transaction-response.dto.ts
|
||||||
|
entities/
|
||||||
|
transaction.entity.ts
|
||||||
|
transaction-item.entity.ts
|
||||||
|
transactions.controller.ts
|
||||||
|
transactions.service.ts
|
||||||
|
transactions.repository.ts
|
||||||
|
transactions.module.ts
|
||||||
|
|
||||||
|
auth/
|
||||||
|
dto/
|
||||||
|
login.dto.ts
|
||||||
|
register.dto.ts
|
||||||
|
strategies/
|
||||||
|
jwt.strategy.ts
|
||||||
|
local.strategy.ts
|
||||||
|
guards/
|
||||||
|
jwt-auth.guard.ts
|
||||||
|
local-auth.guard.ts
|
||||||
|
auth.controller.ts
|
||||||
|
auth.service.ts
|
||||||
|
auth.module.ts
|
||||||
|
|
||||||
|
users/
|
||||||
|
dto/
|
||||||
|
create-user.dto.ts
|
||||||
|
update-user.dto.ts
|
||||||
|
entities/
|
||||||
|
user.entity.ts
|
||||||
|
users.controller.ts
|
||||||
|
users.service.ts
|
||||||
|
users.repository.ts
|
||||||
|
users.module.ts
|
||||||
|
|
||||||
|
sync/
|
||||||
|
dto/
|
||||||
|
sync-request.dto.ts
|
||||||
|
sync-response.dto.ts
|
||||||
|
sync.controller.ts
|
||||||
|
sync.service.ts
|
||||||
|
sync.module.ts
|
||||||
|
|
||||||
|
app.module.ts
|
||||||
|
main.ts
|
||||||
|
|
||||||
|
test/
|
||||||
|
unit/
|
||||||
|
products/
|
||||||
|
products.service.spec.ts
|
||||||
|
products.controller.spec.ts
|
||||||
|
categories/
|
||||||
|
transactions/
|
||||||
|
e2e/
|
||||||
|
products.e2e-spec.ts
|
||||||
|
categories.e2e-spec.ts
|
||||||
|
auth.e2e-spec.ts
|
||||||
|
|
||||||
|
.env
|
||||||
|
.env.example
|
||||||
|
package.json
|
||||||
|
tsconfig.json
|
||||||
|
ormconfig.json
|
||||||
|
docker-compose.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Backend Context - Retail POS API
|
||||||
|
|
||||||
|
## About This Backend
|
||||||
|
A comprehensive NestJS REST API backend that powers the Flutter Retail POS mobile application. Provides robust endpoints for product management, category organization, transaction processing, and user authentication, with offline-sync capabilities and real-time data synchronization.
|
||||||
|
|
||||||
|
## Target Users
|
||||||
|
- **Flutter Mobile App**: Primary consumer of the API
|
||||||
|
- **Admin Dashboard**: Future web-based management interface (optional)
|
||||||
|
- **Third-party Integrations**: Payment gateways, inventory systems
|
||||||
|
|
||||||
|
## Core Features
|
||||||
|
|
||||||
|
### 📦 Product Management
|
||||||
|
**Endpoints**:
|
||||||
|
- `GET /api/products` - List all products with pagination and filtering
|
||||||
|
- `GET /api/products/:id` - Get single product details
|
||||||
|
- `POST /api/products` - Create new product
|
||||||
|
- `PUT /api/products/:id` - Update product
|
||||||
|
- `DELETE /api/products/:id` - Delete product
|
||||||
|
- `GET /api/products/category/:categoryId` - Products by category
|
||||||
|
- `GET /api/products/search` - Search products
|
||||||
|
|
||||||
|
**Features**:
|
||||||
|
- Full CRUD operations
|
||||||
|
- Advanced filtering (category, price range, availability)
|
||||||
|
- Search by name and description
|
||||||
|
- Stock management
|
||||||
|
- Image URL storage
|
||||||
|
- Pagination and sorting
|
||||||
|
- Soft delete support (optional)
|
||||||
|
|
||||||
|
**Business Logic**:
|
||||||
|
- Validate category exists before creating product
|
||||||
|
- Check stock availability
|
||||||
|
- Update product count in category
|
||||||
|
- Track inventory changes
|
||||||
|
|
||||||
|
### 📁 Category Management
|
||||||
|
**Endpoints**:
|
||||||
|
- `GET /api/categories` - List all categories
|
||||||
|
- `GET /api/categories/:id` - Get single category
|
||||||
|
- `POST /api/categories` - Create new category
|
||||||
|
- `PUT /api/categories/:id` - Update category
|
||||||
|
- `DELETE /api/categories/:id` - Delete category
|
||||||
|
- `GET /api/categories/:id/products` - Products in category
|
||||||
|
|
||||||
|
**Features**:
|
||||||
|
- CRUD operations
|
||||||
|
- Product count tracking
|
||||||
|
- Icon/image support
|
||||||
|
- Color coding
|
||||||
|
- Cascade delete (optional)
|
||||||
|
|
||||||
|
**Business Logic**:
|
||||||
|
- Prevent deletion if category has products (or cascade)
|
||||||
|
- Update product count when products added/removed
|
||||||
|
- Unique category names
|
||||||
|
|
||||||
|
### 💰 Transaction Management
|
||||||
|
**Endpoints**:
|
||||||
|
- `GET /api/transactions` - List transactions with pagination
|
||||||
|
- `GET /api/transactions/:id` - Get transaction details
|
||||||
|
- `POST /api/transactions` - Create new transaction
|
||||||
|
- `GET /api/transactions/stats` - Transaction statistics
|
||||||
|
- `GET /api/transactions/daily` - Daily sales report
|
||||||
|
|
||||||
|
**Features**:
|
||||||
|
- Complete transaction logging
|
||||||
|
- Transaction items tracking
|
||||||
|
- Subtotal, tax, discount calculation
|
||||||
|
- Payment method tracking
|
||||||
|
- Transaction history
|
||||||
|
- Sales reporting
|
||||||
|
|
||||||
|
**Business Logic**:
|
||||||
|
- Validate stock availability
|
||||||
|
- Update product stock on transaction
|
||||||
|
- Calculate totals automatically
|
||||||
|
- Atomic transaction creation (all or nothing)
|
||||||
|
- Track product at transaction time (price snapshot)
|
||||||
|
|
||||||
|
### 🔐 Authentication & Authorization
|
||||||
|
**Endpoints**:
|
||||||
|
- `POST /api/auth/register` - Register new user
|
||||||
|
- `POST /api/auth/login` - Login user
|
||||||
|
- `GET /api/auth/profile` - Get current user profile
|
||||||
|
- `POST /api/auth/refresh` - Refresh access token
|
||||||
|
|
||||||
|
**Features**:
|
||||||
|
- JWT-based authentication
|
||||||
|
- Password hashing with bcrypt
|
||||||
|
- Role-based access control (RBAC)
|
||||||
|
- Token refresh mechanism
|
||||||
|
- Secure password policies
|
||||||
|
|
||||||
|
**Roles**:
|
||||||
|
- **Admin**: Full access to all endpoints
|
||||||
|
- **Manager**: Product and category management
|
||||||
|
- **Cashier**: Transaction processing only
|
||||||
|
- **User**: Read-only access (optional)
|
||||||
|
|
||||||
|
### 🔄 Sync Management
|
||||||
|
**Endpoints**:
|
||||||
|
- `POST /api/sync/products` - Sync products to mobile
|
||||||
|
- `POST /api/sync/categories` - Sync categories to mobile
|
||||||
|
- `GET /api/sync/status` - Get last sync status
|
||||||
|
- `GET /api/sync/changes` - Get changes since last sync
|
||||||
|
|
||||||
|
**Features**:
|
||||||
|
- Incremental sync support
|
||||||
|
- Last modified timestamp tracking
|
||||||
|
- Change detection
|
||||||
|
- Bulk data transfer
|
||||||
|
- Sync conflict resolution
|
||||||
|
|
||||||
|
## Technical Stack
|
||||||
|
|
||||||
|
### Core Technologies
|
||||||
|
- **Framework**: NestJS 10.x
|
||||||
|
- **Language**: TypeScript 5.x
|
||||||
|
- **Database**: PostgreSQL 15.x
|
||||||
|
- **ORM**: TypeORM 0.3.x (or Prisma as alternative)
|
||||||
|
- **Cache**: Redis 7.x
|
||||||
|
- **Authentication**: JWT (jsonwebtoken)
|
||||||
|
- **Validation**: class-validator, class-transformer
|
||||||
|
- **Documentation**: Swagger/OpenAPI
|
||||||
|
|
||||||
|
### Key Dependencies
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"@nestjs/common": "^10.0.0",
|
||||||
|
"@nestjs/core": "^10.0.0",
|
||||||
|
"@nestjs/platform-express": "^10.0.0",
|
||||||
|
"@nestjs/typeorm": "^10.0.0",
|
||||||
|
"@nestjs/jwt": "^10.0.0",
|
||||||
|
"@nestjs/passport": "^10.0.0",
|
||||||
|
"@nestjs/swagger": "^7.0.0",
|
||||||
|
"@nestjs/config": "^3.0.0",
|
||||||
|
"@nestjs/cache-manager": "^2.0.0",
|
||||||
|
"@nestjs/throttler": "^5.0.0",
|
||||||
|
"typeorm": "^0.3.17",
|
||||||
|
"pg": "^8.11.0",
|
||||||
|
"bcrypt": "^5.1.0",
|
||||||
|
"passport-jwt": "^4.0.1",
|
||||||
|
"class-validator": "^0.14.0",
|
||||||
|
"class-transformer": "^0.5.1",
|
||||||
|
"cache-manager": "^5.2.0",
|
||||||
|
"cache-manager-redis-store": "^3.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Database Schema
|
||||||
|
|
||||||
|
### Products Table
|
||||||
|
```sql
|
||||||
|
CREATE TABLE products (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
price DECIMAL(10, 2) NOT NULL,
|
||||||
|
image_url VARCHAR(500),
|
||||||
|
category_id UUID REFERENCES categories(id) ON DELETE CASCADE,
|
||||||
|
stock_quantity INTEGER DEFAULT 0,
|
||||||
|
is_available BOOLEAN DEFAULT true,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
INDEX idx_products_name (name),
|
||||||
|
INDEX idx_products_category (category_id),
|
||||||
|
INDEX idx_products_name_category (name, category_id)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Categories Table
|
||||||
|
```sql
|
||||||
|
CREATE TABLE categories (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
name VARCHAR(255) UNIQUE NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
icon_path VARCHAR(255),
|
||||||
|
color VARCHAR(50),
|
||||||
|
product_count INTEGER DEFAULT 0,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
INDEX idx_categories_name (name)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Transactions Table
|
||||||
|
```sql
|
||||||
|
CREATE TABLE transactions (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
subtotal DECIMAL(10, 2) NOT NULL,
|
||||||
|
tax DECIMAL(10, 2) DEFAULT 0,
|
||||||
|
discount DECIMAL(10, 2) DEFAULT 0,
|
||||||
|
total DECIMAL(10, 2) NOT NULL,
|
||||||
|
payment_method VARCHAR(50) NOT NULL,
|
||||||
|
completed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
INDEX idx_transactions_date (completed_at)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Transaction Items Table
|
||||||
|
```sql
|
||||||
|
CREATE TABLE transaction_items (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
transaction_id UUID REFERENCES transactions(id) ON DELETE CASCADE,
|
||||||
|
product_id UUID REFERENCES products(id),
|
||||||
|
product_name VARCHAR(255) NOT NULL,
|
||||||
|
price DECIMAL(10, 2) NOT NULL,
|
||||||
|
quantity INTEGER NOT NULL,
|
||||||
|
line_total DECIMAL(10, 2) NOT NULL,
|
||||||
|
|
||||||
|
INDEX idx_transaction_items_transaction (transaction_id),
|
||||||
|
INDEX idx_transaction_items_product (product_id)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Users Table
|
||||||
|
```sql
|
||||||
|
CREATE TABLE users (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
email VARCHAR(255) UNIQUE NOT NULL,
|
||||||
|
password VARCHAR(255) NOT NULL,
|
||||||
|
roles TEXT[] DEFAULT ARRAY['user'],
|
||||||
|
is_active BOOLEAN DEFAULT true,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
INDEX idx_users_email (email)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Response Format
|
||||||
|
|
||||||
|
### Success Response
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
// Response data
|
||||||
|
},
|
||||||
|
"message": "Operation successful"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Paginated Response
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": [
|
||||||
|
// Array of items
|
||||||
|
],
|
||||||
|
"meta": {
|
||||||
|
"page": 1,
|
||||||
|
"limit": 20,
|
||||||
|
"total": 100,
|
||||||
|
"totalPages": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Response
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": {
|
||||||
|
"statusCode": 400,
|
||||||
|
"message": "Validation failed",
|
||||||
|
"details": [
|
||||||
|
"name must be a string",
|
||||||
|
"price must be a positive number"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"timestamp": "2025-01-15T10:30:00.000Z",
|
||||||
|
"path": "/api/products"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key API Endpoints
|
||||||
|
|
||||||
|
### Products API
|
||||||
|
|
||||||
|
#### List Products
|
||||||
|
```http
|
||||||
|
GET /api/products?page=1&limit=20&categoryId=uuid&search=laptop&minPrice=100&maxPrice=1000
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": "uuid",
|
||||||
|
"name": "Laptop",
|
||||||
|
"description": "Gaming laptop",
|
||||||
|
"price": 999.99,
|
||||||
|
"imageUrl": "https://...",
|
||||||
|
"categoryId": "uuid",
|
||||||
|
"stockQuantity": 50,
|
||||||
|
"isAvailable": true,
|
||||||
|
"category": {
|
||||||
|
"id": "uuid",
|
||||||
|
"name": "Electronics"
|
||||||
|
},
|
||||||
|
"createdAt": "2025-01-15T10:00:00.000Z",
|
||||||
|
"updatedAt": "2025-01-15T10:00:00.000Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"meta": {
|
||||||
|
"page": 1,
|
||||||
|
"limit": 20,
|
||||||
|
"total": 100,
|
||||||
|
"totalPages": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Create Product
|
||||||
|
```http
|
||||||
|
POST /api/products
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Laptop",
|
||||||
|
"description": "Gaming laptop",
|
||||||
|
"price": 999.99,
|
||||||
|
"imageUrl": "https://...",
|
||||||
|
"categoryId": "uuid",
|
||||||
|
"stockQuantity": 50,
|
||||||
|
"isAvailable": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Categories API
|
||||||
|
|
||||||
|
#### List Categories
|
||||||
|
```http
|
||||||
|
GET /api/categories
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": "uuid",
|
||||||
|
"name": "Electronics",
|
||||||
|
"description": "Electronic devices",
|
||||||
|
"iconPath": "/icons/electronics.png",
|
||||||
|
"color": "#FF5722",
|
||||||
|
"productCount": 150,
|
||||||
|
"createdAt": "2025-01-15T10:00:00.000Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Transactions API
|
||||||
|
|
||||||
|
#### Create Transaction
|
||||||
|
```http
|
||||||
|
POST /api/transactions
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"productId": "uuid",
|
||||||
|
"quantity": 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paymentMethod": "cash",
|
||||||
|
"discount": 10.00
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"id": "uuid",
|
||||||
|
"subtotal": 1999.98,
|
||||||
|
"tax": 199.99,
|
||||||
|
"discount": 10.00,
|
||||||
|
"total": 2189.97,
|
||||||
|
"paymentMethod": "cash",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "uuid",
|
||||||
|
"productId": "uuid",
|
||||||
|
"productName": "Laptop",
|
||||||
|
"price": 999.99,
|
||||||
|
"quantity": 2,
|
||||||
|
"lineTotal": 1999.98
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"completedAt": "2025-01-15T10:30:00.000Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Authentication API
|
||||||
|
|
||||||
|
#### Login
|
||||||
|
```http
|
||||||
|
POST /api/auth/login
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "user@example.com",
|
||||||
|
"password": "Password123!"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||||
|
"user": {
|
||||||
|
"id": "uuid",
|
||||||
|
"email": "user@example.com",
|
||||||
|
"name": "John Doe",
|
||||||
|
"roles": ["admin"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Caching Strategy
|
||||||
|
|
||||||
|
### Cache Layers
|
||||||
|
1. **Product List Cache**: 5 minutes TTL
|
||||||
|
2. **Single Product Cache**: 10 minutes TTL
|
||||||
|
3. **Category List Cache**: 15 minutes TTL
|
||||||
|
4. **Transaction Stats Cache**: 1 hour TTL
|
||||||
|
|
||||||
|
### Cache Invalidation
|
||||||
|
- Invalidate on CREATE, UPDATE, DELETE operations
|
||||||
|
- Smart invalidation: Only clear related caches
|
||||||
|
- Use cache tags for grouped invalidation
|
||||||
|
|
||||||
|
### Redis Configuration
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
host: process.env.REDIS_HOST,
|
||||||
|
port: 6379,
|
||||||
|
ttl: 300, // Default 5 minutes
|
||||||
|
max: 1000 // Max items in cache
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Optimization
|
||||||
|
|
||||||
|
### Database Optimization
|
||||||
|
- Index frequently queried columns
|
||||||
|
- Use query builders for complex queries
|
||||||
|
- Implement connection pooling (max: 20, min: 5)
|
||||||
|
- Paginate all list endpoints
|
||||||
|
- Eager load only necessary relations
|
||||||
|
|
||||||
|
### API Optimization
|
||||||
|
- Enable response compression (gzip)
|
||||||
|
- Implement rate limiting (100 req/min per IP)
|
||||||
|
- Cache frequently accessed data
|
||||||
|
- Use streaming for large responses
|
||||||
|
- Implement query result caching
|
||||||
|
|
||||||
|
### Monitoring
|
||||||
|
- Log all API requests with response times
|
||||||
|
- Track database query performance
|
||||||
|
- Monitor cache hit/miss rates
|
||||||
|
- Alert on slow endpoints (>1s)
|
||||||
|
- Track error rates
|
||||||
|
|
||||||
|
## Security Implementation
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
- JWT with 1-day expiration
|
||||||
|
- Refresh tokens with 7-day expiration
|
||||||
|
- bcrypt password hashing (10 rounds)
|
||||||
|
- Rate limiting on auth endpoints (5 req/min)
|
||||||
|
|
||||||
|
### Authorization
|
||||||
|
- Role-based access control (RBAC)
|
||||||
|
- Protected routes with guards
|
||||||
|
- Public endpoints clearly marked
|
||||||
|
- Admin-only endpoints separated
|
||||||
|
|
||||||
|
### API Security
|
||||||
|
- CORS enabled with whitelist
|
||||||
|
- Helmet for security headers
|
||||||
|
- Input validation on all endpoints
|
||||||
|
- SQL injection prevention (TypeORM)
|
||||||
|
- XSS prevention
|
||||||
|
- HTTPS only in production
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
```bash
|
||||||
|
# Application
|
||||||
|
NODE_ENV=development
|
||||||
|
PORT=3000
|
||||||
|
API_PREFIX=api
|
||||||
|
|
||||||
|
# Database
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_USERNAME=postgres
|
||||||
|
DB_PASSWORD=postgres
|
||||||
|
DB_DATABASE=retail_pos
|
||||||
|
|
||||||
|
# JWT
|
||||||
|
JWT_SECRET=your-super-secret-key-change-in-production
|
||||||
|
JWT_EXPIRES_IN=1d
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
REDIS_HOST=localhost
|
||||||
|
REDIS_PORT=6379
|
||||||
|
CACHE_TTL=300
|
||||||
|
|
||||||
|
# CORS
|
||||||
|
CORS_ORIGIN=http://localhost:3000,capacitor://localhost
|
||||||
|
|
||||||
|
# Rate Limiting
|
||||||
|
THROTTLE_TTL=60
|
||||||
|
THROTTLE_LIMIT=100
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
### Unit Tests
|
||||||
|
- Test all service methods
|
||||||
|
- Test business logic
|
||||||
|
- Test error handling
|
||||||
|
- Mock all dependencies
|
||||||
|
- Aim for 80%+ coverage
|
||||||
|
|
||||||
|
### Integration Tests
|
||||||
|
- Test database operations
|
||||||
|
- Test repository methods
|
||||||
|
- Test data transformations
|
||||||
|
|
||||||
|
### E2E Tests
|
||||||
|
- Test complete API flows
|
||||||
|
- Test authentication flows
|
||||||
|
- Test CRUD operations
|
||||||
|
- Test error scenarios
|
||||||
|
- Test edge cases
|
||||||
|
|
||||||
|
### Test Structure
|
||||||
|
```typescript
|
||||||
|
describe('ProductsService', () => {
|
||||||
|
let service: ProductsService;
|
||||||
|
let repository: MockType<ProductsRepository>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
ProductsService,
|
||||||
|
{
|
||||||
|
provide: ProductsRepository,
|
||||||
|
useFactory: mockRepository,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get(ProductsService);
|
||||||
|
repository = module.get(ProductsRepository);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a product', async () => {
|
||||||
|
const dto = { name: 'Test', price: 10 };
|
||||||
|
repository.create.mockResolvedValue(dto);
|
||||||
|
|
||||||
|
const result = await service.create(dto);
|
||||||
|
|
||||||
|
expect(result).toEqual(dto);
|
||||||
|
expect(repository.create).toHaveBeenCalledWith(dto);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
### Docker Setup
|
||||||
|
```dockerfile
|
||||||
|
FROM node:18-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci --only=production
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD ["node", "dist/main"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
```yaml
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
api:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
- redis
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
image: postgres:15-alpine
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: retail_pos
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
volumes:
|
||||||
|
- postgres-data:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres-data:
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Setup database
|
||||||
|
npm run migration:run
|
||||||
|
npm run seed:run
|
||||||
|
|
||||||
|
# Start development server
|
||||||
|
npm run start:dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development Commands
|
||||||
|
```bash
|
||||||
|
# Run tests
|
||||||
|
npm run test
|
||||||
|
npm run test:e2e
|
||||||
|
npm run test:cov
|
||||||
|
|
||||||
|
# Generate migration
|
||||||
|
npm run migration:generate -- -n MigrationName
|
||||||
|
|
||||||
|
# Run migration
|
||||||
|
npm run migration:run
|
||||||
|
|
||||||
|
# Revert migration
|
||||||
|
npm run migration:revert
|
||||||
|
|
||||||
|
# Seed database
|
||||||
|
npm run seed:run
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Documentation
|
||||||
|
- **Swagger UI**: http://localhost:3000/api/docs
|
||||||
|
- **JSON**: http://localhost:3000/api/docs-json
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Remember: ALWAYS DELEGATE TO SPECIALISTS FOR BETTER RESULTS!
|
||||||
|
|
||||||
|
When working on this NestJS backend:
|
||||||
|
- **API/Endpoints** → nestjs-api-expert
|
||||||
|
- **Database/Entities** → nestjs-database-expert
|
||||||
|
- **Architecture** → nestjs-architecture-expert
|
||||||
|
- **Auth/Security** → nestjs-auth-expert
|
||||||
|
- **Performance** → nestjs-performance-expert
|
||||||
|
|
||||||
|
**Think delegation first, implementation second!**
|
||||||
34
eslint.config.mjs
Normal file
34
eslint.config.mjs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
// @ts-check
|
||||||
|
import eslint from '@eslint/js';
|
||||||
|
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
|
||||||
|
import globals from 'globals';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{
|
||||||
|
ignores: ['eslint.config.mjs'],
|
||||||
|
},
|
||||||
|
eslint.configs.recommended,
|
||||||
|
...tseslint.configs.recommendedTypeChecked,
|
||||||
|
eslintPluginPrettierRecommended,
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.node,
|
||||||
|
...globals.jest,
|
||||||
|
},
|
||||||
|
sourceType: 'commonjs',
|
||||||
|
parserOptions: {
|
||||||
|
projectService: true,
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'@typescript-eslint/no-floating-promises': 'warn',
|
||||||
|
'@typescript-eslint/no-unsafe-argument': 'warn'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
8
nest-cli.json
Normal file
8
nest-cli.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/nest-cli",
|
||||||
|
"collection": "@nestjs/schematics",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"compilerOptions": {
|
||||||
|
"deleteOutDir": true
|
||||||
|
}
|
||||||
|
}
|
||||||
10373
package-lock.json
generated
Normal file
10373
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
71
package.json
Normal file
71
package.json
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
{
|
||||||
|
"name": "retail-nest",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "",
|
||||||
|
"author": "",
|
||||||
|
"private": true,
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"scripts": {
|
||||||
|
"build": "nest build",
|
||||||
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
|
"start": "nest start",
|
||||||
|
"start:dev": "nest start --watch",
|
||||||
|
"start:debug": "nest start --debug --watch",
|
||||||
|
"start:prod": "node dist/main",
|
||||||
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch",
|
||||||
|
"test:cov": "jest --coverage",
|
||||||
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
|
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@nestjs/common": "^11.0.1",
|
||||||
|
"@nestjs/core": "^11.0.1",
|
||||||
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
|
"reflect-metadata": "^0.2.2",
|
||||||
|
"rxjs": "^7.8.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
|
"@eslint/js": "^9.18.0",
|
||||||
|
"@nestjs/cli": "^11.0.0",
|
||||||
|
"@nestjs/schematics": "^11.0.0",
|
||||||
|
"@nestjs/testing": "^11.0.1",
|
||||||
|
"@types/express": "^5.0.0",
|
||||||
|
"@types/jest": "^30.0.0",
|
||||||
|
"@types/node": "^22.10.7",
|
||||||
|
"@types/supertest": "^6.0.2",
|
||||||
|
"eslint": "^9.18.0",
|
||||||
|
"eslint-config-prettier": "^10.0.1",
|
||||||
|
"eslint-plugin-prettier": "^5.2.2",
|
||||||
|
"globals": "^16.0.0",
|
||||||
|
"jest": "^30.0.0",
|
||||||
|
"prettier": "^3.4.2",
|
||||||
|
"source-map-support": "^0.5.21",
|
||||||
|
"supertest": "^7.0.0",
|
||||||
|
"ts-jest": "^29.2.5",
|
||||||
|
"ts-loader": "^9.5.2",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"tsconfig-paths": "^4.2.0",
|
||||||
|
"typescript": "^5.7.3",
|
||||||
|
"typescript-eslint": "^8.20.0"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"moduleFileExtensions": [
|
||||||
|
"js",
|
||||||
|
"json",
|
||||||
|
"ts"
|
||||||
|
],
|
||||||
|
"rootDir": "src",
|
||||||
|
"testRegex": ".*\\.spec\\.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
},
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"**/*.(t|j)s"
|
||||||
|
],
|
||||||
|
"coverageDirectory": "../coverage",
|
||||||
|
"testEnvironment": "node"
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/app.controller.spec.ts
Normal file
22
src/app.controller.spec.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { AppController } from './app.controller';
|
||||||
|
import { AppService } from './app.service';
|
||||||
|
|
||||||
|
describe('AppController', () => {
|
||||||
|
let appController: AppController;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const app: TestingModule = await Test.createTestingModule({
|
||||||
|
controllers: [AppController],
|
||||||
|
providers: [AppService],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
appController = app.get<AppController>(AppController);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('root', () => {
|
||||||
|
it('should return "Hello World!"', () => {
|
||||||
|
expect(appController.getHello()).toBe('Hello World!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
12
src/app.controller.ts
Normal file
12
src/app.controller.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Controller, Get } from '@nestjs/common';
|
||||||
|
import { AppService } from './app.service';
|
||||||
|
|
||||||
|
@Controller()
|
||||||
|
export class AppController {
|
||||||
|
constructor(private readonly appService: AppService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
getHello(): string {
|
||||||
|
return this.appService.getHello();
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/app.module.ts
Normal file
10
src/app.module.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { AppController } from './app.controller';
|
||||||
|
import { AppService } from './app.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [],
|
||||||
|
controllers: [AppController],
|
||||||
|
providers: [AppService],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
8
src/app.service.ts
Normal file
8
src/app.service.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AppService {
|
||||||
|
getHello(): string {
|
||||||
|
return 'Hello World!';
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/main.ts
Normal file
8
src/main.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
import { AppModule } from './app.module';
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
const app = await NestFactory.create(AppModule);
|
||||||
|
await app.listen(process.env.PORT ?? 3000);
|
||||||
|
}
|
||||||
|
bootstrap();
|
||||||
25
test/app.e2e-spec.ts
Normal file
25
test/app.e2e-spec.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { INestApplication } from '@nestjs/common';
|
||||||
|
import request from 'supertest';
|
||||||
|
import { App } from 'supertest/types';
|
||||||
|
import { AppModule } from './../src/app.module';
|
||||||
|
|
||||||
|
describe('AppController (e2e)', () => {
|
||||||
|
let app: INestApplication<App>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||||
|
imports: [AppModule],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
app = moduleFixture.createNestApplication();
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('/ (GET)', () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get('/')
|
||||||
|
.expect(200)
|
||||||
|
.expect('Hello World!');
|
||||||
|
});
|
||||||
|
});
|
||||||
9
test/jest-e2e.json
Normal file
9
test/jest-e2e.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"moduleFileExtensions": ["js", "json", "ts"],
|
||||||
|
"rootDir": ".",
|
||||||
|
"testEnvironment": "node",
|
||||||
|
"testRegex": ".e2e-spec.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
}
|
||||||
|
}
|
||||||
4
tsconfig.build.json
Normal file
4
tsconfig.build.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||||
|
}
|
||||||
25
tsconfig.json
Normal file
25
tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "nodenext",
|
||||||
|
"moduleResolution": "nodenext",
|
||||||
|
"resolvePackageJsonExports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"declaration": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"target": "ES2023",
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"incremental": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"strictBindCallApply": false,
|
||||||
|
"noFallthroughCasesInSwitch": false
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user