Initial commit
This commit is contained in:
502
commands/feature/README.md
Normal file
502
commands/feature/README.md
Normal file
@@ -0,0 +1,502 @@
|
||||
# Feature Implementation Skill
|
||||
|
||||
Production-ready feature implementation across database, backend, and frontend layers with incremental phased approach and comprehensive quality standards.
|
||||
|
||||
## Overview
|
||||
|
||||
This skill provides a complete workflow for implementing full-stack features from database schema to frontend components. It follows industry best practices including layered architecture, comprehensive testing, security hardening, and performance optimization.
|
||||
|
||||
## Available Operations
|
||||
|
||||
### `implement` - Complete Full-Stack Implementation
|
||||
|
||||
Implement a feature across all layers (database, backend, frontend, integration) with production-ready code, tests, and documentation.
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
/10x-fullstack-engineer:feature implement description:"user authentication with OAuth and 2FA" tests:"comprehensive"
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `description` (required) - Detailed feature description
|
||||
- `scope` (optional) - Specific area to focus on
|
||||
- `priority` (optional) - high|medium|low
|
||||
- `tests` (optional) - Coverage level
|
||||
- `framework` (optional) - react|vue|angular
|
||||
|
||||
**What it does:**
|
||||
1. **Requirements Understanding** - Clarifies functional and non-functional requirements
|
||||
2. **Codebase Analysis** - Examines existing patterns and conventions
|
||||
3. **Implementation Design** - Designs database schema, API endpoints, and UI components
|
||||
4. **Incremental Implementation** - Implements in phases (data → backend → frontend → integration)
|
||||
5. **Quality Assurance** - Tests, security, performance, and documentation
|
||||
|
||||
### `database` - Database Layer Only
|
||||
|
||||
Implement database migrations, models, schemas, indexes, and validation for a feature.
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
/10x-fullstack-engineer:feature database description:"user profiles table with indexes" migration:"add_user_profiles"
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `description` (required) - Database changes needed
|
||||
- `migration` (optional) - Migration name
|
||||
- `orm` (optional) - prisma|typeorm|sequelize
|
||||
|
||||
**What it does:**
|
||||
- Schema design with proper types and constraints
|
||||
- Index strategy for query optimization
|
||||
- Migration scripts (up and down)
|
||||
- ORM models/entities
|
||||
- Database operation tests
|
||||
|
||||
**Supports:**
|
||||
- SQL databases (PostgreSQL, MySQL, SQLite)
|
||||
- NoSQL databases (MongoDB)
|
||||
- ORMs (Prisma, TypeORM, Sequelize, Mongoose)
|
||||
|
||||
### `backend` - Backend Layer Only
|
||||
|
||||
Implement repositories, services, API endpoints, validation, and tests for a feature.
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
/10x-fullstack-engineer:feature backend description:"REST API for product search with filters" validation:"strict"
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `description` (required) - Backend functionality needed
|
||||
- `api` (optional) - REST|GraphQL
|
||||
- `validation` (optional) - strict|standard
|
||||
- `auth` (optional) - required|optional
|
||||
|
||||
**What it does:**
|
||||
- **Data Access Layer** - Repositories with query builders
|
||||
- **Business Logic Layer** - Services with validation and error handling
|
||||
- **API Layer** - Controllers and routes
|
||||
- **Validation** - Request/response schemas
|
||||
- **Testing** - Unit and integration tests
|
||||
|
||||
**Supports:**
|
||||
- Express, Fastify, NestJS, Koa frameworks
|
||||
- Zod, Joi, class-validator validation
|
||||
- JWT authentication
|
||||
- RBAC authorization
|
||||
|
||||
### `frontend` - Frontend Layer Only
|
||||
|
||||
Implement components, state management, API integration, and tests for a feature.
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
/10x-fullstack-engineer:feature frontend description:"product catalog with infinite scroll and filters" framework:"react"
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `description` (required) - UI functionality needed
|
||||
- `framework` (optional) - react|vue|angular
|
||||
- `state` (optional) - redux|zustand|context
|
||||
- `tests` (optional) - unit|integration|e2e
|
||||
|
||||
**What it does:**
|
||||
- **Components** - Reusable, accessible UI components
|
||||
- **State Management** - Zustand, Redux, Context API
|
||||
- **API Integration** - HTTP client with interceptors
|
||||
- **Custom Hooks** - Reusable logic
|
||||
- **Testing** - Component and hook tests
|
||||
|
||||
**Supports:**
|
||||
- React, Vue, Angular, Svelte
|
||||
- TypeScript
|
||||
- React Hook Form, Formik for forms
|
||||
- React Query, SWR for server state
|
||||
- TailwindCSS, CSS-in-JS
|
||||
|
||||
### `integrate` - Integration & Polish
|
||||
|
||||
Complete integration testing, performance optimization, security hardening, and documentation.
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
/10x-fullstack-engineer:feature integrate feature:"authentication flow" scope:"E2E tests and performance"
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `feature` (required) - Feature name
|
||||
- `scope` (optional) - e2e|performance|security|documentation
|
||||
- `priority` (optional) - high|medium|low
|
||||
|
||||
**What it does:**
|
||||
- **E2E Testing** - Playwright/Cypress tests for user workflows
|
||||
- **Performance** - Frontend (lazy loading, memoization) and backend (caching, indexes) optimization
|
||||
- **Security** - Input validation, XSS/CSRF protection, rate limiting, security headers
|
||||
- **Documentation** - API docs (OpenAPI), user guides, developer documentation
|
||||
|
||||
### `scaffold` - Generate Boilerplate
|
||||
|
||||
Scaffold feature structure and boilerplate across all layers.
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
/10x-fullstack-engineer:feature scaffold name:"notification-system" layers:"database,backend,frontend"
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `name` (required) - Feature name (kebab-case)
|
||||
- `layers` (optional) - database,backend,frontend (default: all)
|
||||
- `pattern` (optional) - crud|workflow|custom
|
||||
|
||||
**What it does:**
|
||||
Generates complete boilerplate structure:
|
||||
- Database migrations and entities
|
||||
- Repository, service, controller, routes
|
||||
- API client and types
|
||||
- React components and hooks
|
||||
- Test files
|
||||
|
||||
## Feature Types Supported
|
||||
|
||||
### Authentication & Authorization
|
||||
- User registration/login
|
||||
- OAuth/SSO integration
|
||||
- 2FA/MFA
|
||||
- Session management
|
||||
- JWT token handling
|
||||
- RBAC/ABAC
|
||||
|
||||
### Data Management (CRUD)
|
||||
- Resource listing with pagination
|
||||
- Filtering and sorting
|
||||
- Search functionality
|
||||
- Create/update/delete operations
|
||||
- Soft delete support
|
||||
- Audit logging
|
||||
|
||||
### Real-time Features
|
||||
- WebSocket connections
|
||||
- Server-Sent Events (SSE)
|
||||
- Live updates
|
||||
- Presence tracking
|
||||
- Collaborative editing
|
||||
|
||||
### Payment Integration
|
||||
- Stripe/PayPal checkout
|
||||
- Subscription management
|
||||
- Invoice generation
|
||||
- Payment webhooks
|
||||
- Refund processing
|
||||
|
||||
### File Management
|
||||
- Upload with progress
|
||||
- Image optimization
|
||||
- S3/GCS integration
|
||||
- Virus scanning
|
||||
- File validation
|
||||
|
||||
### Search Features
|
||||
- Full-text search
|
||||
- Faceted search
|
||||
- Autocomplete
|
||||
- Advanced filtering
|
||||
- Relevance scoring
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: Requirements Understanding
|
||||
- Functional requirements clarification
|
||||
- Non-functional requirements (performance, security, scalability)
|
||||
- Acceptance criteria definition
|
||||
- Edge case identification
|
||||
|
||||
### Phase 2: Codebase Analysis
|
||||
- Project structure discovery
|
||||
- Tech stack identification
|
||||
- Existing patterns examination
|
||||
- Convention adoption
|
||||
|
||||
### Phase 3: Implementation Design
|
||||
- **Database Design** - Schema, relationships, indexes
|
||||
- **Backend Design** - API endpoints, request/response models, service architecture
|
||||
- **Frontend Design** - Component structure, state management, API integration
|
||||
|
||||
### Phase 4: Incremental Implementation
|
||||
|
||||
#### Phase 4.1 - Data Layer
|
||||
1. Create migration scripts
|
||||
2. Create/update models
|
||||
3. Test database operations
|
||||
|
||||
#### Phase 4.2 - Backend Layer
|
||||
1. Create repository layer
|
||||
2. Create service layer
|
||||
3. Create API controllers
|
||||
4. Create routes
|
||||
5. Write tests
|
||||
|
||||
#### Phase 4.3 - Frontend Layer
|
||||
1. Create API client
|
||||
2. Create React hooks
|
||||
3. Create components
|
||||
4. Write component tests
|
||||
|
||||
#### Phase 4.4 - Integration & Polish
|
||||
1. End-to-end tests
|
||||
2. Performance optimization
|
||||
3. Security hardening
|
||||
4. Documentation
|
||||
|
||||
## Quality Standards
|
||||
|
||||
### Code Quality
|
||||
- [x] Single Responsibility Principle
|
||||
- [x] DRY (Don't Repeat Yourself)
|
||||
- [x] Proper error handling
|
||||
- [x] Input validation
|
||||
- [x] Type safety (TypeScript)
|
||||
- [x] Consistent naming conventions
|
||||
|
||||
### Testing
|
||||
- [x] Unit tests (>80% coverage)
|
||||
- [x] Integration tests for APIs
|
||||
- [x] Component tests for UI
|
||||
- [x] E2E tests for critical flows
|
||||
- [x] Edge case coverage
|
||||
|
||||
### Security
|
||||
- [x] Input validation and sanitization
|
||||
- [x] SQL injection prevention (parameterized queries)
|
||||
- [x] XSS prevention (DOMPurify)
|
||||
- [x] CSRF protection
|
||||
- [x] Authentication/authorization
|
||||
- [x] Rate limiting
|
||||
- [x] Security headers (Helmet)
|
||||
- [x] No hardcoded secrets
|
||||
|
||||
### Performance
|
||||
- [x] Database indexes on frequently queried columns
|
||||
- [x] Query optimization (eager loading, no N+1)
|
||||
- [x] Response caching
|
||||
- [x] Connection pooling
|
||||
- [x] Frontend code splitting
|
||||
- [x] Lazy loading images
|
||||
- [x] Memoization
|
||||
- [x] Virtualization for long lists
|
||||
|
||||
### Accessibility
|
||||
- [x] Semantic HTML
|
||||
- [x] ARIA labels
|
||||
- [x] Keyboard navigation
|
||||
- [x] Alt text for images
|
||||
- [x] Color contrast (WCAG 2.1 AA)
|
||||
- [x] Screen reader support
|
||||
|
||||
### Documentation
|
||||
- [x] API documentation (OpenAPI/Swagger)
|
||||
- [x] Code comments for complex logic
|
||||
- [x] Usage examples
|
||||
- [x] Deployment instructions
|
||||
- [x] Environment variables documented
|
||||
|
||||
## Common Workflows
|
||||
|
||||
### 1. Implement Complete CRUD Feature
|
||||
|
||||
```bash
|
||||
# Full-stack implementation
|
||||
/10x-fullstack-engineer:feature implement description:"blog post management with rich text editor, categories, tags, and draft/publish workflow"
|
||||
|
||||
# What you get:
|
||||
# - Database: posts, categories, tags tables with relationships
|
||||
# - Backend: REST API with CRUD endpoints, validation, search
|
||||
# - Frontend: Post list, detail, create/edit forms, rich text editor
|
||||
# - Tests: Unit, integration, E2E
|
||||
# - Docs: API documentation
|
||||
```
|
||||
|
||||
### 2. Add New API Endpoints to Existing Feature
|
||||
|
||||
```bash
|
||||
# Backend only
|
||||
/10x-fullstack-engineer:feature backend description:"Add bulk operations API for products (bulk delete, bulk update status, bulk export)"
|
||||
```
|
||||
|
||||
### 3. Build New UI Screen
|
||||
|
||||
```bash
|
||||
# Frontend only
|
||||
/10x-fullstack-engineer:feature frontend description:"Admin dashboard with charts showing sales, users, and revenue metrics" framework:"react" state:"zustand"
|
||||
```
|
||||
|
||||
### 4. Optimize Existing Feature
|
||||
|
||||
```bash
|
||||
# Integration & polish
|
||||
/10x-fullstack-engineer:feature integrate feature:"product catalog" scope:"performance and E2E tests"
|
||||
```
|
||||
|
||||
### 5. Quick Feature Scaffolding
|
||||
|
||||
```bash
|
||||
# Generate boilerplate
|
||||
/10x-fullstack-engineer:feature scaffold name:"email-notifications" layers:"database,backend"
|
||||
|
||||
# Then customize the generated files
|
||||
```
|
||||
|
||||
## Architecture Patterns
|
||||
|
||||
### Layered Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Presentation Layer │ React/Vue/Angular Components
|
||||
│ (Components, Hooks, State) │
|
||||
└──────────────┬──────────────────────┘
|
||||
│ API Client
|
||||
┌──────────────▼──────────────────────┐
|
||||
│ API Layer │ Controllers, Routes, Middleware
|
||||
│ (Request/Response Handling) │
|
||||
└──────────────┬──────────────────────┘
|
||||
│
|
||||
┌──────────────▼──────────────────────┐
|
||||
│ Business Logic Layer │ Services, Validation, Rules
|
||||
│ (Domain Logic) │
|
||||
└──────────────┬──────────────────────┘
|
||||
│
|
||||
┌──────────────▼──────────────────────┐
|
||||
│ Data Access Layer │ Repositories, Query Builders
|
||||
│ (Database Operations) │
|
||||
└──────────────┬──────────────────────┘
|
||||
│
|
||||
┌──────────────▼──────────────────────┐
|
||||
│ Database Layer │ PostgreSQL, MongoDB, etc.
|
||||
│ (Schema, Migrations, Indexes) │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Repository Pattern
|
||||
- Abstracts data access
|
||||
- Enables testability
|
||||
- Centralizes query logic
|
||||
|
||||
### Service Pattern
|
||||
- Contains business logic
|
||||
- Orchestrates repositories
|
||||
- Handles validation
|
||||
|
||||
### Controller Pattern
|
||||
- HTTP request/response handling
|
||||
- Delegates to services
|
||||
- Thin layer
|
||||
|
||||
## Example Output
|
||||
|
||||
For a feature like "user authentication", the implementation includes:
|
||||
|
||||
### Database Layer
|
||||
```sql
|
||||
-- Migration: users and sessions tables
|
||||
CREATE TABLE users (
|
||||
id UUID PRIMARY KEY,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
email_verified BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_users_email ON users(email);
|
||||
```
|
||||
|
||||
### Backend Layer
|
||||
```typescript
|
||||
// Service with business logic
|
||||
async register(input: RegisterInput): Promise<{ user: User; tokens: AuthTokens }> {
|
||||
this.validateEmail(input.email);
|
||||
this.validatePassword(input.password);
|
||||
|
||||
const passwordHash = await bcrypt.hash(input.password, 12);
|
||||
const user = await this.userRepository.create({ email: input.email, passwordHash });
|
||||
const tokens = await this.generateTokens(user.id);
|
||||
|
||||
return { user, tokens };
|
||||
}
|
||||
```
|
||||
|
||||
### Frontend Layer
|
||||
```typescript
|
||||
// React component with state management
|
||||
export const LoginForm: React.FC = () => {
|
||||
const { login, isLoading, error } = useAuth();
|
||||
const { register, handleSubmit, formState: { errors } } = useForm();
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
await login(data.email, data.password);
|
||||
};
|
||||
|
||||
return <form onSubmit={handleSubmit(onSubmit)}>...</form>;
|
||||
};
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
The skill handles various scenarios:
|
||||
|
||||
### Unclear Requirements
|
||||
- Asks specific questions about acceptance criteria
|
||||
- Requests clarification on edge cases
|
||||
- Provides examples to confirm understanding
|
||||
- Suggests sensible defaults
|
||||
|
||||
### Missing Context
|
||||
- Lists needed information (tech stack, patterns)
|
||||
- Attempts to discover from codebase
|
||||
- Documents assumptions made
|
||||
- Provides alternatives if context unclear
|
||||
|
||||
### Implementation Blockers
|
||||
- Clearly identifies the blocker
|
||||
- Suggests alternative approaches
|
||||
- Provides workarounds if available
|
||||
- Documents issue for resolution
|
||||
- Continues with unblocked portions
|
||||
|
||||
## Dependencies
|
||||
|
||||
This skill works with common tech stacks:
|
||||
|
||||
**Backend:**
|
||||
- Node.js with Express, Fastify, NestJS
|
||||
- TypeScript
|
||||
- TypeORM, Prisma, Sequelize (ORMs)
|
||||
- PostgreSQL, MySQL, MongoDB
|
||||
- Jest, Vitest (testing)
|
||||
|
||||
**Frontend:**
|
||||
- React, Vue, Angular
|
||||
- TypeScript
|
||||
- Zustand, Redux, Context API (state)
|
||||
- React Hook Form, Zod (forms/validation)
|
||||
- React Testing Library (testing)
|
||||
- Playwright, Cypress (E2E)
|
||||
|
||||
## Tips for Best Results
|
||||
|
||||
1. **Be specific in descriptions** - More detail leads to better implementations
|
||||
2. **Specify framework/ORM** - Helps generate appropriate code
|
||||
3. **Start with scaffold** - Use `scaffold` for quick boilerplate, then customize
|
||||
4. **Layer-by-layer approach** - Implement database → backend → frontend for complex features
|
||||
5. **Use integrate for polish** - Don't skip the integration phase for production features
|
||||
|
||||
## Related Skills
|
||||
|
||||
This skill is part of the 10x Fullstack Engineer plugin:
|
||||
- `/api` - API design and implementation
|
||||
- `/database` - Database design and optimization
|
||||
- `/test` - Test generation and coverage
|
||||
- `/deploy` - Deployment and CI/CD
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
779
commands/feature/backend.md
Normal file
779
commands/feature/backend.md
Normal file
@@ -0,0 +1,779 @@
|
||||
# Backend Layer Operation
|
||||
|
||||
Implement backend layer only: repositories, services, API endpoints, validation, and tests for a feature.
|
||||
|
||||
## Parameters
|
||||
|
||||
**Received**: `$ARGUMENTS` (after removing 'backend' operation name)
|
||||
|
||||
Expected format: `description:"backend functionality needed" [api:"REST|GraphQL"] [validation:"strict|standard"] [auth:"required|optional"]`
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Understand Backend Requirements
|
||||
|
||||
Clarify:
|
||||
- What business logic needs to be implemented?
|
||||
- What API endpoints are needed (methods, paths, parameters)?
|
||||
- What validation rules apply?
|
||||
- What authentication/authorization is required?
|
||||
- What external services need integration?
|
||||
|
||||
### 2. Analyze Existing Backend Structure
|
||||
|
||||
```bash
|
||||
# Find backend structure
|
||||
find . -path "*/src/server/*" -o -path "*/api/*" -o -path "*/backend/*"
|
||||
|
||||
# Identify framework
|
||||
cat package.json | grep -E "(express|fastify|nest|koa|hapi)"
|
||||
|
||||
# Find existing patterns
|
||||
find . -path "*/controllers/*" -o -path "*/services/*" -o -path "*/routes/*"
|
||||
```
|
||||
|
||||
**Identify:**
|
||||
- Framework (Express, Fastify, NestJS, etc.)
|
||||
- Architecture pattern (MVC, Clean Architecture, Layered)
|
||||
- Error handling approach
|
||||
- Validation library (class-validator, Joi, Zod)
|
||||
- Testing framework (Jest, Mocha, Vitest)
|
||||
|
||||
### 3. Implement Layers
|
||||
|
||||
#### Layer 1: Data Access (Repository Pattern)
|
||||
|
||||
```typescript
|
||||
// repositories/ProductRepository.ts
|
||||
import { Repository } from 'typeorm';
|
||||
import { Product } from '../entities/Product.entity';
|
||||
import { AppDataSource } from '../config/database';
|
||||
|
||||
export interface ProductFilters {
|
||||
categoryId?: string;
|
||||
minPrice?: number;
|
||||
maxPrice?: number;
|
||||
inStock?: boolean;
|
||||
search?: string;
|
||||
}
|
||||
|
||||
export interface PaginationOptions {
|
||||
page: number;
|
||||
limit: number;
|
||||
sortBy?: string;
|
||||
sortOrder?: 'ASC' | 'DESC';
|
||||
}
|
||||
|
||||
export class ProductRepository {
|
||||
private repository: Repository<Product>;
|
||||
|
||||
constructor() {
|
||||
this.repository = AppDataSource.getRepository(Product);
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Product | null> {
|
||||
return this.repository.findOne({
|
||||
where: { id },
|
||||
relations: ['category', 'images', 'tags'],
|
||||
});
|
||||
}
|
||||
|
||||
async findAll(
|
||||
filters: ProductFilters,
|
||||
pagination: PaginationOptions
|
||||
): Promise<{ products: Product[]; total: number }> {
|
||||
const query = this.repository
|
||||
.createQueryBuilder('product')
|
||||
.leftJoinAndSelect('product.category', 'category')
|
||||
.leftJoinAndSelect('product.images', 'images')
|
||||
.leftJoinAndSelect('product.tags', 'tags');
|
||||
|
||||
// Apply filters
|
||||
if (filters.categoryId) {
|
||||
query.andWhere('product.categoryId = :categoryId', {
|
||||
categoryId: filters.categoryId,
|
||||
});
|
||||
}
|
||||
|
||||
if (filters.minPrice !== undefined) {
|
||||
query.andWhere('product.price >= :minPrice', { minPrice: filters.minPrice });
|
||||
}
|
||||
|
||||
if (filters.maxPrice !== undefined) {
|
||||
query.andWhere('product.price <= :maxPrice', { maxPrice: filters.maxPrice });
|
||||
}
|
||||
|
||||
if (filters.inStock) {
|
||||
query.andWhere('product.stockQuantity > 0');
|
||||
}
|
||||
|
||||
if (filters.search) {
|
||||
query.andWhere(
|
||||
'(product.name ILIKE :search OR product.description ILIKE :search)',
|
||||
{ search: `%${filters.search}%` }
|
||||
);
|
||||
}
|
||||
|
||||
// Apply sorting
|
||||
const sortBy = pagination.sortBy || 'createdAt';
|
||||
const sortOrder = pagination.sortOrder || 'DESC';
|
||||
query.orderBy(`product.${sortBy}`, sortOrder);
|
||||
|
||||
// Apply pagination
|
||||
const skip = (pagination.page - 1) * pagination.limit;
|
||||
query.skip(skip).take(pagination.limit);
|
||||
|
||||
const [products, total] = await query.getManyAndCount();
|
||||
|
||||
return { products, total };
|
||||
}
|
||||
|
||||
async create(data: Partial<Product>): Promise<Product> {
|
||||
const product = this.repository.create(data);
|
||||
return this.repository.save(product);
|
||||
}
|
||||
|
||||
async update(id: string, data: Partial<Product>): Promise<Product> {
|
||||
await this.repository.update(id, data);
|
||||
const updated = await this.findById(id);
|
||||
if (!updated) {
|
||||
throw new Error('Product not found after update');
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
await this.repository.softDelete(id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Layer 2: Business Logic (Service Layer)
|
||||
|
||||
```typescript
|
||||
// services/ProductService.ts
|
||||
import { ProductRepository, ProductFilters, PaginationOptions } from '../repositories/ProductRepository';
|
||||
import { Product } from '../entities/Product.entity';
|
||||
import { NotFoundError, ValidationError, ConflictError } from '../errors';
|
||||
import { slugify } from '../utils/slugify';
|
||||
|
||||
export interface CreateProductInput {
|
||||
name: string;
|
||||
description?: string;
|
||||
price: number;
|
||||
currency?: string;
|
||||
stockQuantity: number;
|
||||
categoryId?: string;
|
||||
images?: Array<{ url: string; altText?: string }>;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export interface UpdateProductInput {
|
||||
name?: string;
|
||||
description?: string;
|
||||
price?: number;
|
||||
stockQuantity?: number;
|
||||
categoryId?: string;
|
||||
}
|
||||
|
||||
export class ProductService {
|
||||
constructor(private productRepository: ProductRepository) {}
|
||||
|
||||
async getProduct(id: string): Promise<Product> {
|
||||
const product = await this.productRepository.findById(id);
|
||||
if (!product) {
|
||||
throw new NotFoundError(`Product with ID ${id} not found`);
|
||||
}
|
||||
return product;
|
||||
}
|
||||
|
||||
async listProducts(
|
||||
filters: ProductFilters,
|
||||
pagination: PaginationOptions
|
||||
): Promise<{ products: Product[]; total: number; page: number; totalPages: number }> {
|
||||
const { products, total } = await this.productRepository.findAll(filters, pagination);
|
||||
|
||||
return {
|
||||
products,
|
||||
total,
|
||||
page: pagination.page,
|
||||
totalPages: Math.ceil(total / pagination.limit),
|
||||
};
|
||||
}
|
||||
|
||||
async createProduct(input: CreateProductInput): Promise<Product> {
|
||||
// Validate input
|
||||
this.validateProductInput(input);
|
||||
|
||||
// Generate slug from name
|
||||
const slug = slugify(input.name);
|
||||
|
||||
// Check if slug already exists
|
||||
const existing = await this.productRepository.findBySlug(slug);
|
||||
if (existing) {
|
||||
throw new ConflictError('Product with this name already exists');
|
||||
}
|
||||
|
||||
// Create product
|
||||
const product = await this.productRepository.create({
|
||||
...input,
|
||||
slug,
|
||||
});
|
||||
|
||||
return product;
|
||||
}
|
||||
|
||||
async updateProduct(id: string, input: UpdateProductInput): Promise<Product> {
|
||||
// Check if product exists
|
||||
await this.getProduct(id);
|
||||
|
||||
// Validate input
|
||||
if (input.price !== undefined && input.price < 0) {
|
||||
throw new ValidationError('Price must be non-negative');
|
||||
}
|
||||
|
||||
if (input.stockQuantity !== undefined && input.stockQuantity < 0) {
|
||||
throw new ValidationError('Stock quantity must be non-negative');
|
||||
}
|
||||
|
||||
// Update product
|
||||
const updated = await this.productRepository.update(id, input);
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
async deleteProduct(id: string): Promise<void> {
|
||||
// Check if product exists
|
||||
await this.getProduct(id);
|
||||
|
||||
// Soft delete
|
||||
await this.productRepository.delete(id);
|
||||
}
|
||||
|
||||
async adjustStock(id: string, quantity: number): Promise<Product> {
|
||||
const product = await this.getProduct(id);
|
||||
|
||||
const newQuantity = product.stockQuantity + quantity;
|
||||
if (newQuantity < 0) {
|
||||
throw new ValidationError('Insufficient stock');
|
||||
}
|
||||
|
||||
return this.productRepository.update(id, { stockQuantity: newQuantity });
|
||||
}
|
||||
|
||||
private validateProductInput(input: CreateProductInput): void {
|
||||
if (!input.name || input.name.trim().length === 0) {
|
||||
throw new ValidationError('Product name is required');
|
||||
}
|
||||
|
||||
if (input.name.length > 255) {
|
||||
throw new ValidationError('Product name must not exceed 255 characters');
|
||||
}
|
||||
|
||||
if (input.price < 0) {
|
||||
throw new ValidationError('Price must be non-negative');
|
||||
}
|
||||
|
||||
if (input.stockQuantity < 0) {
|
||||
throw new ValidationError('Stock quantity must be non-negative');
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Layer 3: API Layer (Controllers & Routes)
|
||||
|
||||
```typescript
|
||||
// controllers/ProductController.ts
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { ProductService } from '../services/ProductService';
|
||||
|
||||
export class ProductController {
|
||||
constructor(private productService: ProductService) {}
|
||||
|
||||
getProduct = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const product = await this.productService.getProduct(id);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: product,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
listProducts = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const filters = {
|
||||
categoryId: req.query.categoryId as string,
|
||||
minPrice: req.query.minPrice ? parseFloat(req.query.minPrice as string) : undefined,
|
||||
maxPrice: req.query.maxPrice ? parseFloat(req.query.maxPrice as string) : undefined,
|
||||
inStock: req.query.inStock === 'true',
|
||||
search: req.query.search as string,
|
||||
};
|
||||
|
||||
const pagination = {
|
||||
page: parseInt(req.query.page as string) || 1,
|
||||
limit: parseInt(req.query.limit as string) || 20,
|
||||
sortBy: (req.query.sortBy as string) || 'createdAt',
|
||||
sortOrder: (req.query.sortOrder as 'ASC' | 'DESC') || 'DESC',
|
||||
};
|
||||
|
||||
const result = await this.productService.listProducts(filters, pagination);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result.products,
|
||||
meta: {
|
||||
total: result.total,
|
||||
page: result.page,
|
||||
totalPages: result.totalPages,
|
||||
limit: pagination.limit,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
createProduct = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const product = await this.productService.createProduct(req.body);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: product,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
updateProduct = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const product = await this.productService.updateProduct(id, req.body);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: product,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
deleteProduct = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
await this.productService.deleteProduct(id);
|
||||
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// routes/product.routes.ts
|
||||
import { Router } from 'express';
|
||||
import { ProductController } from '../controllers/ProductController';
|
||||
import { ProductService } from '../services/ProductService';
|
||||
import { ProductRepository } from '../repositories/ProductRepository';
|
||||
import { authenticate } from '../middlewares/auth.middleware';
|
||||
import { validate } from '../middlewares/validation.middleware';
|
||||
import { createProductSchema, updateProductSchema } from '../schemas/product.schemas';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Initialize dependencies
|
||||
const productRepository = new ProductRepository();
|
||||
const productService = new ProductService(productRepository);
|
||||
const productController = new ProductController(productService);
|
||||
|
||||
// Public routes
|
||||
router.get('/', productController.listProducts);
|
||||
router.get('/:id', productController.getProduct);
|
||||
|
||||
// Protected routes (require authentication)
|
||||
router.post(
|
||||
'/',
|
||||
authenticate,
|
||||
validate(createProductSchema),
|
||||
productController.createProduct
|
||||
);
|
||||
|
||||
router.put(
|
||||
'/:id',
|
||||
authenticate,
|
||||
validate(updateProductSchema),
|
||||
productController.updateProduct
|
||||
);
|
||||
|
||||
router.delete('/:id', authenticate, productController.deleteProduct);
|
||||
|
||||
export default router;
|
||||
```
|
||||
|
||||
#### Validation Schemas
|
||||
|
||||
```typescript
|
||||
// schemas/product.schemas.ts
|
||||
import { z } from 'zod';
|
||||
|
||||
export const createProductSchema = z.object({
|
||||
body: z.object({
|
||||
name: z.string().min(1).max(255),
|
||||
description: z.string().optional(),
|
||||
price: z.number().min(0),
|
||||
currency: z.string().length(3).optional(),
|
||||
stockQuantity: z.number().int().min(0),
|
||||
categoryId: z.string().uuid().optional(),
|
||||
images: z.array(
|
||||
z.object({
|
||||
url: z.string().url(),
|
||||
altText: z.string().optional(),
|
||||
})
|
||||
).optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const updateProductSchema = z.object({
|
||||
body: z.object({
|
||||
name: z.string().min(1).max(255).optional(),
|
||||
description: z.string().optional(),
|
||||
price: z.number().min(0).optional(),
|
||||
stockQuantity: z.number().int().min(0).optional(),
|
||||
categoryId: z.string().uuid().optional(),
|
||||
}),
|
||||
params: z.object({
|
||||
id: z.string().uuid(),
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
### 4. Write Tests
|
||||
|
||||
```typescript
|
||||
// services/__tests__/ProductService.test.ts
|
||||
import { ProductService } from '../ProductService';
|
||||
import { ProductRepository } from '../../repositories/ProductRepository';
|
||||
import { NotFoundError, ValidationError } from '../../errors';
|
||||
|
||||
describe('ProductService', () => {
|
||||
let productService: ProductService;
|
||||
let productRepository: jest.Mocked<ProductRepository>;
|
||||
|
||||
beforeEach(() => {
|
||||
productRepository = {
|
||||
findById: jest.fn(),
|
||||
findAll: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
} as any;
|
||||
|
||||
productService = new ProductService(productRepository);
|
||||
});
|
||||
|
||||
describe('createProduct', () => {
|
||||
it('should create product with valid input', async () => {
|
||||
const input = {
|
||||
name: 'Test Product',
|
||||
price: 99.99,
|
||||
stockQuantity: 10,
|
||||
};
|
||||
|
||||
productRepository.create.mockResolvedValue({
|
||||
id: 'product-id',
|
||||
...input,
|
||||
slug: 'test-product',
|
||||
} as any);
|
||||
|
||||
const result = await productService.createProduct(input);
|
||||
|
||||
expect(result.name).toBe('Test Product');
|
||||
expect(productRepository.create).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw ValidationError for negative price', async () => {
|
||||
await expect(
|
||||
productService.createProduct({
|
||||
name: 'Test',
|
||||
price: -10,
|
||||
stockQuantity: 5,
|
||||
})
|
||||
).rejects.toThrow(ValidationError);
|
||||
});
|
||||
|
||||
it('should throw ValidationError for empty name', async () => {
|
||||
await expect(
|
||||
productService.createProduct({
|
||||
name: '',
|
||||
price: 10,
|
||||
stockQuantity: 5,
|
||||
})
|
||||
).rejects.toThrow(ValidationError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getProduct', () => {
|
||||
it('should return product if found', async () => {
|
||||
const product = { id: 'product-id', name: 'Test Product' };
|
||||
productRepository.findById.mockResolvedValue(product as any);
|
||||
|
||||
const result = await productService.getProduct('product-id');
|
||||
|
||||
expect(result).toEqual(product);
|
||||
});
|
||||
|
||||
it('should throw NotFoundError if product not found', async () => {
|
||||
productRepository.findById.mockResolvedValue(null);
|
||||
|
||||
await expect(productService.getProduct('invalid-id')).rejects.toThrow(
|
||||
NotFoundError
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
```typescript
|
||||
// controllers/__tests__/ProductController.test.ts
|
||||
import request from 'supertest';
|
||||
import { app } from '../../app';
|
||||
import { ProductRepository } from '../../repositories/ProductRepository';
|
||||
|
||||
describe('ProductController', () => {
|
||||
let productRepository: ProductRepository;
|
||||
|
||||
beforeEach(async () => {
|
||||
await clearDatabase();
|
||||
productRepository = new ProductRepository();
|
||||
});
|
||||
|
||||
describe('GET /api/products', () => {
|
||||
it('should return list of products', async () => {
|
||||
await productRepository.create({
|
||||
name: 'Product 1',
|
||||
slug: 'product-1',
|
||||
price: 10,
|
||||
stockQuantity: 5,
|
||||
});
|
||||
|
||||
const response = await request(app).get('/api/products').expect(200);
|
||||
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(response.body.data).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should filter by category', async () => {
|
||||
const category = await createTestCategory();
|
||||
|
||||
await productRepository.create({
|
||||
name: 'Product 1',
|
||||
slug: 'product-1',
|
||||
price: 10,
|
||||
stockQuantity: 5,
|
||||
categoryId: category.id,
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/products')
|
||||
.query({ categoryId: category.id })
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.data).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should paginate results', async () => {
|
||||
// Create 25 products
|
||||
for (let i = 0; i < 25; i++) {
|
||||
await productRepository.create({
|
||||
name: `Product ${i}`,
|
||||
slug: `product-${i}`,
|
||||
price: 10,
|
||||
stockQuantity: 5,
|
||||
});
|
||||
}
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/products')
|
||||
.query({ page: 2, limit: 10 })
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.data).toHaveLength(10);
|
||||
expect(response.body.meta.page).toBe(2);
|
||||
expect(response.body.meta.totalPages).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/products', () => {
|
||||
it('should create product with valid data', async () => {
|
||||
const authToken = await getAuthToken();
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/products')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send({
|
||||
name: 'New Product',
|
||||
price: 99.99,
|
||||
stockQuantity: 10,
|
||||
})
|
||||
.expect(201);
|
||||
|
||||
expect(response.body.data.name).toBe('New Product');
|
||||
});
|
||||
|
||||
it('should return 401 without authentication', async () => {
|
||||
await request(app)
|
||||
.post('/api/products')
|
||||
.send({
|
||||
name: 'New Product',
|
||||
price: 99.99,
|
||||
stockQuantity: 10,
|
||||
})
|
||||
.expect(401);
|
||||
});
|
||||
|
||||
it('should return 400 for invalid data', async () => {
|
||||
const authToken = await getAuthToken();
|
||||
|
||||
await request(app)
|
||||
.post('/api/products')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send({
|
||||
name: '',
|
||||
price: -10,
|
||||
})
|
||||
.expect(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /api/products/:id', () => {
|
||||
it('should update product', async () => {
|
||||
const product = await productRepository.create({
|
||||
name: 'Original Name',
|
||||
slug: 'original',
|
||||
price: 10,
|
||||
stockQuantity: 5,
|
||||
});
|
||||
|
||||
const authToken = await getAuthToken();
|
||||
|
||||
const response = await request(app)
|
||||
.put(`/api/products/${product.id}`)
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send({
|
||||
name: 'Updated Name',
|
||||
price: 20,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.data.name).toBe('Updated Name');
|
||||
expect(response.body.data.price).toBe(20);
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent product', async () => {
|
||||
const authToken = await getAuthToken();
|
||||
|
||||
await request(app)
|
||||
.put('/api/products/non-existent-id')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send({ name: 'Updated' })
|
||||
.expect(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /api/products/:id', () => {
|
||||
it('should delete product', async () => {
|
||||
const product = await productRepository.create({
|
||||
name: 'To Delete',
|
||||
slug: 'to-delete',
|
||||
price: 10,
|
||||
stockQuantity: 5,
|
||||
});
|
||||
|
||||
const authToken = await getAuthToken();
|
||||
|
||||
await request(app)
|
||||
.delete(`/api/products/${product.id}`)
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.expect(204);
|
||||
|
||||
const deleted = await productRepository.findById(product.id);
|
||||
expect(deleted).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Output Format
|
||||
|
||||
```markdown
|
||||
# Backend Layer: {Feature Name}
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### {Method} {Path}
|
||||
- Description: {description}
|
||||
- Authentication: {required|optional|none}
|
||||
- Request: {request_schema}
|
||||
- Response: {response_schema}
|
||||
- Status Codes: {codes}
|
||||
|
||||
## Architecture
|
||||
|
||||
### Repository Layer
|
||||
\`\`\`typescript
|
||||
{repository_code}
|
||||
\`\`\`
|
||||
|
||||
### Service Layer
|
||||
\`\`\`typescript
|
||||
{service_code}
|
||||
\`\`\`
|
||||
|
||||
### Controller Layer
|
||||
\`\`\`typescript
|
||||
{controller_code}
|
||||
\`\`\`
|
||||
|
||||
### Routes
|
||||
\`\`\`typescript
|
||||
{routes_code}
|
||||
\`\`\`
|
||||
|
||||
## Validation
|
||||
|
||||
### Schemas
|
||||
\`\`\`typescript
|
||||
{validation_schemas}
|
||||
\`\`\`
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Tests
|
||||
- {test_description}: {status}
|
||||
|
||||
### Integration Tests
|
||||
- {test_description}: {status}
|
||||
|
||||
## Error Handling
|
||||
- {error_type}: {handling_approach}
|
||||
|
||||
## Authentication
|
||||
- {auth_details}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
- If framework unclear: Detect from package.json or ask
|
||||
- If auth unclear: Suggest standard JWT approach
|
||||
- If validation library unclear: Provide examples for common libraries
|
||||
916
commands/feature/database.md
Normal file
916
commands/feature/database.md
Normal file
@@ -0,0 +1,916 @@
|
||||
# Database Layer Operation
|
||||
|
||||
Implement database layer only: migrations, models, schemas, indexes, and validation for a feature.
|
||||
|
||||
## Parameters
|
||||
|
||||
**Received**: `$ARGUMENTS` (after removing 'database' operation name)
|
||||
|
||||
Expected format: `description:"database changes needed" [migration:"migration_name"] [orm:"prisma|typeorm|sequelize"]`
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Understand Database Requirements
|
||||
|
||||
Parse the requirements and clarify:
|
||||
- What tables/collections need to be created or modified?
|
||||
- What are the relationships between entities?
|
||||
- What queries will be frequently executed (for index design)?
|
||||
- What are the data validation requirements?
|
||||
- Are there any data migration needs (existing data to transform)?
|
||||
|
||||
### 2. Analyze Existing Database Structure
|
||||
|
||||
Examine current database setup:
|
||||
|
||||
```bash
|
||||
# Find existing migrations
|
||||
find . -path "*/migrations/*" -o -path "*/prisma/migrations/*"
|
||||
|
||||
# Find existing models
|
||||
find . -path "*/models/*" -o -path "*/entities/*" -o -name "schema.prisma"
|
||||
|
||||
# Check ORM configuration
|
||||
find . -name "ormconfig.js" -o -name "datasource.ts" -o -name "schema.prisma"
|
||||
```
|
||||
|
||||
**Identify:**
|
||||
- ORM being used (Prisma, TypeORM, Sequelize, Mongoose, etc.)
|
||||
- Database type (PostgreSQL, MySQL, MongoDB, etc.)
|
||||
- Naming conventions for tables and columns
|
||||
- Migration strategy and tooling
|
||||
- Existing relationships and constraints
|
||||
|
||||
### 3. Design Database Schema
|
||||
|
||||
#### Schema Design Template
|
||||
|
||||
**For SQL Databases:**
|
||||
```sql
|
||||
-- Example: E-commerce Product Catalog
|
||||
|
||||
-- Main products table
|
||||
CREATE TABLE products (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(255) NOT NULL,
|
||||
slug VARCHAR(255) UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
price DECIMAL(10, 2) NOT NULL,
|
||||
currency VARCHAR(3) DEFAULT 'USD',
|
||||
stock_quantity INTEGER NOT NULL DEFAULT 0,
|
||||
category_id UUID REFERENCES categories(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW(),
|
||||
deleted_at TIMESTAMP, -- Soft delete
|
||||
|
||||
-- Constraints
|
||||
CONSTRAINT price_positive CHECK (price >= 0),
|
||||
CONSTRAINT stock_non_negative CHECK (stock_quantity >= 0),
|
||||
CONSTRAINT slug_format CHECK (slug ~* '^[a-z0-9-]+$')
|
||||
);
|
||||
|
||||
-- Categories table
|
||||
CREATE TABLE categories (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(100) NOT NULL,
|
||||
slug VARCHAR(100) UNIQUE NOT NULL,
|
||||
parent_id UUID REFERENCES categories(id) ON DELETE CASCADE,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Product images table (one-to-many)
|
||||
CREATE TABLE product_images (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
product_id UUID NOT NULL REFERENCES products(id) ON DELETE CASCADE,
|
||||
url VARCHAR(500) NOT NULL,
|
||||
alt_text VARCHAR(255),
|
||||
display_order INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Product tags (many-to-many)
|
||||
CREATE TABLE tags (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(50) UNIQUE NOT NULL,
|
||||
slug VARCHAR(50) UNIQUE NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE product_tags (
|
||||
product_id UUID NOT NULL REFERENCES products(id) ON DELETE CASCADE,
|
||||
tag_id UUID NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (product_id, tag_id)
|
||||
);
|
||||
|
||||
-- Indexes for performance
|
||||
CREATE INDEX idx_products_category_id ON products(category_id);
|
||||
CREATE INDEX idx_products_slug ON products(slug);
|
||||
CREATE INDEX idx_products_created_at ON products(created_at DESC);
|
||||
CREATE INDEX idx_products_price ON products(price);
|
||||
CREATE INDEX idx_products_stock ON products(stock_quantity) WHERE stock_quantity > 0;
|
||||
CREATE INDEX idx_products_deleted_at ON products(deleted_at) WHERE deleted_at IS NULL;
|
||||
|
||||
CREATE INDEX idx_categories_parent_id ON categories(parent_id);
|
||||
CREATE INDEX idx_categories_slug ON categories(slug);
|
||||
|
||||
CREATE INDEX idx_product_images_product_id ON product_images(product_id);
|
||||
CREATE INDEX idx_product_tags_product_id ON product_tags(product_id);
|
||||
CREATE INDEX idx_product_tags_tag_id ON product_tags(tag_id);
|
||||
|
||||
-- Full-text search index
|
||||
CREATE INDEX idx_products_search ON products USING GIN(to_tsvector('english', name || ' ' || COALESCE(description, '')));
|
||||
```
|
||||
|
||||
**For NoSQL (MongoDB):**
|
||||
```javascript
|
||||
// Product schema
|
||||
{
|
||||
_id: ObjectId,
|
||||
name: String,
|
||||
slug: String, // indexed, unique
|
||||
description: String,
|
||||
price: {
|
||||
amount: Number,
|
||||
currency: String
|
||||
},
|
||||
stockQuantity: Number,
|
||||
category: {
|
||||
id: ObjectId,
|
||||
name: String, // denormalized for performance
|
||||
slug: String
|
||||
},
|
||||
images: [
|
||||
{
|
||||
url: String,
|
||||
altText: String,
|
||||
displayOrder: Number
|
||||
}
|
||||
],
|
||||
tags: [String], // indexed for queries
|
||||
createdAt: Date,
|
||||
updatedAt: Date,
|
||||
deletedAt: Date // soft delete
|
||||
}
|
||||
|
||||
// Indexes
|
||||
db.products.createIndex({ slug: 1 }, { unique: true })
|
||||
db.products.createIndex({ "category.id": 1 })
|
||||
db.products.createIndex({ price.amount: 1 })
|
||||
db.products.createIndex({ tags: 1 })
|
||||
db.products.createIndex({ createdAt: -1 })
|
||||
db.products.createIndex({ name: "text", description: "text" }) // Full-text search
|
||||
```
|
||||
|
||||
#### Index Strategy
|
||||
|
||||
**When to add indexes:**
|
||||
- Primary keys (always)
|
||||
- Foreign keys (for JOIN performance)
|
||||
- Columns used in WHERE clauses
|
||||
- Columns used in ORDER BY
|
||||
- Columns used in GROUP BY
|
||||
- Columns used for full-text search
|
||||
|
||||
**Composite indexes** for queries with multiple conditions:
|
||||
```sql
|
||||
-- Query: SELECT * FROM products WHERE category_id = ? AND price > ? ORDER BY created_at DESC
|
||||
CREATE INDEX idx_products_category_price_created ON products(category_id, price, created_at DESC);
|
||||
```
|
||||
|
||||
**Partial indexes** for specific conditions:
|
||||
```sql
|
||||
-- Only index active (non-deleted) products
|
||||
CREATE INDEX idx_active_products ON products(created_at) WHERE deleted_at IS NULL;
|
||||
```
|
||||
|
||||
### 4. Create Migration Scripts
|
||||
|
||||
#### Example: TypeORM Migration
|
||||
|
||||
```typescript
|
||||
// migrations/1704124800000-AddProductCatalog.ts
|
||||
import { MigrationInterface, QueryRunner, Table, TableForeignKey, TableIndex } from 'typeorm';
|
||||
|
||||
export class AddProductCatalog1704124800000 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
// Create categories table
|
||||
await queryRunner.createTable(
|
||||
new Table({
|
||||
name: 'categories',
|
||||
columns: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'uuid',
|
||||
isPrimary: true,
|
||||
default: 'gen_random_uuid()',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: 'varchar',
|
||||
length: '100',
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
name: 'slug',
|
||||
type: 'varchar',
|
||||
length: '100',
|
||||
isUnique: true,
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
name: 'parent_id',
|
||||
type: 'uuid',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'text',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
name: 'created_at',
|
||||
type: 'timestamp',
|
||||
default: 'now()',
|
||||
},
|
||||
{
|
||||
name: 'updated_at',
|
||||
type: 'timestamp',
|
||||
default: 'now()',
|
||||
},
|
||||
],
|
||||
}),
|
||||
true
|
||||
);
|
||||
|
||||
// Create products table
|
||||
await queryRunner.createTable(
|
||||
new Table({
|
||||
name: 'products',
|
||||
columns: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'uuid',
|
||||
isPrimary: true,
|
||||
default: 'gen_random_uuid()',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: 'varchar',
|
||||
length: '255',
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
name: 'slug',
|
||||
type: 'varchar',
|
||||
length: '255',
|
||||
isUnique: true,
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'text',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
name: 'price',
|
||||
type: 'decimal',
|
||||
precision: 10,
|
||||
scale: 2,
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
name: 'currency',
|
||||
type: 'varchar',
|
||||
length: '3',
|
||||
default: "'USD'",
|
||||
},
|
||||
{
|
||||
name: 'stock_quantity',
|
||||
type: 'integer',
|
||||
default: 0,
|
||||
},
|
||||
{
|
||||
name: 'category_id',
|
||||
type: 'uuid',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
name: 'created_at',
|
||||
type: 'timestamp',
|
||||
default: 'now()',
|
||||
},
|
||||
{
|
||||
name: 'updated_at',
|
||||
type: 'timestamp',
|
||||
default: 'now()',
|
||||
},
|
||||
{
|
||||
name: 'deleted_at',
|
||||
type: 'timestamp',
|
||||
isNullable: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
true
|
||||
);
|
||||
|
||||
// Add foreign keys
|
||||
await queryRunner.createForeignKey(
|
||||
'categories',
|
||||
new TableForeignKey({
|
||||
columnNames: ['parent_id'],
|
||||
referencedColumnNames: ['id'],
|
||||
referencedTableName: 'categories',
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
);
|
||||
|
||||
await queryRunner.createForeignKey(
|
||||
'products',
|
||||
new TableForeignKey({
|
||||
columnNames: ['category_id'],
|
||||
referencedColumnNames: ['id'],
|
||||
referencedTableName: 'categories',
|
||||
onDelete: 'SET NULL',
|
||||
})
|
||||
);
|
||||
|
||||
// Create indexes
|
||||
await queryRunner.createIndex(
|
||||
'products',
|
||||
new TableIndex({
|
||||
name: 'idx_products_category_id',
|
||||
columnNames: ['category_id'],
|
||||
})
|
||||
);
|
||||
|
||||
await queryRunner.createIndex(
|
||||
'products',
|
||||
new TableIndex({
|
||||
name: 'idx_products_slug',
|
||||
columnNames: ['slug'],
|
||||
})
|
||||
);
|
||||
|
||||
await queryRunner.createIndex(
|
||||
'products',
|
||||
new TableIndex({
|
||||
name: 'idx_products_price',
|
||||
columnNames: ['price'],
|
||||
})
|
||||
);
|
||||
|
||||
await queryRunner.createIndex(
|
||||
'categories',
|
||||
new TableIndex({
|
||||
name: 'idx_categories_parent_id',
|
||||
columnNames: ['parent_id'],
|
||||
})
|
||||
);
|
||||
|
||||
// Add check constraints
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE products ADD CONSTRAINT price_positive CHECK (price >= 0)`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE products ADD CONSTRAINT stock_non_negative CHECK (stock_quantity >= 0)`
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
// Drop in reverse order
|
||||
await queryRunner.dropTable('products');
|
||||
await queryRunner.dropTable('categories');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Example: Prisma Migration
|
||||
|
||||
```prisma
|
||||
// prisma/schema.prisma
|
||||
model Category {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
name String @db.VarChar(100)
|
||||
slug String @unique @db.VarChar(100)
|
||||
parentId String? @map("parent_id") @db.Uuid
|
||||
description String? @db.Text
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
parent Category? @relation("CategoryHierarchy", fields: [parentId], references: [id], onDelete: Cascade)
|
||||
children Category[] @relation("CategoryHierarchy")
|
||||
products Product[]
|
||||
|
||||
@@index([parentId])
|
||||
@@index([slug])
|
||||
@@map("categories")
|
||||
}
|
||||
|
||||
model Product {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
name String @db.VarChar(255)
|
||||
slug String @unique @db.VarChar(255)
|
||||
description String? @db.Text
|
||||
price Decimal @db.Decimal(10, 2)
|
||||
currency String @default("USD") @db.VarChar(3)
|
||||
stockQuantity Int @default(0) @map("stock_quantity")
|
||||
categoryId String? @map("category_id") @db.Uuid
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
deletedAt DateTime? @map("deleted_at")
|
||||
|
||||
category Category? @relation(fields: [categoryId], references: [id], onDelete: SetNull)
|
||||
images ProductImage[]
|
||||
tags ProductTag[]
|
||||
|
||||
@@index([categoryId])
|
||||
@@index([slug])
|
||||
@@index([price])
|
||||
@@index([createdAt(sort: Desc)])
|
||||
@@index([stockQuantity], where: stockQuantity > 0)
|
||||
@@map("products")
|
||||
}
|
||||
|
||||
model ProductImage {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
productId String @map("product_id") @db.Uuid
|
||||
url String @db.VarChar(500)
|
||||
altText String? @map("alt_text") @db.VarChar(255)
|
||||
displayOrder Int @default(0) @map("display_order")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([productId])
|
||||
@@map("product_images")
|
||||
}
|
||||
|
||||
model Tag {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
name String @unique @db.VarChar(50)
|
||||
slug String @unique @db.VarChar(50)
|
||||
|
||||
products ProductTag[]
|
||||
|
||||
@@map("tags")
|
||||
}
|
||||
|
||||
model ProductTag {
|
||||
productId String @map("product_id") @db.Uuid
|
||||
tagId String @map("tag_id") @db.Uuid
|
||||
|
||||
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
||||
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@id([productId, tagId])
|
||||
@@index([productId])
|
||||
@@index([tagId])
|
||||
@@map("product_tags")
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
# Generate migration
|
||||
npx prisma migrate dev --name add_product_catalog
|
||||
|
||||
# Apply migration to production
|
||||
npx prisma migrate deploy
|
||||
```
|
||||
|
||||
### 5. Create/Update Models
|
||||
|
||||
#### TypeORM Models
|
||||
|
||||
```typescript
|
||||
// entities/Product.entity.ts
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
DeleteDateColumn,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
ManyToMany,
|
||||
JoinTable,
|
||||
JoinColumn,
|
||||
Index,
|
||||
Check,
|
||||
} from 'typeorm';
|
||||
import { Category } from './Category.entity';
|
||||
import { ProductImage } from './ProductImage.entity';
|
||||
import { Tag } from './Tag.entity';
|
||||
|
||||
@Entity('products')
|
||||
@Check('"price" >= 0')
|
||||
@Check('"stock_quantity" >= 0')
|
||||
export class Product {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 255 })
|
||||
name: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, unique: true })
|
||||
@Index()
|
||||
slug: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
description: string | null;
|
||||
|
||||
@Column({ type: 'decimal', precision: 10, scale: 2 })
|
||||
@Index()
|
||||
price: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 3, default: 'USD' })
|
||||
currency: string;
|
||||
|
||||
@Column({ type: 'integer', default: 0, name: 'stock_quantity' })
|
||||
stockQuantity: number;
|
||||
|
||||
@Column({ type: 'uuid', name: 'category_id', nullable: true })
|
||||
@Index()
|
||||
categoryId: string | null;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
@Index()
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt: Date;
|
||||
|
||||
@DeleteDateColumn({ name: 'deleted_at' })
|
||||
deletedAt: Date | null;
|
||||
|
||||
// Relations
|
||||
@ManyToOne(() => Category, (category) => category.products, {
|
||||
onDelete: 'SET NULL',
|
||||
})
|
||||
@JoinColumn({ name: 'category_id' })
|
||||
category: Category;
|
||||
|
||||
@OneToMany(() => ProductImage, (image) => image.product, {
|
||||
cascade: true,
|
||||
})
|
||||
images: ProductImage[];
|
||||
|
||||
@ManyToMany(() => Tag, (tag) => tag.products)
|
||||
@JoinTable({
|
||||
name: 'product_tags',
|
||||
joinColumn: { name: 'product_id', referencedColumnName: 'id' },
|
||||
inverseJoinColumn: { name: 'tag_id', referencedColumnName: 'id' },
|
||||
})
|
||||
tags: Tag[];
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// entities/Category.entity.ts
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
JoinColumn,
|
||||
Index,
|
||||
} from 'typeorm';
|
||||
import { Product } from './Product.entity';
|
||||
|
||||
@Entity('categories')
|
||||
export class Category {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 100 })
|
||||
name: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 100, unique: true })
|
||||
@Index()
|
||||
slug: string;
|
||||
|
||||
@Column({ type: 'uuid', name: 'parent_id', nullable: true })
|
||||
@Index()
|
||||
parentId: string | null;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
description: string | null;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt: Date;
|
||||
|
||||
// Relations
|
||||
@ManyToOne(() => Category, (category) => category.children, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn({ name: 'parent_id' })
|
||||
parent: Category | null;
|
||||
|
||||
@OneToMany(() => Category, (category) => category.parent)
|
||||
children: Category[];
|
||||
|
||||
@OneToMany(() => Product, (product) => product.category)
|
||||
products: Product[];
|
||||
}
|
||||
```
|
||||
|
||||
#### Validation
|
||||
|
||||
Add validation decorators if using class-validator:
|
||||
|
||||
```typescript
|
||||
import { IsString, IsNumber, Min, IsOptional, IsUUID, MaxLength, Matches } from 'class-validator';
|
||||
|
||||
export class CreateProductDto {
|
||||
@IsString()
|
||||
@MaxLength(255)
|
||||
name: string;
|
||||
|
||||
@IsString()
|
||||
@MaxLength(255)
|
||||
@Matches(/^[a-z0-9-]+$/, { message: 'Slug must contain only lowercase letters, numbers, and hyphens' })
|
||||
slug: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
description?: string;
|
||||
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
price: number;
|
||||
|
||||
@IsString()
|
||||
@MaxLength(3)
|
||||
@IsOptional()
|
||||
currency?: string;
|
||||
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
stockQuantity: number;
|
||||
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
categoryId?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Test Database Operations
|
||||
|
||||
```typescript
|
||||
// entities/__tests__/Product.entity.test.ts
|
||||
import { DataSource } from 'typeorm';
|
||||
import { Product } from '../Product.entity';
|
||||
import { Category } from '../Category.entity';
|
||||
import { createTestDataSource } from '../../test/utils';
|
||||
|
||||
describe('Product Entity', () => {
|
||||
let dataSource: DataSource;
|
||||
let productRepository: ReturnType<typeof dataSource.getRepository<Product>>;
|
||||
let categoryRepository: ReturnType<typeof dataSource.getRepository<Category>>;
|
||||
|
||||
beforeAll(async () => {
|
||||
dataSource = await createTestDataSource();
|
||||
productRepository = dataSource.getRepository(Product);
|
||||
categoryRepository = dataSource.getRepository(Category);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await dataSource.destroy();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await productRepository.delete({});
|
||||
await categoryRepository.delete({});
|
||||
});
|
||||
|
||||
describe('Creation', () => {
|
||||
it('should create product with valid data', async () => {
|
||||
const product = productRepository.create({
|
||||
name: 'Test Product',
|
||||
slug: 'test-product',
|
||||
price: 99.99,
|
||||
stockQuantity: 10,
|
||||
});
|
||||
|
||||
await productRepository.save(product);
|
||||
|
||||
expect(product.id).toBeDefined();
|
||||
expect(product.name).toBe('Test Product');
|
||||
expect(product.price).toBe(99.99);
|
||||
});
|
||||
|
||||
it('should enforce unique slug constraint', async () => {
|
||||
await productRepository.save({
|
||||
name: 'Product 1',
|
||||
slug: 'duplicate-slug',
|
||||
price: 10,
|
||||
stockQuantity: 1,
|
||||
});
|
||||
|
||||
await expect(
|
||||
productRepository.save({
|
||||
name: 'Product 2',
|
||||
slug: 'duplicate-slug',
|
||||
price: 20,
|
||||
stockQuantity: 2,
|
||||
})
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should enforce price check constraint', async () => {
|
||||
await expect(
|
||||
productRepository.save({
|
||||
name: 'Invalid Product',
|
||||
slug: 'invalid-price',
|
||||
price: -10,
|
||||
stockQuantity: 1,
|
||||
})
|
||||
).rejects.toThrow(/price_positive/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Relations', () => {
|
||||
it('should set category relationship', async () => {
|
||||
const category = await categoryRepository.save({
|
||||
name: 'Electronics',
|
||||
slug: 'electronics',
|
||||
});
|
||||
|
||||
const product = await productRepository.save({
|
||||
name: 'Laptop',
|
||||
slug: 'laptop',
|
||||
price: 999,
|
||||
stockQuantity: 5,
|
||||
categoryId: category.id,
|
||||
});
|
||||
|
||||
const loaded = await productRepository.findOne({
|
||||
where: { id: product.id },
|
||||
relations: ['category'],
|
||||
});
|
||||
|
||||
expect(loaded?.category?.name).toBe('Electronics');
|
||||
});
|
||||
|
||||
it('should cascade delete images', async () => {
|
||||
const product = await productRepository.save({
|
||||
name: 'Product with Images',
|
||||
slug: 'product-images',
|
||||
price: 50,
|
||||
stockQuantity: 1,
|
||||
images: [
|
||||
{ url: 'https://example.com/image1.jpg', displayOrder: 0 },
|
||||
{ url: 'https://example.com/image2.jpg', displayOrder: 1 },
|
||||
],
|
||||
});
|
||||
|
||||
await productRepository.delete(product.id);
|
||||
|
||||
// Images should be deleted automatically
|
||||
// Verify by checking the images table is empty
|
||||
});
|
||||
});
|
||||
|
||||
describe('Soft Delete', () => {
|
||||
it('should soft delete product', async () => {
|
||||
const product = await productRepository.save({
|
||||
name: 'Product to Delete',
|
||||
slug: 'product-delete',
|
||||
price: 10,
|
||||
stockQuantity: 1,
|
||||
});
|
||||
|
||||
await productRepository.softDelete(product.id);
|
||||
|
||||
const found = await productRepository.findOne({
|
||||
where: { id: product.id },
|
||||
});
|
||||
|
||||
expect(found).toBeNull();
|
||||
|
||||
// Can still find with withDeleted
|
||||
const deleted = await productRepository.findOne({
|
||||
where: { id: product.id },
|
||||
withDeleted: true,
|
||||
});
|
||||
|
||||
expect(deleted).toBeDefined();
|
||||
expect(deleted?.deletedAt).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Queries', () => {
|
||||
beforeEach(async () => {
|
||||
// Seed test data
|
||||
await productRepository.save([
|
||||
{ name: 'Product A', slug: 'product-a', price: 10, stockQuantity: 5 },
|
||||
{ name: 'Product B', slug: 'product-b', price: 20, stockQuantity: 0 },
|
||||
{ name: 'Product C', slug: 'product-c', price: 30, stockQuantity: 10 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should find products by price range', async () => {
|
||||
const products = await productRepository.find({
|
||||
where: {
|
||||
price: Between(15, 35),
|
||||
},
|
||||
});
|
||||
|
||||
expect(products).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should find in-stock products', async () => {
|
||||
const products = await productRepository
|
||||
.createQueryBuilder('product')
|
||||
.where('product.stock_quantity > 0')
|
||||
.getMany();
|
||||
|
||||
expect(products).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should order by created date', async () => {
|
||||
const products = await productRepository.find({
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
|
||||
expect(products[0].name).toBe('Product C');
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Output Format
|
||||
|
||||
```markdown
|
||||
# Database Layer: {Feature Name}
|
||||
|
||||
## Schema Design
|
||||
|
||||
### Tables Created/Modified
|
||||
- {table_name}: {description}
|
||||
|
||||
### Relationships
|
||||
- {relationship_description}
|
||||
|
||||
### Indexes
|
||||
- {index_name}: {purpose}
|
||||
|
||||
## Migration Scripts
|
||||
|
||||
### Up Migration
|
||||
\`\`\`sql
|
||||
{migration_sql}
|
||||
\`\`\`
|
||||
|
||||
### Down Migration
|
||||
\`\`\`sql
|
||||
{rollback_sql}
|
||||
\`\`\`
|
||||
|
||||
## Models/Entities
|
||||
|
||||
### {ModelName}
|
||||
\`\`\`typescript
|
||||
{model_code}
|
||||
\`\`\`
|
||||
|
||||
## Validation
|
||||
|
||||
### DTOs
|
||||
\`\`\`typescript
|
||||
{validation_code}
|
||||
\`\`\`
|
||||
|
||||
## Testing
|
||||
|
||||
### Test Results
|
||||
- {test_description}: {status}
|
||||
|
||||
## Migration Commands
|
||||
|
||||
\`\`\`bash
|
||||
# Run migration
|
||||
{migration_command}
|
||||
|
||||
# Rollback migration
|
||||
{rollback_command}
|
||||
\`\`\`
|
||||
|
||||
## Performance Considerations
|
||||
- {performance_note}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
- If ORM unclear: Ask which ORM is used or detect from codebase
|
||||
- If database type unclear: Suggest common options or auto-detect
|
||||
- If migration fails: Provide rollback instructions
|
||||
- If constraints fail: Explain the constraint and suggest fixes
|
||||
649
commands/feature/frontend.md
Normal file
649
commands/feature/frontend.md
Normal file
@@ -0,0 +1,649 @@
|
||||
# Frontend Layer Operation
|
||||
|
||||
Implement frontend layer only: components, state management, API integration, and tests for a feature.
|
||||
|
||||
## Parameters
|
||||
|
||||
**Received**: `$ARGUMENTS` (after removing 'frontend' operation name)
|
||||
|
||||
Expected format: `description:"UI functionality needed" [framework:"react|vue|angular"] [state:"redux|zustand|context"] [tests:"unit|integration|e2e"]`
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Understand Frontend Requirements
|
||||
|
||||
Clarify:
|
||||
- What UI components are needed?
|
||||
- What user interactions are supported?
|
||||
- What state management is required?
|
||||
- What API endpoints to consume?
|
||||
- What responsive/accessibility requirements?
|
||||
|
||||
### 2. Analyze Existing Frontend Structure
|
||||
|
||||
```bash
|
||||
# Find frontend framework
|
||||
cat package.json | grep -E "(react|vue|angular|svelte)"
|
||||
|
||||
# Find component structure
|
||||
find . -path "*/components/*" -o -path "*/src/app/*"
|
||||
|
||||
# Find state management
|
||||
cat package.json | grep -E "(redux|zustand|mobx|pinia|ngrx)"
|
||||
```
|
||||
|
||||
### 3. Implement Components
|
||||
|
||||
#### Component Structure Example (React + TypeScript)
|
||||
|
||||
```typescript
|
||||
// features/products/components/ProductCard.tsx
|
||||
import React from 'react';
|
||||
import { Product } from '../types';
|
||||
|
||||
interface ProductCardProps {
|
||||
product: Product;
|
||||
onAddToCart?: (productId: string) => void;
|
||||
onViewDetails?: (productId: string) => void;
|
||||
}
|
||||
|
||||
export const ProductCard: React.FC<ProductCardProps> = ({
|
||||
product,
|
||||
onAddToCart,
|
||||
onViewDetails,
|
||||
}) => {
|
||||
const [imageError, setImageError] = React.useState(false);
|
||||
|
||||
const handleAddToCart = () => {
|
||||
if (onAddToCart) {
|
||||
onAddToCart(product.id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewDetails = () => {
|
||||
if (onViewDetails) {
|
||||
onViewDetails(product.id);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="product-card" role="article" aria-label={`Product: ${product.name}`}>
|
||||
<div className="product-card__image-container">
|
||||
{!imageError && product.images[0] ? (
|
||||
<img
|
||||
src={product.images[0].url}
|
||||
alt={product.images[0].altText || product.name}
|
||||
className="product-card__image"
|
||||
onError={() => setImageError(true)}
|
||||
loading="lazy"
|
||||
/>
|
||||
) : (
|
||||
<div className="product-card__placeholder">No image</div>
|
||||
)}
|
||||
|
||||
{product.stockQuantity === 0 && (
|
||||
<div className="product-card__badge product-card__badge--out-of-stock">
|
||||
Out of Stock
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="product-card__content">
|
||||
<h3 className="product-card__title">{product.name}</h3>
|
||||
|
||||
{product.description && (
|
||||
<p className="product-card__description">
|
||||
{product.description.slice(0, 100)}
|
||||
{product.description.length > 100 && '...'}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="product-card__footer">
|
||||
<div className="product-card__price">
|
||||
{product.currency} {product.price.toFixed(2)}
|
||||
</div>
|
||||
|
||||
<div className="product-card__actions">
|
||||
<button
|
||||
onClick={handleViewDetails}
|
||||
className="button button--secondary"
|
||||
aria-label={`View details for ${product.name}`}
|
||||
>
|
||||
Details
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleAddToCart}
|
||||
disabled={product.stockQuantity === 0}
|
||||
className="button button--primary"
|
||||
aria-label={`Add ${product.name} to cart`}
|
||||
>
|
||||
Add to Cart
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
```typescript
|
||||
// features/products/components/ProductList.tsx
|
||||
import React from 'react';
|
||||
import { ProductCard } from './ProductCard';
|
||||
import { useProducts } from '../hooks/useProducts';
|
||||
import { Pagination } from '@/components/Pagination';
|
||||
import { LoadingSpinner } from '@/components/LoadingSpinner';
|
||||
import { ErrorMessage } from '@/components/ErrorMessage';
|
||||
|
||||
interface ProductListProps {
|
||||
categoryId?: string;
|
||||
searchQuery?: string;
|
||||
}
|
||||
|
||||
export const ProductList: React.FC<ProductListProps> = ({
|
||||
categoryId,
|
||||
searchQuery,
|
||||
}) => {
|
||||
const {
|
||||
products,
|
||||
isLoading,
|
||||
error,
|
||||
pagination,
|
||||
onPageChange,
|
||||
onAddToCart,
|
||||
} = useProducts({ categoryId, searchQuery });
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<ErrorMessage
|
||||
message={error.message}
|
||||
onRetry={() => window.location.reload()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (products.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-gray-500">No products found.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="product-list">
|
||||
<div className="product-list__grid">
|
||||
{products.map((product) => (
|
||||
<ProductCard
|
||||
key={product.id}
|
||||
product={product}
|
||||
onAddToCart={onAddToCart}
|
||||
onViewDetails={(id) => console.log('View', id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{pagination && (
|
||||
<Pagination
|
||||
currentPage={pagination.page}
|
||||
totalPages={pagination.totalPages}
|
||||
onPageChange={onPageChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 4. Implement State Management
|
||||
|
||||
#### Using Zustand
|
||||
|
||||
```typescript
|
||||
// features/products/store/productStore.ts
|
||||
import { create } from 'zustand';
|
||||
import { devtools, persist } from 'zustand/middleware';
|
||||
import { productApi } from '../api/productApi';
|
||||
import { Product } from '../types';
|
||||
|
||||
interface ProductState {
|
||||
products: Product[];
|
||||
selectedProduct: Product | null;
|
||||
isLoading: boolean;
|
||||
error: Error | null;
|
||||
|
||||
fetchProducts: (filters?: any) => Promise<void>;
|
||||
fetchProduct: (id: string) => Promise<void>;
|
||||
createProduct: (data: any) => Promise<void>;
|
||||
updateProduct: (id: string, data: any) => Promise<void>;
|
||||
deleteProduct: (id: string) => Promise<void>;
|
||||
clearError: () => void;
|
||||
}
|
||||
|
||||
export const useProductStore = create<ProductState>()(
|
||||
devtools(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
products: [],
|
||||
selectedProduct: null,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
|
||||
fetchProducts: async (filters = {}) => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const response = await productApi.list(filters);
|
||||
set({ products: response.data, isLoading: false });
|
||||
} catch (error: any) {
|
||||
set({ error, isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
fetchProduct: async (id: string) => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const product = await productApi.getById(id);
|
||||
set({ selectedProduct: product, isLoading: false });
|
||||
} catch (error: any) {
|
||||
set({ error, isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
createProduct: async (data) => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const product = await productApi.create(data);
|
||||
set((state) => ({
|
||||
products: [...state.products, product],
|
||||
isLoading: false,
|
||||
}));
|
||||
} catch (error: any) {
|
||||
set({ error, isLoading: false });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
updateProduct: async (id, data) => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const product = await productApi.update(id, data);
|
||||
set((state) => ({
|
||||
products: state.products.map((p) =>
|
||||
p.id === id ? product : p
|
||||
),
|
||||
selectedProduct:
|
||||
state.selectedProduct?.id === id
|
||||
? product
|
||||
: state.selectedProduct,
|
||||
isLoading: false,
|
||||
}));
|
||||
} catch (error: any) {
|
||||
set({ error, isLoading: false });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
deleteProduct: async (id) => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
await productApi.delete(id);
|
||||
set((state) => ({
|
||||
products: state.products.filter((p) => p.id !== id),
|
||||
isLoading: false,
|
||||
}));
|
||||
} catch (error: any) {
|
||||
set({ error, isLoading: false });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
clearError: () => set({ error: null }),
|
||||
}),
|
||||
{
|
||||
name: 'product-storage',
|
||||
partialize: (state) => ({ products: state.products }),
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
### 5. Implement API Integration
|
||||
|
||||
```typescript
|
||||
// features/products/api/productApi.ts
|
||||
import axios from 'axios';
|
||||
import { Product, ProductFilters, PaginatedResponse } from '../types';
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_API_URL || '/api';
|
||||
|
||||
const apiClient = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// Request interceptor
|
||||
apiClient.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('accessToken');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
// Response interceptor
|
||||
apiClient.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error) => {
|
||||
if (error.response?.status === 401) {
|
||||
// Handle unauthorized
|
||||
localStorage.removeItem('accessToken');
|
||||
window.location.href = '/login';
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
);
|
||||
|
||||
export const productApi = {
|
||||
list: async (filters: ProductFilters): Promise<PaginatedResponse<Product>> => {
|
||||
const response = await apiClient.get('/products', { params: filters });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getById: async (id: string): Promise<Product> => {
|
||||
const response = await apiClient.get(`/products/${id}`);
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
create: async (data: Partial<Product>): Promise<Product> => {
|
||||
const response = await apiClient.post('/products', data);
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
update: async (id: string, data: Partial<Product>): Promise<Product> => {
|
||||
const response = await apiClient.put(`/products/${id}`, data);
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
delete: async (id: string): Promise<void> => {
|
||||
await apiClient.delete(`/products/${id}`);
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### 6. Create Custom Hooks
|
||||
|
||||
```typescript
|
||||
// features/products/hooks/useProducts.ts
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { productApi } from '../api/productApi';
|
||||
import { Product, ProductFilters } from '../types';
|
||||
|
||||
interface UseProductsOptions {
|
||||
categoryId?: string;
|
||||
searchQuery?: string;
|
||||
autoFetch?: boolean;
|
||||
}
|
||||
|
||||
export const useProducts = (options: UseProductsOptions = {}) => {
|
||||
const [products, setProducts] = useState<Product[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [pagination, setPagination] = useState({
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const fetchProducts = useCallback(
|
||||
async (page: number = 1) => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const filters: ProductFilters = {
|
||||
page,
|
||||
limit: 20,
|
||||
categoryId: options.categoryId,
|
||||
search: options.searchQuery,
|
||||
};
|
||||
|
||||
const response = await productApi.list(filters);
|
||||
|
||||
setProducts(response.data);
|
||||
setPagination({
|
||||
page: response.meta.page,
|
||||
totalPages: response.meta.totalPages,
|
||||
total: response.meta.total,
|
||||
});
|
||||
} catch (err: any) {
|
||||
setError(err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[options.categoryId, options.searchQuery]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (options.autoFetch !== false) {
|
||||
fetchProducts();
|
||||
}
|
||||
}, [fetchProducts, options.autoFetch]);
|
||||
|
||||
const onPageChange = useCallback(
|
||||
(page: number) => {
|
||||
fetchProducts(page);
|
||||
},
|
||||
[fetchProducts]
|
||||
);
|
||||
|
||||
const onAddToCart = useCallback((productId: string) => {
|
||||
// Implement add to cart logic
|
||||
console.log('Add to cart:', productId);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
products,
|
||||
isLoading,
|
||||
error,
|
||||
pagination,
|
||||
fetchProducts,
|
||||
onPageChange,
|
||||
onAddToCart,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### 7. Write Tests
|
||||
|
||||
```typescript
|
||||
// features/products/components/__tests__/ProductCard.test.tsx
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { ProductCard } from '../ProductCard';
|
||||
|
||||
const mockProduct = {
|
||||
id: '1',
|
||||
name: 'Test Product',
|
||||
description: 'Test description',
|
||||
price: 99.99,
|
||||
currency: 'USD',
|
||||
stockQuantity: 10,
|
||||
images: [{ url: 'https://example.com/image.jpg', altText: 'Product image' }],
|
||||
};
|
||||
|
||||
describe('ProductCard', () => {
|
||||
it('should render product information', () => {
|
||||
render(<ProductCard product={mockProduct} />);
|
||||
|
||||
expect(screen.getByText('Test Product')).toBeInTheDocument();
|
||||
expect(screen.getByText(/Test description/)).toBeInTheDocument();
|
||||
expect(screen.getByText('USD 99.99')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call onAddToCart when button clicked', () => {
|
||||
const onAddToCart = jest.fn();
|
||||
render(<ProductCard product={mockProduct} onAddToCart={onAddToCart} />);
|
||||
|
||||
const addButton = screen.getByRole('button', { name: /add to cart/i });
|
||||
fireEvent.click(addButton);
|
||||
|
||||
expect(onAddToCart).toHaveBeenCalledWith('1');
|
||||
});
|
||||
|
||||
it('should disable add to cart button when out of stock', () => {
|
||||
const outOfStockProduct = { ...mockProduct, stockQuantity: 0 };
|
||||
render(<ProductCard product={outOfStockProduct} />);
|
||||
|
||||
const addButton = screen.getByRole('button', { name: /add to cart/i });
|
||||
expect(addButton).toBeDisabled();
|
||||
expect(screen.getByText('Out of Stock')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle image load error', () => {
|
||||
render(<ProductCard product={mockProduct} />);
|
||||
|
||||
const image = screen.getByRole('img');
|
||||
fireEvent.error(image);
|
||||
|
||||
expect(screen.getByText('No image')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
```typescript
|
||||
// features/products/hooks/__tests__/useProducts.test.ts
|
||||
import { renderHook, act, waitFor } from '@testing-library/react';
|
||||
import { useProducts } from '../useProducts';
|
||||
import { productApi } from '../../api/productApi';
|
||||
|
||||
jest.mock('../../api/productApi');
|
||||
|
||||
const mockProductApi = productApi as jest.Mocked<typeof productApi>;
|
||||
|
||||
describe('useProducts', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should fetch products on mount', async () => {
|
||||
mockProductApi.list.mockResolvedValue({
|
||||
data: [{ id: '1', name: 'Product 1' }],
|
||||
meta: { page: 1, totalPages: 1, total: 1 },
|
||||
} as any);
|
||||
|
||||
const { result } = renderHook(() => useProducts());
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.products).toHaveLength(1);
|
||||
expect(mockProductApi.list).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle fetch error', async () => {
|
||||
const error = new Error('Fetch failed');
|
||||
mockProductApi.list.mockRejectedValue(error);
|
||||
|
||||
const { result } = renderHook(() => useProducts());
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.error).toEqual(error);
|
||||
});
|
||||
|
||||
it('should refetch on page change', async () => {
|
||||
mockProductApi.list.mockResolvedValue({
|
||||
data: [],
|
||||
meta: { page: 1, totalPages: 2, total: 20 },
|
||||
} as any);
|
||||
|
||||
const { result } = renderHook(() => useProducts());
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.onPageChange(2);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockProductApi.list).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ page: 2 })
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Output Format
|
||||
|
||||
```markdown
|
||||
# Frontend Layer: {Feature Name}
|
||||
|
||||
## Components
|
||||
|
||||
### {ComponentName}
|
||||
- Purpose: {description}
|
||||
- Props: {props_list}
|
||||
- State: {state_description}
|
||||
- Code: {component_code}
|
||||
|
||||
## State Management
|
||||
|
||||
### Store/Context
|
||||
\`\`\`typescript
|
||||
{state_management_code}
|
||||
\`\`\`
|
||||
|
||||
## API Integration
|
||||
|
||||
### API Client
|
||||
\`\`\`typescript
|
||||
{api_client_code}
|
||||
\`\`\`
|
||||
|
||||
## Custom Hooks
|
||||
|
||||
### {HookName}
|
||||
\`\`\`typescript
|
||||
{hook_code}
|
||||
\`\`\`
|
||||
|
||||
## Testing
|
||||
|
||||
### Component Tests
|
||||
- {test_description}: {status}
|
||||
|
||||
### Hook Tests
|
||||
- {test_description}: {status}
|
||||
|
||||
## Accessibility
|
||||
- {a11y_considerations}
|
||||
|
||||
## Performance
|
||||
- {performance_optimizations}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
- If framework unclear: Detect from package.json or ask
|
||||
- If state management unclear: Suggest options
|
||||
- Provide examples for detected framework
|
||||
2293
commands/feature/implement.md
Normal file
2293
commands/feature/implement.md
Normal file
File diff suppressed because it is too large
Load Diff
722
commands/feature/integrate.md
Normal file
722
commands/feature/integrate.md
Normal file
@@ -0,0 +1,722 @@
|
||||
# Integration & Polish Operation
|
||||
|
||||
Complete integration testing, performance optimization, security hardening, and documentation for a feature.
|
||||
|
||||
## Parameters
|
||||
|
||||
**Received**: `$ARGUMENTS` (after removing 'integrate' operation name)
|
||||
|
||||
Expected format: `feature:"feature name" [scope:"e2e|performance|security|documentation"] [priority:"high|medium|low"]`
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. End-to-End Testing
|
||||
|
||||
Create comprehensive E2E tests covering critical user workflows.
|
||||
|
||||
#### Using Playwright
|
||||
|
||||
```typescript
|
||||
// e2e/products.spec.ts
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Product Management', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('http://localhost:3000');
|
||||
});
|
||||
|
||||
test('should complete full product browsing flow', async ({ page }) => {
|
||||
// Navigate to products page
|
||||
await page.click('text=Products');
|
||||
await expect(page).toHaveURL(/\/products/);
|
||||
|
||||
// Verify products are loaded
|
||||
await expect(page.locator('.product-card')).toHaveCount(20, { timeout: 10000 });
|
||||
|
||||
// Filter by category
|
||||
await page.click('text=Electronics');
|
||||
await expect(page.locator('.product-card')).toHaveCount(5);
|
||||
|
||||
// Search for product
|
||||
await page.fill('input[placeholder="Search products"]', 'laptop');
|
||||
await page.keyboard.press('Enter');
|
||||
await expect(page.locator('.product-card')).toHaveCount(2);
|
||||
|
||||
// Click on first product
|
||||
await page.click('.product-card:first-child');
|
||||
await expect(page).toHaveURL(/\/products\/[a-z0-9-]+/);
|
||||
|
||||
// Verify product details
|
||||
await expect(page.locator('h1')).toContainText('Laptop');
|
||||
await expect(page.locator('.product-price')).toBeVisible();
|
||||
|
||||
// Add to cart
|
||||
await page.click('button:has-text("Add to Cart")');
|
||||
await expect(page.locator('.cart-badge')).toContainText('1');
|
||||
});
|
||||
|
||||
test('should handle error states gracefully', async ({ page }) => {
|
||||
// Simulate network error
|
||||
await page.route('**/api/products', (route) => route.abort());
|
||||
|
||||
await page.goto('http://localhost:3000/products');
|
||||
|
||||
// Should show error message
|
||||
await expect(page.locator('text=Failed to load products')).toBeVisible();
|
||||
|
||||
// Should have retry button
|
||||
await expect(page.locator('button:has-text("Retry")')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should handle authentication flow', async ({ page }) => {
|
||||
// Try to create product without auth
|
||||
await page.goto('http://localhost:3000/products/new');
|
||||
|
||||
// Should redirect to login
|
||||
await expect(page).toHaveURL(/\/login/);
|
||||
|
||||
// Login
|
||||
await page.fill('input[name="email"]', 'admin@example.com');
|
||||
await page.fill('input[name="password"]', 'Password123');
|
||||
await page.click('button:has-text("Login")');
|
||||
|
||||
// Should redirect back to product creation
|
||||
await expect(page).toHaveURL(/\/products\/new/);
|
||||
|
||||
// Create product
|
||||
await page.fill('input[name="name"]', 'New Test Product');
|
||||
await page.fill('textarea[name="description"]', 'Test description');
|
||||
await page.fill('input[name="price"]', '99.99');
|
||||
await page.fill('input[name="stockQuantity"]', '10');
|
||||
|
||||
await page.click('button:has-text("Create Product")');
|
||||
|
||||
// Should show success message
|
||||
await expect(page.locator('text=Product created successfully')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should be accessible', async ({ page }) => {
|
||||
await page.goto('http://localhost:3000/products');
|
||||
|
||||
// Check for proper heading hierarchy
|
||||
const h1 = await page.locator('h1').count();
|
||||
expect(h1).toBeGreaterThan(0);
|
||||
|
||||
// Check for alt text on images
|
||||
const images = page.locator('img');
|
||||
const count = await images.count();
|
||||
for (let i = 0; i < count; i++) {
|
||||
const alt = await images.nth(i).getAttribute('alt');
|
||||
expect(alt).toBeTruthy();
|
||||
}
|
||||
|
||||
// Check for keyboard navigation
|
||||
await page.keyboard.press('Tab');
|
||||
const focusedElement = await page.evaluate(() => document.activeElement?.tagName);
|
||||
expect(focusedElement).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should work on mobile devices', async ({ page, viewport }) => {
|
||||
// Set mobile viewport
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
|
||||
await page.goto('http://localhost:3000/products');
|
||||
|
||||
// Mobile menu should be visible
|
||||
await expect(page.locator('[aria-label="Menu"]')).toBeVisible();
|
||||
|
||||
// Products should be in single column
|
||||
const gridColumns = await page.locator('.product-grid').evaluate((el) => {
|
||||
return window.getComputedStyle(el).gridTemplateColumns.split(' ').length;
|
||||
});
|
||||
|
||||
expect(gridColumns).toBe(1);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 2. Performance Optimization
|
||||
|
||||
#### Frontend Performance
|
||||
|
||||
```typescript
|
||||
// Performance monitoring
|
||||
import { onCLS, onFID, onLCP, onFCP, onTTFB } from 'web-vitals';
|
||||
|
||||
function sendToAnalytics(metric) {
|
||||
console.log(metric);
|
||||
// Send to analytics service
|
||||
}
|
||||
|
||||
onCLS(sendToAnalytics);
|
||||
onFID(sendToAnalytics);
|
||||
onLCP(sendToAnalytics);
|
||||
onFCP(sendToAnalytics);
|
||||
onTTFB(sendToAnalytics);
|
||||
|
||||
// Code splitting
|
||||
const ProductList = React.lazy(() => import('./features/products/components/ProductList'));
|
||||
const ProductDetail = React.lazy(() => import('./features/products/components/ProductDetail'));
|
||||
|
||||
// Image optimization
|
||||
<img
|
||||
src={product.images[0].url}
|
||||
srcSet={`
|
||||
${product.images[0].url}?w=320 320w,
|
||||
${product.images[0].url}?w=640 640w,
|
||||
${product.images[0].url}?w=1024 1024w
|
||||
`}
|
||||
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
|
||||
// Memoization
|
||||
const MemoizedProductCard = React.memo(ProductCard, (prevProps, nextProps) => {
|
||||
return prevProps.product.id === nextProps.product.id &&
|
||||
prevProps.product.stockQuantity === nextProps.product.stockQuantity;
|
||||
});
|
||||
|
||||
// Virtualization for long lists
|
||||
import { FixedSizeList } from 'react-window';
|
||||
|
||||
const ProductVirtualList = ({ products }) => (
|
||||
<FixedSizeList
|
||||
height={600}
|
||||
itemCount={products.length}
|
||||
itemSize={200}
|
||||
width="100%"
|
||||
>
|
||||
{({ index, style }) => (
|
||||
<div style={style}>
|
||||
<ProductCard product={products[index]} />
|
||||
</div>
|
||||
)}
|
||||
</FixedSizeList>
|
||||
);
|
||||
```
|
||||
|
||||
#### Backend Performance
|
||||
|
||||
```typescript
|
||||
// Database query optimization
|
||||
// Add indexes (already in database.md)
|
||||
|
||||
// Query result caching
|
||||
import { Redis } from 'ioredis';
|
||||
const redis = new Redis();
|
||||
|
||||
async function getCachedProducts(filters: ProductFilters) {
|
||||
const cacheKey = `products:${JSON.stringify(filters)}`;
|
||||
const cached = await redis.get(cacheKey);
|
||||
|
||||
if (cached) {
|
||||
return JSON.parse(cached);
|
||||
}
|
||||
|
||||
const products = await productRepository.findAll(filters, pagination);
|
||||
await redis.setex(cacheKey, 300, JSON.stringify(products)); // 5 minutes
|
||||
|
||||
return products;
|
||||
}
|
||||
|
||||
// N+1 query prevention
|
||||
const products = await productRepository.find({
|
||||
relations: ['category', 'images', 'tags'], // Eager load
|
||||
});
|
||||
|
||||
// Response compression
|
||||
import compression from 'compression';
|
||||
app.use(compression());
|
||||
|
||||
// Connection pooling (already configured in database setup)
|
||||
|
||||
// API response caching
|
||||
import apicache from 'apicache';
|
||||
app.use('/api/products', apicache.middleware('5 minutes'));
|
||||
```
|
||||
|
||||
### 3. Security Hardening
|
||||
|
||||
#### Input Validation & Sanitization
|
||||
|
||||
```typescript
|
||||
// Backend validation (already in backend.md with Zod)
|
||||
|
||||
// SQL Injection prevention (using parameterized queries with TypeORM)
|
||||
|
||||
// XSS Prevention
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
function sanitizeHtml(dirty: string): string {
|
||||
return DOMPurify.sanitize(dirty);
|
||||
}
|
||||
|
||||
// In component
|
||||
<div dangerouslySetInnerHTML={{ __html: sanitizeHtml(product.description) }} />
|
||||
```
|
||||
|
||||
#### Security Headers
|
||||
|
||||
```typescript
|
||||
// helmet middleware
|
||||
import helmet from 'helmet';
|
||||
|
||||
app.use(helmet());
|
||||
|
||||
app.use(helmet.contentSecurityPolicy({
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'"],
|
||||
scriptSrc: ["'self'"],
|
||||
imgSrc: ["'self'", 'data:', 'https:'],
|
||||
connectSrc: ["'self'", process.env.API_URL],
|
||||
},
|
||||
}));
|
||||
|
||||
// CORS configuration
|
||||
import cors from 'cors';
|
||||
|
||||
app.use(cors({
|
||||
origin: process.env.FRONTEND_URL,
|
||||
credentials: true,
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization'],
|
||||
}));
|
||||
```
|
||||
|
||||
#### Rate Limiting
|
||||
|
||||
```typescript
|
||||
import rateLimit from 'express-rate-limit';
|
||||
|
||||
// General API rate limit
|
||||
const apiLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 100, // 100 requests per window
|
||||
message: 'Too many requests from this IP',
|
||||
});
|
||||
|
||||
app.use('/api/', apiLimiter);
|
||||
|
||||
// Stricter rate limit for mutations
|
||||
const createLimiter = rateLimit({
|
||||
windowMs: 60 * 60 * 1000, // 1 hour
|
||||
max: 10, // 10 creates per hour
|
||||
});
|
||||
|
||||
app.use('/api/products', createLimiter);
|
||||
```
|
||||
|
||||
#### Authentication & Authorization
|
||||
|
||||
```typescript
|
||||
// JWT validation middleware (already in backend.md)
|
||||
|
||||
// RBAC (Role-Based Access Control)
|
||||
function authorize(...allowedRoles: string[]) {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
if (!allowedRoles.includes(req.user.role)) {
|
||||
return res.status(403).json({ error: 'Forbidden' });
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
// Usage
|
||||
router.post('/products', authenticate, authorize('admin', 'editor'), createProduct);
|
||||
```
|
||||
|
||||
### 4. Error Handling & Logging
|
||||
|
||||
```typescript
|
||||
// Centralized error handler
|
||||
class AppError extends Error {
|
||||
constructor(
|
||||
public statusCode: number,
|
||||
public message: string,
|
||||
public isOperational = true
|
||||
) {
|
||||
super(message);
|
||||
Object.setPrototypeOf(this, AppError.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
|
||||
if (err instanceof AppError) {
|
||||
return res.status(err.statusCode).json({
|
||||
error: {
|
||||
message: err.message,
|
||||
statusCode: err.statusCode,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Log unexpected errors
|
||||
console.error('Unexpected error:', err);
|
||||
|
||||
res.status(500).json({
|
||||
error: {
|
||||
message: 'Internal server error',
|
||||
statusCode: 500,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// Structured logging
|
||||
import winston from 'winston';
|
||||
|
||||
const logger = winston.createLogger({
|
||||
level: 'info',
|
||||
format: winston.format.json(),
|
||||
transports: [
|
||||
new winston.transports.File({ filename: 'error.log', level: 'error' }),
|
||||
new winston.transports.File({ filename: 'combined.log' }),
|
||||
],
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
logger.add(new winston.transports.Console({
|
||||
format: winston.format.simple(),
|
||||
}));
|
||||
}
|
||||
|
||||
// Request logging
|
||||
import morgan from 'morgan';
|
||||
app.use(morgan('combined', { stream: { write: (msg) => logger.info(msg) } }));
|
||||
```
|
||||
|
||||
### 5. Documentation
|
||||
|
||||
#### API Documentation
|
||||
|
||||
```yaml
|
||||
# openapi.yaml
|
||||
openapi: 3.0.0
|
||||
info:
|
||||
title: Product API
|
||||
version: 1.0.0
|
||||
description: API for managing products
|
||||
|
||||
servers:
|
||||
- url: http://localhost:3000/api
|
||||
description: Local server
|
||||
- url: https://api.example.com
|
||||
description: Production server
|
||||
|
||||
paths:
|
||||
/products:
|
||||
get:
|
||||
summary: List products
|
||||
description: Retrieve a paginated list of products with optional filters
|
||||
parameters:
|
||||
- name: page
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 1
|
||||
- name: limit
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 20
|
||||
- name: categoryId
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
- name: search
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Successful response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Product'
|
||||
meta:
|
||||
type: object
|
||||
properties:
|
||||
page:
|
||||
type: integer
|
||||
totalPages:
|
||||
type: integer
|
||||
total:
|
||||
type: integer
|
||||
|
||||
post:
|
||||
summary: Create product
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CreateProductInput'
|
||||
responses:
|
||||
'201':
|
||||
description: Product created
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Product'
|
||||
'401':
|
||||
description: Unauthorized
|
||||
'400':
|
||||
description: Invalid input
|
||||
|
||||
/products/{id}:
|
||||
get:
|
||||
summary: Get product by ID
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
responses:
|
||||
'200':
|
||||
description: Successful response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Product'
|
||||
'404':
|
||||
description: Product not found
|
||||
|
||||
components:
|
||||
schemas:
|
||||
Product:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
name:
|
||||
type: string
|
||||
slug:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
price:
|
||||
type: number
|
||||
format: decimal
|
||||
currency:
|
||||
type: string
|
||||
stockQuantity:
|
||||
type: integer
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
CreateProductInput:
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
- price
|
||||
- stockQuantity
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
maxLength: 255
|
||||
description:
|
||||
type: string
|
||||
price:
|
||||
type: number
|
||||
minimum: 0
|
||||
stockQuantity:
|
||||
type: integer
|
||||
minimum: 0
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
```
|
||||
|
||||
#### Feature Documentation
|
||||
|
||||
```markdown
|
||||
# Product Management Feature
|
||||
|
||||
## Overview
|
||||
|
||||
Complete product catalog management with support for categories, images, tags, search, and filtering.
|
||||
|
||||
## Features
|
||||
|
||||
- Product listing with pagination
|
||||
- Product search and filtering
|
||||
- Category hierarchy
|
||||
- Multiple product images
|
||||
- Tag management
|
||||
- Stock tracking
|
||||
- Soft delete support
|
||||
|
||||
## User Flows
|
||||
|
||||
### Browsing Products
|
||||
|
||||
1. User navigates to products page
|
||||
2. Products are loaded with pagination (20 per page)
|
||||
3. User can filter by category
|
||||
4. User can search by name/description
|
||||
5. User clicks on product to view details
|
||||
|
||||
### Creating Product (Admin)
|
||||
|
||||
1. Admin logs in
|
||||
2. Admin navigates to "Create Product"
|
||||
3. Admin fills in product details
|
||||
4. Admin uploads product images
|
||||
5. Admin selects category and tags
|
||||
6. Admin submits form
|
||||
7. Product is created and admin is redirected to product page
|
||||
|
||||
## API Usage Examples
|
||||
|
||||
### List Products
|
||||
|
||||
\`\`\`bash
|
||||
curl -X GET "http://localhost:3000/api/products?page=1&limit=20&categoryId=abc123"
|
||||
\`\`\`
|
||||
|
||||
### Create Product
|
||||
|
||||
\`\`\`bash
|
||||
curl -X POST "http://localhost:3000/api/products" \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "New Product",
|
||||
"description": "Product description",
|
||||
"price": 99.99,
|
||||
"stockQuantity": 10,
|
||||
"categoryId": "abc123"
|
||||
}'
|
||||
\`\`\`
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
- API response time: <100ms (p95)
|
||||
- Page load time: <2s (p95)
|
||||
- Database queries: Optimized with indexes
|
||||
- Image loading: Lazy loaded with srcSet
|
||||
- List rendering: Virtualized for 1000+ items
|
||||
|
||||
## Security Measures
|
||||
|
||||
- JWT authentication for mutations
|
||||
- Role-based access control (RBAC)
|
||||
- Input validation on backend
|
||||
- XSS protection with DOMPurify
|
||||
- SQL injection prevention
|
||||
- Rate limiting (100 req/15min)
|
||||
- CORS configured
|
||||
- Security headers with Helmet
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- Maximum 10 images per product
|
||||
- Product names limited to 255 characters
|
||||
- Search limited to name and description
|
||||
- Bulk operations not yet supported
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- [ ] Bulk product import/export
|
||||
- [ ] Product variants (size, color)
|
||||
- [ ] Advanced inventory management
|
||||
- [ ] Product recommendations
|
||||
- [ ] Analytics dashboard
|
||||
```
|
||||
|
||||
## Output Format
|
||||
|
||||
```markdown
|
||||
# Integration & Polish: {Feature Name}
|
||||
|
||||
## E2E Test Results
|
||||
|
||||
### Test Suites
|
||||
- {suite_name}: {passed/failed} ({count} tests)
|
||||
|
||||
### Coverage
|
||||
- User flows covered: {percentage}%
|
||||
- Edge cases tested: {count}
|
||||
|
||||
## Performance Metrics
|
||||
|
||||
### Frontend
|
||||
- LCP: {time}ms
|
||||
- FID: {time}ms
|
||||
- CLS: {score}
|
||||
|
||||
### Backend
|
||||
- API response time (p95): {time}ms
|
||||
- Database query time (p95): {time}ms
|
||||
- Memory usage: {mb}MB
|
||||
|
||||
### Optimizations Applied
|
||||
- {optimization_description}
|
||||
|
||||
## Security Audit
|
||||
|
||||
### Vulnerabilities Fixed
|
||||
- {vulnerability}: {fix_description}
|
||||
|
||||
### Security Measures
|
||||
- {measure_description}
|
||||
|
||||
## Documentation
|
||||
|
||||
### API Documentation
|
||||
- {documentation_location}
|
||||
|
||||
### User Guide
|
||||
- {guide_location}
|
||||
|
||||
### Developer Documentation
|
||||
- {docs_location}
|
||||
|
||||
## Deployment Checklist
|
||||
|
||||
- [ ] All tests passing
|
||||
- [ ] Performance benchmarks met
|
||||
- [ ] Security audit completed
|
||||
- [ ] Documentation updated
|
||||
- [ ] Environment variables documented
|
||||
- [ ] Monitoring configured
|
||||
- [ ] Backup strategy in place
|
||||
|
||||
## Known Issues
|
||||
|
||||
- {issue_description}: {workaround}
|
||||
|
||||
## Next Steps
|
||||
|
||||
- {future_enhancement}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
- If tests fail: Provide failure details and suggested fixes
|
||||
- If performance targets not met: Suggest optimizations
|
||||
- If security issues found: Provide remediation steps
|
||||
798
commands/feature/scaffold.md
Normal file
798
commands/feature/scaffold.md
Normal file
@@ -0,0 +1,798 @@
|
||||
# Scaffold Feature Operation
|
||||
|
||||
Generate boilerplate code structure for a new feature across database, backend, and frontend layers.
|
||||
|
||||
## Parameters
|
||||
|
||||
**Received**: `$ARGUMENTS` (after removing 'scaffold' operation name)
|
||||
|
||||
Expected format: `name:"feature-name" [layers:"database,backend,frontend"] [pattern:"crud|workflow|custom"]`
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Understand Scaffolding Requirements
|
||||
|
||||
Clarify:
|
||||
- What is the feature name?
|
||||
- Which layers need scaffolding?
|
||||
- What pattern does it follow (CRUD, workflow, custom)?
|
||||
- What entity/resource is being managed?
|
||||
|
||||
### 2. Analyze Project Structure
|
||||
|
||||
```bash
|
||||
# Detect project structure
|
||||
ls -la src/
|
||||
|
||||
# Detect ORM
|
||||
cat package.json | grep -E "(prisma|typeorm|sequelize|mongoose)"
|
||||
|
||||
# Detect frontend framework
|
||||
cat package.json | grep -E "(react|vue|angular|svelte)"
|
||||
|
||||
# Detect backend framework
|
||||
cat package.json | grep -E "(express|fastify|nest|koa)"
|
||||
```
|
||||
|
||||
### 3. Generate Database Layer
|
||||
|
||||
#### Migration Scaffold
|
||||
|
||||
```typescript
|
||||
// migrations/TIMESTAMP_add_{feature_name}.ts
|
||||
import { MigrationInterface, QueryRunner, Table, TableIndex, TableForeignKey } from 'typeorm';
|
||||
|
||||
export class Add{FeatureName}{Timestamp} implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.createTable(
|
||||
new Table({
|
||||
name: '{table_name}',
|
||||
columns: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'uuid',
|
||||
isPrimary: true,
|
||||
default: 'gen_random_uuid()',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: 'varchar',
|
||||
length: '255',
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
name: 'created_at',
|
||||
type: 'timestamp',
|
||||
default: 'now()',
|
||||
},
|
||||
{
|
||||
name: 'updated_at',
|
||||
type: 'timestamp',
|
||||
default: 'now()',
|
||||
},
|
||||
],
|
||||
}),
|
||||
true
|
||||
);
|
||||
|
||||
// Add indexes
|
||||
await queryRunner.createIndex(
|
||||
'{table_name}',
|
||||
new TableIndex({
|
||||
name: 'idx_{table_name}_created_at',
|
||||
columnNames: ['created_at'],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.dropTable('{table_name}');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Entity/Model Scaffold
|
||||
|
||||
```typescript
|
||||
// entities/{FeatureName}.entity.ts
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
Index,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity('{table_name}')
|
||||
export class {FeatureName} {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 255 })
|
||||
name: string;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
@Index()
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Generate Backend Layer
|
||||
|
||||
#### Repository Scaffold
|
||||
|
||||
```typescript
|
||||
// repositories/{FeatureName}Repository.ts
|
||||
import { Repository } from 'typeorm';
|
||||
import { {FeatureName} } from '../entities/{FeatureName}.entity';
|
||||
import { AppDataSource } from '../config/database';
|
||||
|
||||
export class {FeatureName}Repository {
|
||||
private repository: Repository<{FeatureName}>;
|
||||
|
||||
constructor() {
|
||||
this.repository = AppDataSource.getRepository({FeatureName});
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<{FeatureName} | null> {
|
||||
return this.repository.findOne({ where: { id } });
|
||||
}
|
||||
|
||||
async findAll(page: number = 1, limit: number = 20): Promise<[{FeatureName}[], number]> {
|
||||
const skip = (page - 1) * limit;
|
||||
return this.repository.findAndCount({
|
||||
skip,
|
||||
take: limit,
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
async create(data: Partial<{FeatureName}>): Promise<{FeatureName}> {
|
||||
const entity = this.repository.create(data);
|
||||
return this.repository.save(entity);
|
||||
}
|
||||
|
||||
async update(id: string, data: Partial<{FeatureName}>): Promise<{FeatureName}> {
|
||||
await this.repository.update(id, data);
|
||||
const updated = await this.findById(id);
|
||||
if (!updated) {
|
||||
throw new Error('{FeatureName} not found after update');
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
await this.repository.delete(id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Service Scaffold
|
||||
|
||||
```typescript
|
||||
// services/{FeatureName}Service.ts
|
||||
import { {FeatureName}Repository } from '../repositories/{FeatureName}Repository';
|
||||
import { {FeatureName} } from '../entities/{FeatureName}.entity';
|
||||
import { NotFoundError, ValidationError } from '../errors';
|
||||
|
||||
export interface Create{FeatureName}Input {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface Update{FeatureName}Input {
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export class {FeatureName}Service {
|
||||
constructor(private repository: {FeatureName}Repository) {}
|
||||
|
||||
async get{FeatureName}(id: string): Promise<{FeatureName}> {
|
||||
const entity = await this.repository.findById(id);
|
||||
if (!entity) {
|
||||
throw new NotFoundError(`{FeatureName} with ID ${id} not found`);
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
|
||||
async list{FeatureName}s(
|
||||
page: number = 1,
|
||||
limit: number = 20
|
||||
): Promise<{ data: {FeatureName}[]; total: number; page: number; totalPages: number }> {
|
||||
const [data, total] = await this.repository.findAll(page, limit);
|
||||
|
||||
return {
|
||||
data,
|
||||
total,
|
||||
page,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
};
|
||||
}
|
||||
|
||||
async create{FeatureName}(input: Create{FeatureName}Input): Promise<{FeatureName}> {
|
||||
this.validateInput(input);
|
||||
return this.repository.create(input);
|
||||
}
|
||||
|
||||
async update{FeatureName}(id: string, input: Update{FeatureName}Input): Promise<{FeatureName}> {
|
||||
await this.get{FeatureName}(id); // Verify exists
|
||||
return this.repository.update(id, input);
|
||||
}
|
||||
|
||||
async delete{FeatureName}(id: string): Promise<void> {
|
||||
await this.get{FeatureName}(id); // Verify exists
|
||||
await this.repository.delete(id);
|
||||
}
|
||||
|
||||
private validateInput(input: Create{FeatureName}Input): void {
|
||||
if (!input.name || input.name.trim().length === 0) {
|
||||
throw new ValidationError('Name is required');
|
||||
}
|
||||
|
||||
if (input.name.length > 255) {
|
||||
throw new ValidationError('Name must not exceed 255 characters');
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Controller Scaffold
|
||||
|
||||
```typescript
|
||||
// controllers/{FeatureName}Controller.ts
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { {FeatureName}Service } from '../services/{FeatureName}Service';
|
||||
|
||||
export class {FeatureName}Controller {
|
||||
constructor(private service: {FeatureName}Service) {}
|
||||
|
||||
get{FeatureName} = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const entity = await this.service.get{FeatureName}(id);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: entity,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
list{FeatureName}s = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const page = parseInt(req.query.page as string) || 1;
|
||||
const limit = parseInt(req.query.limit as string) || 20;
|
||||
|
||||
const result = await this.service.list{FeatureName}s(page, limit);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
meta: {
|
||||
total: result.total,
|
||||
page: result.page,
|
||||
totalPages: result.totalPages,
|
||||
limit,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
create{FeatureName} = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const entity = await this.service.create{FeatureName}(req.body);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: entity,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
update{FeatureName} = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const entity = await this.service.update{FeatureName}(id, req.body);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: entity,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
delete{FeatureName} = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
await this.service.delete{FeatureName}(id);
|
||||
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
#### Routes Scaffold
|
||||
|
||||
```typescript
|
||||
// routes/{feature-name}.routes.ts
|
||||
import { Router } from 'express';
|
||||
import { {FeatureName}Controller } from '../controllers/{FeatureName}Controller';
|
||||
import { {FeatureName}Service } from '../services/{FeatureName}Service';
|
||||
import { {FeatureName}Repository } from '../repositories/{FeatureName}Repository';
|
||||
import { authenticate } from '../middlewares/auth.middleware';
|
||||
import { validate } from '../middlewares/validation.middleware';
|
||||
import { create{FeatureName}Schema, update{FeatureName}Schema } from '../schemas/{feature-name}.schemas';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Initialize dependencies
|
||||
const repository = new {FeatureName}Repository();
|
||||
const service = new {FeatureName}Service(repository);
|
||||
const controller = new {FeatureName}Controller(service);
|
||||
|
||||
// Public routes
|
||||
router.get('/', controller.list{FeatureName}s);
|
||||
router.get('/:id', controller.get{FeatureName});
|
||||
|
||||
// Protected routes
|
||||
router.post(
|
||||
'/',
|
||||
authenticate,
|
||||
validate(create{FeatureName}Schema),
|
||||
controller.create{FeatureName}
|
||||
);
|
||||
|
||||
router.put(
|
||||
'/:id',
|
||||
authenticate,
|
||||
validate(update{FeatureName}Schema),
|
||||
controller.update{FeatureName}
|
||||
);
|
||||
|
||||
router.delete('/:id', authenticate, controller.delete{FeatureName});
|
||||
|
||||
export default router;
|
||||
```
|
||||
|
||||
#### Validation Schema Scaffold
|
||||
|
||||
```typescript
|
||||
// schemas/{feature-name}.schemas.ts
|
||||
import { z } from 'zod';
|
||||
|
||||
export const create{FeatureName}Schema = z.object({
|
||||
body: z.object({
|
||||
name: z.string().min(1).max(255),
|
||||
// Add more fields as needed
|
||||
}),
|
||||
});
|
||||
|
||||
export const update{FeatureName}Schema = z.object({
|
||||
body: z.object({
|
||||
name: z.string().min(1).max(255).optional(),
|
||||
// Add more fields as needed
|
||||
}),
|
||||
params: z.object({
|
||||
id: z.string().uuid(),
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
### 5. Generate Frontend Layer
|
||||
|
||||
#### Types Scaffold
|
||||
|
||||
```typescript
|
||||
// features/{feature-name}/types/index.ts
|
||||
export interface {FeatureName} {
|
||||
id: string;
|
||||
name: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface Create{FeatureName}Input {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface Update{FeatureName}Input {
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface {FeatureName}Filters {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
success: boolean;
|
||||
data: T[];
|
||||
meta: {
|
||||
total: number;
|
||||
page: number;
|
||||
totalPages: number;
|
||||
limit: number;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
#### API Client Scaffold
|
||||
|
||||
```typescript
|
||||
// features/{feature-name}/api/{feature-name}Api.ts
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import { {FeatureName}, Create{FeatureName}Input, Update{FeatureName}Input, {FeatureName}Filters, PaginatedResponse } from '../types';
|
||||
|
||||
class {FeatureName}Api {
|
||||
private client: AxiosInstance;
|
||||
|
||||
constructor() {
|
||||
this.client = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_URL || '/api',
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
this.client.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('accessToken');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
}
|
||||
|
||||
async list(filters: {FeatureName}Filters = {}): Promise<PaginatedResponse<{FeatureName}>> {
|
||||
const response = await this.client.get('/{feature-name}', { params: filters });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getById(id: string): Promise<{FeatureName}> {
|
||||
const response = await this.client.get(`/{feature-name}/${id}`);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
async create(data: Create{FeatureName}Input): Promise<{FeatureName}> {
|
||||
const response = await this.client.post('/{feature-name}', data);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
async update(id: string, data: Update{FeatureName}Input): Promise<{FeatureName}> {
|
||||
const response = await this.client.put(`/{feature-name}/${id}`, data);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
await this.client.delete(`/{feature-name}/${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
export const {featureName}Api = new {FeatureName}Api();
|
||||
```
|
||||
|
||||
#### Component Scaffolds
|
||||
|
||||
```typescript
|
||||
// features/{feature-name}/components/{FeatureName}List.tsx
|
||||
import React from 'react';
|
||||
import { use{FeatureName}s } from '../hooks/use{FeatureName}s';
|
||||
import { {FeatureName}Card } from './{FeatureName}Card';
|
||||
import { LoadingSpinner } from '@/components/LoadingSpinner';
|
||||
import { ErrorMessage } from '@/components/ErrorMessage';
|
||||
|
||||
export const {FeatureName}List: React.FC = () => {
|
||||
const { items, isLoading, error, refetch } = use{FeatureName}s();
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorMessage message={error.message} onRetry={refetch} />;
|
||||
}
|
||||
|
||||
if (items.length === 0) {
|
||||
return <div>No items found.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="{feature-name}-list">
|
||||
{items.map((item) => (
|
||||
<{FeatureName}Card key={item.id} item={item} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
```typescript
|
||||
// features/{feature-name}/components/{FeatureName}Card.tsx
|
||||
import React from 'react';
|
||||
import { {FeatureName} } from '../types';
|
||||
|
||||
interface {FeatureName}CardProps {
|
||||
item: {FeatureName};
|
||||
onEdit?: (id: string) => void;
|
||||
onDelete?: (id: string) => void;
|
||||
}
|
||||
|
||||
export const {FeatureName}Card: React.FC<{FeatureName}CardProps> = ({
|
||||
item,
|
||||
onEdit,
|
||||
onDelete,
|
||||
}) => {
|
||||
return (
|
||||
<div className="{feature-name}-card">
|
||||
<h3>{item.name}</h3>
|
||||
<div className="{feature-name}-card__actions">
|
||||
{onEdit && (
|
||||
<button onClick={() => onEdit(item.id)}>Edit</button>
|
||||
)}
|
||||
{onDelete && (
|
||||
<button onClick={() => onDelete(item.id)}>Delete</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
```typescript
|
||||
// features/{feature-name}/components/{FeatureName}Form.tsx
|
||||
import React from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
const {featureName}Schema = z.object({
|
||||
name: z.string().min(1, 'Name is required').max(255),
|
||||
});
|
||||
|
||||
type {FeatureName}FormData = z.infer<typeof {featureName}Schema>;
|
||||
|
||||
interface {FeatureName}FormProps {
|
||||
initialData?: {FeatureName}FormData;
|
||||
onSubmit: (data: {FeatureName}FormData) => Promise<void>;
|
||||
onCancel?: () => void;
|
||||
}
|
||||
|
||||
export const {FeatureName}Form: React.FC<{FeatureName}FormProps> = ({
|
||||
initialData,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}) => {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<{FeatureName}FormData>({
|
||||
resolver: zodResolver({featureName}Schema),
|
||||
defaultValues: initialData,
|
||||
});
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="{feature-name}-form">
|
||||
<div className="form-group">
|
||||
<label htmlFor="name">Name</label>
|
||||
<input
|
||||
id="name"
|
||||
type="text"
|
||||
{...register('name')}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
{errors.name && (
|
||||
<span className="error">{errors.name.message}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="form-actions">
|
||||
{onCancel && (
|
||||
<button type="button" onClick={onCancel} disabled={isSubmitting}>
|
||||
Cancel
|
||||
</button>
|
||||
)}
|
||||
<button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting ? 'Submitting...' : 'Submit'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
#### Custom Hook Scaffold
|
||||
|
||||
```typescript
|
||||
// features/{feature-name}/hooks/use{FeatureName}s.ts
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { {featureName}Api } from '../api/{feature-name}Api';
|
||||
import { {FeatureName} } from '../types';
|
||||
|
||||
export const use{FeatureName}s = () => {
|
||||
const [items, setItems] = useState<{FeatureName}[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const fetch{FeatureName}s = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await {featureName}Api.list();
|
||||
setItems(response.data);
|
||||
} catch (err: any) {
|
||||
setError(err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetch{FeatureName}s();
|
||||
}, [fetch{FeatureName}s]);
|
||||
|
||||
const create = useCallback(async (data: any) => {
|
||||
const newItem = await {featureName}Api.create(data);
|
||||
setItems((prev) => [...prev, newItem]);
|
||||
}, []);
|
||||
|
||||
const update = useCallback(async (id: string, data: any) => {
|
||||
const updated = await {featureName}Api.update(id, data);
|
||||
setItems((prev) => prev.map((item) => (item.id === id ? updated : item)));
|
||||
}, []);
|
||||
|
||||
const remove = useCallback(async (id: string) => {
|
||||
await {featureName}Api.delete(id);
|
||||
setItems((prev) => prev.filter((item) => item.id !== id));
|
||||
}, []);
|
||||
|
||||
return {
|
||||
items,
|
||||
isLoading,
|
||||
error,
|
||||
refetch: fetch{FeatureName}s,
|
||||
create,
|
||||
update,
|
||||
remove,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### 6. Generate Test Scaffolds
|
||||
|
||||
```typescript
|
||||
// Backend test scaffold
|
||||
// repositories/__tests__/{FeatureName}Repository.test.ts
|
||||
import { {FeatureName}Repository } from '../{FeatureName}Repository';
|
||||
import { createTestDataSource } from '../../test/utils';
|
||||
|
||||
describe('{FeatureName}Repository', () => {
|
||||
let repository: {FeatureName}Repository;
|
||||
|
||||
beforeAll(async () => {
|
||||
await createTestDataSource();
|
||||
repository = new {FeatureName}Repository();
|
||||
});
|
||||
|
||||
it('should create {feature-name}', async () => {
|
||||
const entity = await repository.create({ name: 'Test' });
|
||||
expect(entity.id).toBeDefined();
|
||||
expect(entity.name).toBe('Test');
|
||||
});
|
||||
|
||||
it('should find {feature-name} by id', async () => {
|
||||
const created = await repository.create({ name: 'Test' });
|
||||
const found = await repository.findById(created.id);
|
||||
expect(found?.name).toBe('Test');
|
||||
});
|
||||
|
||||
// Add more tests
|
||||
});
|
||||
```
|
||||
|
||||
```typescript
|
||||
// Frontend test scaffold
|
||||
// features/{feature-name}/components/__tests__/{FeatureName}List.test.tsx
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { {FeatureName}List } from '../{FeatureName}List';
|
||||
import { use{FeatureName}s } from '../../hooks/use{FeatureName}s';
|
||||
|
||||
jest.mock('../../hooks/use{FeatureName}s');
|
||||
|
||||
describe('{FeatureName}List', () => {
|
||||
it('should render list of items', () => {
|
||||
(use{FeatureName}s as jest.Mock).mockReturnValue({
|
||||
items: [{ id: '1', name: 'Test Item' }],
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
|
||||
render(<{FeatureName}List />);
|
||||
|
||||
expect(screen.getByText('Test Item')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show loading state', () => {
|
||||
(use{FeatureName}s as jest.Mock).mockReturnValue({
|
||||
items: [],
|
||||
isLoading: true,
|
||||
error: null,
|
||||
});
|
||||
|
||||
render(<{FeatureName}List />);
|
||||
|
||||
expect(screen.getByTestId('loading-spinner')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Add more tests
|
||||
});
|
||||
```
|
||||
|
||||
## Output Format
|
||||
|
||||
```markdown
|
||||
# Scaffolded Feature: {Feature Name}
|
||||
|
||||
## Generated Files
|
||||
|
||||
### Database Layer
|
||||
- migrations/TIMESTAMP_add_{feature_name}.ts
|
||||
- entities/{FeatureName}.entity.ts
|
||||
|
||||
### Backend Layer
|
||||
- repositories/{FeatureName}Repository.ts
|
||||
- services/{FeatureName}Service.ts
|
||||
- controllers/{FeatureName}Controller.ts
|
||||
- routes/{feature-name}.routes.ts
|
||||
- schemas/{feature-name}.schemas.ts
|
||||
|
||||
### Frontend Layer
|
||||
- features/{feature-name}/types/index.ts
|
||||
- features/{feature-name}/api/{feature-name}Api.ts
|
||||
- features/{feature-name}/components/{FeatureName}List.tsx
|
||||
- features/{feature-name}/components/{FeatureName}Card.tsx
|
||||
- features/{feature-name}/components/{FeatureName}Form.tsx
|
||||
- features/{feature-name}/hooks/use{FeatureName}s.ts
|
||||
|
||||
### Test Files
|
||||
- repositories/__tests__/{FeatureName}Repository.test.ts
|
||||
- services/__tests__/{FeatureName}Service.test.ts
|
||||
- components/__tests__/{FeatureName}List.test.tsx
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Run database migration
|
||||
2. Register routes in main app
|
||||
3. Implement custom business logic
|
||||
4. Add additional validations
|
||||
5. Customize UI components
|
||||
6. Write comprehensive tests
|
||||
7. Add documentation
|
||||
|
||||
## Customization Points
|
||||
|
||||
- Add custom fields to entity
|
||||
- Implement complex queries in repository
|
||||
- Add business logic to service
|
||||
- Customize UI components
|
||||
- Add additional API endpoints
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
- If project structure unclear: Ask for clarification or detect automatically
|
||||
- If naming conflicts: Suggest alternative names
|
||||
- Generate placeholders for unknown patterns
|
||||
75
commands/feature/skill.md
Normal file
75
commands/feature/skill.md
Normal file
@@ -0,0 +1,75 @@
|
||||
---
|
||||
description: Implement production-ready features across database, backend, and frontend layers with incremental phased approach
|
||||
argument-hint: <operation> [parameters...]
|
||||
---
|
||||
|
||||
# Feature Implementation Router
|
||||
|
||||
Comprehensive feature implementation across the full stack with phased, incremental development approach. Routes feature implementation requests to specialized operations for different layers or full-stack implementation.
|
||||
|
||||
## Operations
|
||||
|
||||
- **implement** - Complete full-stack feature implementation across all layers (database, backend, frontend, integration)
|
||||
- **database** - Database layer only (migrations, models, schemas, indexes)
|
||||
- **backend** - Backend layer only (services, API endpoints, validation, tests)
|
||||
- **frontend** - Frontend layer only (components, state, API integration, tests)
|
||||
- **integrate** - Integration and polish phase (E2E tests, performance, security, documentation)
|
||||
- **scaffold** - Scaffold feature structure and boilerplate across all layers
|
||||
|
||||
## Usage Examples
|
||||
|
||||
```bash
|
||||
# Complete full-stack feature
|
||||
/feature implement description:"user authentication with OAuth and 2FA" tests:"comprehensive"
|
||||
|
||||
# Database layer only
|
||||
/feature database description:"user profiles table with indexes" migration:"add_user_profiles"
|
||||
|
||||
# Backend API only
|
||||
/feature backend description:"REST API for product search with filters" validation:"strict"
|
||||
|
||||
# Frontend components only
|
||||
/feature frontend description:"product catalog with infinite scroll and filters" framework:"react"
|
||||
|
||||
# Integration and polish
|
||||
/feature integrate feature:"authentication flow" scope:"E2E tests and performance"
|
||||
|
||||
# Scaffold feature structure
|
||||
/feature scaffold name:"notification-system" layers:"database,backend,frontend"
|
||||
```
|
||||
|
||||
## Router Logic
|
||||
|
||||
Parse the first word of $ARGUMENTS to determine operation:
|
||||
|
||||
1. Extract operation from first word of $ARGUMENTS
|
||||
2. Extract remaining arguments as operation parameters
|
||||
3. Route to instruction file:
|
||||
- "implement" → Read `.claude/commands/fullstack/feature/implement.md` and execute
|
||||
- "database" → Read `.claude/commands/fullstack/feature/database.md` and execute
|
||||
- "backend" → Read `.claude/commands/fullstack/feature/backend.md` and execute
|
||||
- "frontend" → Read `.claude/commands/fullstack/feature/frontend.md` and execute
|
||||
- "integrate" → Read `.claude/commands/fullstack/feature/integrate.md` and execute
|
||||
- "scaffold" → Read `.claude/commands/fullstack/feature/scaffold.md` and execute
|
||||
|
||||
4. Pass extracted parameters to the instruction file
|
||||
5. Return structured implementation
|
||||
|
||||
**Error Handling:**
|
||||
- If operation is unrecognized, list available operations with examples
|
||||
- If parameters are missing, request clarification with expected format
|
||||
- If requirements are unclear, ask specific questions about scope and acceptance criteria
|
||||
- Provide clear error messages with usage examples
|
||||
|
||||
**Security:**
|
||||
- Validate all input parameters
|
||||
- Ensure no hardcoded secrets in generated code
|
||||
- Follow security best practices for each layer
|
||||
- Include validation and sanitization in generated code
|
||||
|
||||
---
|
||||
|
||||
**Base directory:** `.claude/commands/fullstack/feature`
|
||||
**Current Request:** $ARGUMENTS
|
||||
|
||||
Parse operation and route to appropriate instruction file now.
|
||||
Reference in New Issue
Block a user