Initial commit
This commit is contained in:
489
commands/sng-database.md
Normal file
489
commands/sng-database.md
Normal file
@@ -0,0 +1,489 @@
|
||||
# Database Configuration Command
|
||||
|
||||
You are helping the user configure database connections, optimize queries, and set up database-related infrastructure following Sngular's best practices.
|
||||
|
||||
## Instructions
|
||||
|
||||
1. **Determine the task type**:
|
||||
- Initial database setup and connection
|
||||
- Connection pool configuration
|
||||
- Query optimization
|
||||
- Migration setup
|
||||
- Backup strategy
|
||||
- Performance tuning
|
||||
|
||||
2. **Detect database and tools**:
|
||||
- Database type (PostgreSQL, MySQL, MongoDB, etc.)
|
||||
- ORM/Query builder (TypeORM, Prisma, Sequelize, etc.)
|
||||
- Connection library
|
||||
- Current project structure
|
||||
|
||||
3. **Ask for specific needs**:
|
||||
- Development, staging, or production environment
|
||||
- Connection pooling requirements
|
||||
- Read replicas needed
|
||||
- Caching strategy
|
||||
- Monitoring requirements
|
||||
|
||||
## Implementation Tasks
|
||||
|
||||
### 1. Database Connection Setup
|
||||
|
||||
#### TypeORM Configuration
|
||||
|
||||
```typescript
|
||||
// src/config/database.ts
|
||||
import { DataSource } from 'typeorm'
|
||||
import { config } from 'dotenv'
|
||||
|
||||
config()
|
||||
|
||||
export const AppDataSource = new DataSource({
|
||||
type: 'postgres',
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: parseInt(process.env.DB_PORT || '5432'),
|
||||
username: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_NAME,
|
||||
|
||||
// Connection pool
|
||||
extra: {
|
||||
max: 20, // Maximum connections
|
||||
min: 5, // Minimum connections
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 2000,
|
||||
},
|
||||
|
||||
// Entities
|
||||
entities: ['src/entities/**/*.ts'],
|
||||
migrations: ['src/migrations/**/*.ts'],
|
||||
subscribers: ['src/subscribers/**/*.ts'],
|
||||
|
||||
// Development settings
|
||||
synchronize: process.env.NODE_ENV === 'development',
|
||||
logging: process.env.NODE_ENV === 'development' ? ['query', 'error'] : ['error'],
|
||||
|
||||
// Connection retry
|
||||
retryAttempts: 10,
|
||||
retryDelay: 3000,
|
||||
|
||||
// SSL for production
|
||||
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
|
||||
})
|
||||
|
||||
// Initialize connection
|
||||
export const initializeDatabase = async () => {
|
||||
try {
|
||||
await AppDataSource.initialize()
|
||||
console.log('✅ Database connection established')
|
||||
} catch (error) {
|
||||
console.error('❌ Database connection failed:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Prisma Configuration
|
||||
|
||||
```typescript
|
||||
// src/config/database.ts
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
const prismaClientSingleton = () => {
|
||||
return new PrismaClient({
|
||||
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
|
||||
datasources: {
|
||||
db: {
|
||||
url: process.env.DATABASE_URL,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
declare global {
|
||||
var prisma: undefined | ReturnType<typeof prismaClientSingleton>
|
||||
}
|
||||
|
||||
export const prisma = globalThis.prisma ?? prismaClientSingleton()
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') globalThis.prisma = prisma
|
||||
|
||||
// Graceful shutdown
|
||||
export const disconnectDatabase = async () => {
|
||||
await prisma.$disconnect()
|
||||
}
|
||||
|
||||
// Health check
|
||||
export const checkDatabaseConnection = async () => {
|
||||
try {
|
||||
await prisma.$queryRaw`SELECT 1`
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Database health check failed:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Mongoose (MongoDB) Configuration
|
||||
|
||||
```typescript
|
||||
// src/config/database.ts
|
||||
import mongoose from 'mongoose'
|
||||
|
||||
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/myapp'
|
||||
|
||||
export const connectDatabase = async () => {
|
||||
try {
|
||||
await mongoose.connect(MONGODB_URI, {
|
||||
maxPoolSize: 10,
|
||||
minPoolSize: 5,
|
||||
socketTimeoutMS: 45000,
|
||||
serverSelectionTimeoutMS: 5000,
|
||||
family: 4, // Use IPv4
|
||||
})
|
||||
|
||||
console.log('✅ MongoDB connected')
|
||||
|
||||
// Connection events
|
||||
mongoose.connection.on('error', (err) => {
|
||||
console.error('MongoDB connection error:', err)
|
||||
})
|
||||
|
||||
mongoose.connection.on('disconnected', () => {
|
||||
console.log('MongoDB disconnected')
|
||||
})
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGINT', async () => {
|
||||
await mongoose.connection.close()
|
||||
process.exit(0)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to connect to MongoDB:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Environment Variables
|
||||
|
||||
```bash
|
||||
# .env
|
||||
# Database
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_USER=myapp_user
|
||||
DB_PASSWORD=secure_password_here
|
||||
DB_NAME=myapp_db
|
||||
|
||||
# Alternative: Full connection string
|
||||
DATABASE_URL=postgresql://myapp_user:secure_password_here@localhost:5432/myapp_db
|
||||
|
||||
# MongoDB
|
||||
MONGODB_URI=mongodb://localhost:27017/myapp
|
||||
|
||||
# Connection pool
|
||||
DB_POOL_MIN=5
|
||||
DB_POOL_MAX=20
|
||||
|
||||
# Production
|
||||
NODE_ENV=production
|
||||
DB_SSL=true
|
||||
```
|
||||
|
||||
```bash
|
||||
# .env.example (commit this to git)
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_USER=your_db_user
|
||||
DB_PASSWORD=your_db_password
|
||||
DB_NAME=your_db_name
|
||||
DATABASE_URL=postgresql://user:password@host:port/database
|
||||
```
|
||||
|
||||
### 3. Connection Pool Configuration
|
||||
|
||||
```typescript
|
||||
// Optimized pool settings by environment
|
||||
export const getPoolConfig = () => {
|
||||
const env = process.env.NODE_ENV
|
||||
|
||||
if (env === 'production') {
|
||||
return {
|
||||
max: 20,
|
||||
min: 10,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 2000,
|
||||
}
|
||||
} else if (env === 'staging') {
|
||||
return {
|
||||
max: 10,
|
||||
min: 5,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 2000,
|
||||
}
|
||||
} else {
|
||||
// development
|
||||
return {
|
||||
max: 5,
|
||||
min: 2,
|
||||
idleTimeoutMillis: 10000,
|
||||
connectionTimeoutMillis: 2000,
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Query Optimization
|
||||
|
||||
```typescript
|
||||
// Bad: N+1 query problem
|
||||
const users = await User.find()
|
||||
for (const user of users) {
|
||||
const posts = await Post.find({ authorId: user.id }) // N queries
|
||||
}
|
||||
|
||||
// Good: Eager loading
|
||||
const users = await User.find({
|
||||
relations: ['posts'],
|
||||
})
|
||||
|
||||
// Good: Join query
|
||||
const users = await dataSource
|
||||
.createQueryBuilder(User, 'user')
|
||||
.leftJoinAndSelect('user.posts', 'post')
|
||||
.getMany()
|
||||
|
||||
// Prisma with includes
|
||||
const users = await prisma.user.findMany({
|
||||
include: {
|
||||
posts: true,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
```typescript
|
||||
// Use indexes effectively
|
||||
const users = await User.find({
|
||||
where: { email: 'test@example.com' }, // email column should be indexed
|
||||
})
|
||||
|
||||
// Use select to fetch only needed fields
|
||||
const users = await User.find({
|
||||
select: ['id', 'email', 'name'], // Don't fetch all columns
|
||||
})
|
||||
|
||||
// Pagination with cursors (better than offset)
|
||||
const users = await User.find({
|
||||
where: { id: MoreThan(lastSeenId) },
|
||||
take: 20,
|
||||
order: { id: 'ASC' },
|
||||
})
|
||||
```
|
||||
|
||||
### 5. Transactions
|
||||
|
||||
```typescript
|
||||
// TypeORM transaction
|
||||
await AppDataSource.transaction(async (manager) => {
|
||||
const user = await manager.save(User, { email: 'test@example.com' })
|
||||
await manager.save(Profile, { userId: user.id, bio: 'Hello' })
|
||||
// Both saved or both rolled back
|
||||
})
|
||||
|
||||
// Prisma transaction
|
||||
await prisma.$transaction(async (tx) => {
|
||||
const user = await tx.user.create({ data: { email: 'test@example.com' } })
|
||||
await tx.profile.create({ data: { userId: user.id, bio: 'Hello' } })
|
||||
})
|
||||
|
||||
// Prisma sequential operations
|
||||
await prisma.$transaction([
|
||||
prisma.user.create({ data: { email: 'test@example.com' } }),
|
||||
prisma.post.create({ data: { title: 'First post' } }),
|
||||
])
|
||||
```
|
||||
|
||||
### 6. Database Migrations
|
||||
|
||||
```typescript
|
||||
// Create migration script in package.json
|
||||
{
|
||||
"scripts": {
|
||||
"migration:generate": "typeorm migration:generate -d src/config/database.ts src/migrations/Migration",
|
||||
"migration:run": "typeorm migration:run -d src/config/database.ts",
|
||||
"migration:revert": "typeorm migration:revert -d src/config/database.ts",
|
||||
"schema:sync": "typeorm schema:sync -d src/config/database.ts",
|
||||
"schema:drop": "typeorm schema:drop -d src/config/database.ts"
|
||||
}
|
||||
}
|
||||
|
||||
// Prisma migrations
|
||||
{
|
||||
"scripts": {
|
||||
"prisma:migrate:dev": "prisma migrate dev",
|
||||
"prisma:migrate:deploy": "prisma migrate deploy",
|
||||
"prisma:migrate:reset": "prisma migrate reset",
|
||||
"prisma:generate": "prisma generate",
|
||||
"prisma:studio": "prisma studio"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Health Check Endpoint
|
||||
|
||||
```typescript
|
||||
// src/routes/health.ts
|
||||
import { Request, Response } from 'express'
|
||||
import { AppDataSource } from '../config/database'
|
||||
|
||||
export const healthCheck = async (req: Request, res: Response) => {
|
||||
try {
|
||||
// Check database connection
|
||||
await AppDataSource.query('SELECT 1')
|
||||
|
||||
res.status(200).json({
|
||||
status: 'healthy',
|
||||
database: 'connected',
|
||||
timestamp: new Date().toISOString(),
|
||||
})
|
||||
} catch (error) {
|
||||
res.status(503).json({
|
||||
status: 'unhealthy',
|
||||
database: 'disconnected',
|
||||
error: error.message,
|
||||
timestamp: new Date().toISOString(),
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 8. Database Seeding
|
||||
|
||||
```typescript
|
||||
// src/seeds/seed.ts
|
||||
import { AppDataSource } from '../config/database'
|
||||
import { User } from '../entities/User'
|
||||
import { Role } from '../entities/Role'
|
||||
|
||||
export const seedDatabase = async () => {
|
||||
await AppDataSource.initialize()
|
||||
|
||||
// Create roles
|
||||
const adminRole = await Role.create({ name: 'admin' }).save()
|
||||
const userRole = await Role.create({ name: 'user' }).save()
|
||||
|
||||
// Create users
|
||||
await User.create({
|
||||
email: 'admin@example.com',
|
||||
name: 'Admin User',
|
||||
role: adminRole,
|
||||
}).save()
|
||||
|
||||
await User.create({
|
||||
email: 'user@example.com',
|
||||
name: 'Regular User',
|
||||
role: userRole,
|
||||
}).save()
|
||||
|
||||
console.log('✅ Database seeded')
|
||||
|
||||
await AppDataSource.destroy()
|
||||
}
|
||||
|
||||
// Run: ts-node src/seeds/seed.ts
|
||||
if (require.main === module) {
|
||||
seedDatabase().catch(console.error)
|
||||
}
|
||||
```
|
||||
|
||||
### 9. Query Logging and Monitoring
|
||||
|
||||
```typescript
|
||||
// Custom query logger
|
||||
import { Logger, QueryRunner } from 'typeorm'
|
||||
|
||||
export class DatabaseLogger implements Logger {
|
||||
logQuery(query: string, parameters?: any[], queryRunner?: QueryRunner) {
|
||||
console.log('Query:', query)
|
||||
console.log('Parameters:', parameters)
|
||||
}
|
||||
|
||||
logQueryError(error: string, query: string, parameters?: any[]) {
|
||||
console.error('Query Error:', error)
|
||||
console.error('Query:', query)
|
||||
}
|
||||
|
||||
logQuerySlow(time: number, query: string, parameters?: any[]) {
|
||||
console.warn(`Slow query (${time}ms):`, query)
|
||||
}
|
||||
|
||||
logSchemaBuild(message: string) {
|
||||
console.log('Schema Build:', message)
|
||||
}
|
||||
|
||||
logMigration(message: string) {
|
||||
console.log('Migration:', message)
|
||||
}
|
||||
|
||||
log(level: 'log' | 'info' | 'warn', message: any) {
|
||||
console.log(`[${level.toUpperCase()}]`, message)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 10. Database Backup Script
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# scripts/backup-db.sh
|
||||
|
||||
# Load environment variables
|
||||
source .env
|
||||
|
||||
# Create backup directory
|
||||
mkdir -p backups
|
||||
|
||||
# Generate timestamp
|
||||
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
|
||||
|
||||
# PostgreSQL backup
|
||||
pg_dump -h $DB_HOST -U $DB_USER -d $DB_NAME > "backups/backup_${TIMESTAMP}.sql"
|
||||
|
||||
# Compress backup
|
||||
gzip "backups/backup_${TIMESTAMP}.sql"
|
||||
|
||||
# Delete backups older than 30 days
|
||||
find backups/ -name "*.gz" -mtime +30 -delete
|
||||
|
||||
echo "✅ Backup completed: backup_${TIMESTAMP}.sql.gz"
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always use connection pooling** - Reuse connections instead of creating new ones
|
||||
2. **Use environment variables** - Never hardcode credentials
|
||||
3. **Implement health checks** - Monitor database connectivity
|
||||
4. **Use migrations** - Never modify database schema manually
|
||||
5. **Index appropriately** - Index foreign keys and frequently queried columns
|
||||
6. **Optimize queries** - Use explain plans to identify slow queries
|
||||
7. **Use transactions** - For operations that must succeed or fail together
|
||||
8. **Implement read replicas** - For high-read applications
|
||||
9. **Set up monitoring** - Track query performance and connection pool metrics
|
||||
10. **Regular backups** - Automate database backups
|
||||
|
||||
## Security Checklist
|
||||
|
||||
- [ ] Use SSL/TLS for database connections in production
|
||||
- [ ] Store credentials in environment variables or secrets manager
|
||||
- [ ] Use least privilege principle for database users
|
||||
- [ ] Enable audit logging for sensitive operations
|
||||
- [ ] Implement connection timeout and retry logic
|
||||
- [ ] Validate and sanitize all inputs
|
||||
- [ ] Use parameterized queries (prevent SQL injection)
|
||||
- [ ] Regular security patches and updates
|
||||
- [ ] Implement IP whitelisting for database access
|
||||
- [ ] Enable database firewall rules
|
||||
|
||||
Ask the user: "What database configuration task would you like help with?"
|
||||
318
commands/sng-endpoint.md
Normal file
318
commands/sng-endpoint.md
Normal file
@@ -0,0 +1,318 @@
|
||||
# Create API Endpoint Command
|
||||
|
||||
You are helping the user create a new API endpoint following Sngular's backend development best practices.
|
||||
|
||||
## Instructions
|
||||
|
||||
1. **Detect the backend framework**:
|
||||
- Node.js with Express
|
||||
- Node.js with Fastify
|
||||
- NestJS
|
||||
- Python with FastAPI
|
||||
- Python with Flask/Django
|
||||
- Go with Gin/Echo
|
||||
- Other framework
|
||||
|
||||
2. **Ask for endpoint details**:
|
||||
- HTTP method (GET, POST, PUT, PATCH, DELETE)
|
||||
- Route path (e.g., `/api/users`, `/api/posts/:id`)
|
||||
- Purpose and description
|
||||
- Request body schema (if applicable)
|
||||
- Response schema
|
||||
- Authentication required (yes/no)
|
||||
- Rate limiting needed (yes/no)
|
||||
|
||||
3. **Determine API style**:
|
||||
- REST API
|
||||
- GraphQL (query/mutation/subscription)
|
||||
- gRPC
|
||||
- WebSocket
|
||||
|
||||
## Implementation Tasks
|
||||
|
||||
### For REST Endpoints:
|
||||
|
||||
1. **Create route handler** with:
|
||||
- HTTP method and path
|
||||
- Request validation middleware
|
||||
- Business logic / controller method
|
||||
- Response formatting
|
||||
- Error handling
|
||||
|
||||
2. **Add request validation**:
|
||||
- Query parameters validation
|
||||
- Path parameters validation
|
||||
- Request body validation (using Zod, Joi, class-validator)
|
||||
- File upload validation (if needed)
|
||||
|
||||
3. **Implement authentication/authorization**:
|
||||
- JWT token verification
|
||||
- Role-based access control (RBAC)
|
||||
- Permission checks
|
||||
- API key validation
|
||||
|
||||
4. **Add error handling**:
|
||||
- Try-catch blocks
|
||||
- Custom error classes
|
||||
- HTTP status codes
|
||||
- Error response formatting
|
||||
|
||||
5. **Create tests**:
|
||||
- Unit tests for controller logic
|
||||
- Integration tests for full endpoint
|
||||
- Mock database/external services
|
||||
- Test authentication flows
|
||||
|
||||
6. **Add documentation**:
|
||||
- OpenAPI/Swagger annotations
|
||||
- JSDoc/docstrings
|
||||
- Request/response examples
|
||||
- Error codes documentation
|
||||
|
||||
### For GraphQL:
|
||||
|
||||
1. **Define schema**:
|
||||
- Type definitions
|
||||
- Input types
|
||||
- Custom scalars
|
||||
|
||||
2. **Create resolver**:
|
||||
- Query/Mutation/Subscription resolver
|
||||
- Field resolvers
|
||||
- DataLoader for N+1 prevention
|
||||
|
||||
3. **Add validation & auth**:
|
||||
- Schema directives
|
||||
- Resolver-level authorization
|
||||
- Input validation
|
||||
|
||||
## Files to Create/Update
|
||||
|
||||
1. **Route/Controller file**: Define the endpoint handler
|
||||
2. **Validation schema**: Request/response validation
|
||||
3. **Service layer**: Business logic (separate from controller)
|
||||
4. **Tests**: Comprehensive endpoint testing
|
||||
5. **Types/Interfaces**: TypeScript types or Pydantic models
|
||||
6. **Documentation**: API docs/Swagger definitions
|
||||
|
||||
## Best Practices to Follow
|
||||
|
||||
### Code Structure
|
||||
```
|
||||
src/
|
||||
├── routes/
|
||||
│ └── users.routes.ts # Route definitions
|
||||
├── controllers/
|
||||
│ └── users.controller.ts # Request handlers
|
||||
├── services/
|
||||
│ └── users.service.ts # Business logic
|
||||
├── validators/
|
||||
│ └── users.validator.ts # Input validation
|
||||
├── types/
|
||||
│ └── users.types.ts # TypeScript types
|
||||
└── tests/
|
||||
└── users.test.ts # Endpoint tests
|
||||
```
|
||||
|
||||
### Request Validation
|
||||
```typescript
|
||||
import { z } from 'zod'
|
||||
|
||||
const CreateUserSchema = z.object({
|
||||
email: z.string().email(),
|
||||
name: z.string().min(2).max(100),
|
||||
age: z.number().int().positive().optional(),
|
||||
})
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
```typescript
|
||||
// Custom error classes
|
||||
class BadRequestError extends Error {
|
||||
statusCode = 400
|
||||
}
|
||||
|
||||
class UnauthorizedError extends Error {
|
||||
statusCode = 401
|
||||
}
|
||||
|
||||
// Error handling middleware
|
||||
app.use((err, req, res, next) => {
|
||||
res.status(err.statusCode || 500).json({
|
||||
error: {
|
||||
message: err.message,
|
||||
code: err.code,
|
||||
},
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Authentication
|
||||
```typescript
|
||||
// JWT middleware
|
||||
const authMiddleware = async (req, res, next) => {
|
||||
const token = req.headers.authorization?.split(' ')[1]
|
||||
|
||||
if (!token) {
|
||||
throw new UnauthorizedError('No token provided')
|
||||
}
|
||||
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET)
|
||||
req.user = decoded
|
||||
|
||||
next()
|
||||
}
|
||||
```
|
||||
|
||||
### Rate Limiting
|
||||
```typescript
|
||||
import rateLimit from 'express-rate-limit'
|
||||
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 100, // limit each IP to 100 requests per windowMs
|
||||
})
|
||||
|
||||
app.use('/api/', limiter)
|
||||
```
|
||||
|
||||
### Response Formatting
|
||||
```typescript
|
||||
// Success response
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result,
|
||||
meta: {
|
||||
page: 1,
|
||||
limit: 20,
|
||||
total: 100,
|
||||
},
|
||||
})
|
||||
|
||||
// Error response
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: {
|
||||
code: 'VALIDATION_ERROR',
|
||||
message: 'Invalid email format',
|
||||
details: validationErrors,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Database Operations
|
||||
```typescript
|
||||
// Use transactions for multiple operations
|
||||
await db.transaction(async (trx) => {
|
||||
const user = await trx('users').insert(userData)
|
||||
await trx('profiles').insert({ user_id: user.id, ...profileData })
|
||||
})
|
||||
```
|
||||
|
||||
### Logging
|
||||
```typescript
|
||||
import logger from './utils/logger'
|
||||
|
||||
app.post('/api/users', async (req, res) => {
|
||||
logger.info('Creating new user', { email: req.body.email })
|
||||
|
||||
try {
|
||||
const user = await createUser(req.body)
|
||||
logger.info('User created successfully', { userId: user.id })
|
||||
res.status(201).json({ data: user })
|
||||
} catch (error) {
|
||||
logger.error('Failed to create user', { error, body: req.body })
|
||||
throw error
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Testing Example
|
||||
|
||||
```typescript
|
||||
import request from 'supertest'
|
||||
import app from '../app'
|
||||
|
||||
describe('POST /api/users', () => {
|
||||
it('creates a new user with valid data', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/users')
|
||||
.send({
|
||||
email: 'test@example.com',
|
||||
name: 'Test User',
|
||||
})
|
||||
.expect(201)
|
||||
|
||||
expect(response.body.data).toHaveProperty('id')
|
||||
expect(response.body.data.email).toBe('test@example.com')
|
||||
})
|
||||
|
||||
it('returns 400 for invalid email', async () => {
|
||||
await request(app)
|
||||
.post('/api/users')
|
||||
.send({
|
||||
email: 'invalid-email',
|
||||
name: 'Test User',
|
||||
})
|
||||
.expect(400)
|
||||
})
|
||||
|
||||
it('requires authentication', async () => {
|
||||
await request(app)
|
||||
.post('/api/users')
|
||||
.send({ email: 'test@example.com' })
|
||||
.expect(401)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## OpenAPI/Swagger Documentation
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* @swagger
|
||||
* /api/users:
|
||||
* post:
|
||||
* summary: Create a new user
|
||||
* tags: [Users]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - email
|
||||
* - name
|
||||
* properties:
|
||||
* email:
|
||||
* type: string
|
||||
* format: email
|
||||
* name:
|
||||
* type: string
|
||||
* responses:
|
||||
* 201:
|
||||
* description: User created successfully
|
||||
* 400:
|
||||
* description: Invalid input
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
*/
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Always validate and sanitize input
|
||||
- Use parameterized queries to prevent SQL injection
|
||||
- Implement rate limiting
|
||||
- Use HTTPS in production
|
||||
- Never expose sensitive data in responses
|
||||
- Hash passwords with bcrypt
|
||||
- Implement CORS properly
|
||||
- Use security headers (helmet.js)
|
||||
- Validate JWT tokens properly
|
||||
- Implement proper session management
|
||||
|
||||
Ask the user: "What API endpoint would you like to create?"
|
||||
455
commands/sng-model.md
Normal file
455
commands/sng-model.md
Normal file
@@ -0,0 +1,455 @@
|
||||
# Create Database Model Command
|
||||
|
||||
You are helping the user create a database model with proper relationships, validation, and migrations following Sngular's backend best practices.
|
||||
|
||||
## Instructions
|
||||
|
||||
1. **Detect the ORM/database tool**:
|
||||
- TypeORM (TypeScript/Node.js)
|
||||
- Prisma (TypeScript/Node.js)
|
||||
- Sequelize (JavaScript/TypeScript)
|
||||
- Mongoose (MongoDB)
|
||||
- SQLAlchemy (Python)
|
||||
- Django ORM (Python)
|
||||
- GORM (Go)
|
||||
- Other
|
||||
|
||||
2. **Determine database type**:
|
||||
- PostgreSQL
|
||||
- MySQL/MariaDB
|
||||
- MongoDB
|
||||
- SQLite
|
||||
- SQL Server
|
||||
- Other
|
||||
|
||||
3. **Ask for model details**:
|
||||
- Model name (e.g., User, Product, Order)
|
||||
- Fields/attributes with types
|
||||
- Validation rules
|
||||
- Relationships to other models
|
||||
- Indexes needed
|
||||
- Timestamps (created_at, updated_at)
|
||||
- Soft deletes needed
|
||||
|
||||
4. **Identify relationships**:
|
||||
- One-to-One
|
||||
- One-to-Many
|
||||
- Many-to-Many
|
||||
- Self-referential
|
||||
|
||||
## Implementation Tasks
|
||||
|
||||
### 1. Create Model Class/Schema
|
||||
|
||||
```typescript
|
||||
// TypeORM Example
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, OneToMany } from 'typeorm'
|
||||
|
||||
@Entity('users')
|
||||
export class User {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string
|
||||
|
||||
@Column({ unique: true })
|
||||
email: string
|
||||
|
||||
@Column()
|
||||
name: string
|
||||
|
||||
@Column({ nullable: true })
|
||||
avatar?: string
|
||||
|
||||
@Column({ default: true })
|
||||
isActive: boolean
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt: Date
|
||||
|
||||
// Relationships
|
||||
@OneToMany(() => Post, post => post.author)
|
||||
posts: Post[]
|
||||
|
||||
@ManyToOne(() => Role, role => role.users)
|
||||
role: Role
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Add Validation
|
||||
|
||||
```typescript
|
||||
import { IsEmail, IsString, MinLength, MaxLength, IsOptional } from 'class-validator'
|
||||
|
||||
export class CreateUserDto {
|
||||
@IsEmail()
|
||||
email: string
|
||||
|
||||
@IsString()
|
||||
@MinLength(2)
|
||||
@MaxLength(100)
|
||||
name: string
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
avatar?: string
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Create Migration
|
||||
|
||||
```typescript
|
||||
// TypeORM Migration
|
||||
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm'
|
||||
|
||||
export class CreateUsersTable1234567890 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.createTable(
|
||||
new Table({
|
||||
name: 'users',
|
||||
columns: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'uuid',
|
||||
isPrimary: true,
|
||||
generationStrategy: 'uuid',
|
||||
default: 'uuid_generate_v4()',
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
type: 'varchar',
|
||||
isUnique: true,
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: 'varchar',
|
||||
},
|
||||
{
|
||||
name: 'avatar',
|
||||
type: 'varchar',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
name: 'is_active',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
name: 'role_id',
|
||||
type: 'uuid',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
name: 'created_at',
|
||||
type: 'timestamp',
|
||||
default: 'now()',
|
||||
},
|
||||
{
|
||||
name: 'updated_at',
|
||||
type: 'timestamp',
|
||||
default: 'now()',
|
||||
},
|
||||
],
|
||||
}),
|
||||
true,
|
||||
)
|
||||
|
||||
// Add foreign key
|
||||
await queryRunner.createForeignKey(
|
||||
'users',
|
||||
new TableForeignKey({
|
||||
columnNames: ['role_id'],
|
||||
referencedColumnNames: ['id'],
|
||||
referencedTableName: 'roles',
|
||||
onDelete: 'SET NULL',
|
||||
}),
|
||||
)
|
||||
|
||||
// Add indexes
|
||||
await queryRunner.createIndex('users', {
|
||||
name: 'IDX_USER_EMAIL',
|
||||
columnNames: ['email'],
|
||||
})
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.dropTable('users')
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Create Repository/Service
|
||||
|
||||
```typescript
|
||||
// Repository pattern
|
||||
import { Repository } from 'typeorm'
|
||||
import { User } from './user.entity'
|
||||
|
||||
export class UserRepository extends Repository<User> {
|
||||
async findByEmail(email: string): Promise<User | null> {
|
||||
return this.findOne({ where: { email } })
|
||||
}
|
||||
|
||||
async findActiveUsers(): Promise<User[]> {
|
||||
return this.find({
|
||||
where: { isActive: true },
|
||||
relations: ['role', 'posts'],
|
||||
})
|
||||
}
|
||||
|
||||
async createUser(data: CreateUserDto): Promise<User> {
|
||||
const user = this.create(data)
|
||||
return this.save(user)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Prisma Example
|
||||
|
||||
```typescript
|
||||
// schema.prisma
|
||||
model User {
|
||||
id String @id @default(uuid())
|
||||
email String @unique
|
||||
name String
|
||||
avatar String?
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// Relations
|
||||
posts Post[]
|
||||
role Role? @relation(fields: [roleId], references: [id])
|
||||
roleId String?
|
||||
|
||||
@@index([email])
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
model Post {
|
||||
id String @id @default(uuid())
|
||||
title String
|
||||
content String
|
||||
published Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
author User @relation(fields: [authorId], references: [id])
|
||||
authorId String
|
||||
|
||||
@@map("posts")
|
||||
}
|
||||
|
||||
model Role {
|
||||
id String @id @default(uuid())
|
||||
name String @unique
|
||||
users User[]
|
||||
|
||||
@@map("roles")
|
||||
}
|
||||
```
|
||||
|
||||
## Mongoose Example (MongoDB)
|
||||
|
||||
```typescript
|
||||
import mongoose, { Schema, Document } from 'mongoose'
|
||||
|
||||
export interface IUser extends Document {
|
||||
email: string
|
||||
name: string
|
||||
avatar?: string
|
||||
isActive: boolean
|
||||
roleId?: mongoose.Types.ObjectId
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
const UserSchema = new Schema<IUser>(
|
||||
{
|
||||
email: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
lowercase: true,
|
||||
trim: true,
|
||||
validate: {
|
||||
validator: (v: string) => /\S+@\S+\.\S+/.test(v),
|
||||
message: 'Invalid email format',
|
||||
},
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
minlength: 2,
|
||||
maxlength: 100,
|
||||
},
|
||||
avatar: {
|
||||
type: String,
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
roleId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Role',
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
},
|
||||
)
|
||||
|
||||
// Indexes
|
||||
UserSchema.index({ email: 1 })
|
||||
UserSchema.index({ isActive: 1, createdAt: -1 })
|
||||
|
||||
// Virtual populate
|
||||
UserSchema.virtual('posts', {
|
||||
ref: 'Post',
|
||||
localField: '_id',
|
||||
foreignField: 'authorId',
|
||||
})
|
||||
|
||||
// Methods
|
||||
UserSchema.methods.toJSON = function () {
|
||||
const obj = this.toObject()
|
||||
delete obj.__v
|
||||
return obj
|
||||
}
|
||||
|
||||
export const User = mongoose.model<IUser>('User', UserSchema)
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Naming Conventions
|
||||
- Use singular names for models (User, not Users)
|
||||
- Use camelCase for field names in code
|
||||
- Use snake_case for database column names
|
||||
- Prefix foreign keys with table name (user_id, not just id)
|
||||
|
||||
### 2. Data Types
|
||||
- Use UUID for primary keys
|
||||
- Use ENUM for fixed sets of values
|
||||
- Use appropriate numeric types (int, bigint, decimal)
|
||||
- Use TEXT for unlimited length strings
|
||||
- Use JSONB for flexible data (PostgreSQL)
|
||||
|
||||
### 3. Relationships
|
||||
- Always define both sides of relationships
|
||||
- Use appropriate cascade options (CASCADE, SET NULL, RESTRICT)
|
||||
- Index foreign key columns
|
||||
- Consider soft deletes for important data
|
||||
|
||||
### 4. Indexes
|
||||
- Index columns used in WHERE clauses
|
||||
- Index foreign key columns
|
||||
- Create composite indexes for multi-column queries
|
||||
- Don't over-index (impacts write performance)
|
||||
|
||||
### 5. Validation
|
||||
- Validate at both model and database level
|
||||
- Use appropriate constraints (NOT NULL, UNIQUE, CHECK)
|
||||
- Validate data types and formats
|
||||
- Implement custom validators for complex rules
|
||||
|
||||
### 6. Timestamps
|
||||
- Always include created_at and updated_at
|
||||
- Consider deleted_at for soft deletes
|
||||
- Use database-level defaults (now())
|
||||
|
||||
### 7. Security
|
||||
- Never store passwords in plain text
|
||||
- Hash sensitive data
|
||||
- Use appropriate field types for sensitive data
|
||||
- Implement row-level security where needed
|
||||
|
||||
## Files to Create
|
||||
|
||||
1. **Entity/Model file**: Model definition
|
||||
2. **DTO files**: Data transfer objects for validation
|
||||
3. **Migration file**: Database schema changes
|
||||
4. **Repository file**: Data access methods (if applicable)
|
||||
5. **Seed file**: Sample data for development/testing
|
||||
6. **Tests**: Model and repository tests
|
||||
|
||||
## Testing Example
|
||||
|
||||
```typescript
|
||||
import { User } from './user.entity'
|
||||
import { AppDataSource } from './data-source'
|
||||
|
||||
describe('User Model', () => {
|
||||
beforeAll(async () => {
|
||||
await AppDataSource.initialize()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await AppDataSource.destroy()
|
||||
})
|
||||
|
||||
it('creates a user with valid data', async () => {
|
||||
const user = User.create({
|
||||
email: 'test@example.com',
|
||||
name: 'Test User',
|
||||
})
|
||||
|
||||
await user.save()
|
||||
|
||||
expect(user.id).toBeDefined()
|
||||
expect(user.email).toBe('test@example.com')
|
||||
expect(user.createdAt).toBeInstanceOf(Date)
|
||||
})
|
||||
|
||||
it('enforces unique email constraint', async () => {
|
||||
await User.create({ email: 'duplicate@example.com', name: 'User 1' }).save()
|
||||
|
||||
await expect(
|
||||
User.create({ email: 'duplicate@example.com', name: 'User 2' }).save()
|
||||
).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('validates email format', async () => {
|
||||
const user = User.create({ email: 'invalid-email', name: 'Test User' })
|
||||
|
||||
await expect(user.save()).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Common Relationship Patterns
|
||||
|
||||
### One-to-Many
|
||||
```typescript
|
||||
// One user has many posts
|
||||
@OneToMany(() => Post, post => post.author)
|
||||
posts: Post[]
|
||||
|
||||
@ManyToOne(() => User, user => user.posts)
|
||||
author: User
|
||||
```
|
||||
|
||||
### Many-to-Many
|
||||
```typescript
|
||||
// Users can have many roles, roles can have many users
|
||||
@ManyToMany(() => Role, role => role.users)
|
||||
@JoinTable({ name: 'user_roles' })
|
||||
roles: Role[]
|
||||
|
||||
@ManyToMany(() => User, user => user.roles)
|
||||
users: User[]
|
||||
```
|
||||
|
||||
### Self-Referential
|
||||
```typescript
|
||||
// User can have a manager who is also a User
|
||||
@ManyToOne(() => User, user => user.subordinates)
|
||||
manager: User
|
||||
|
||||
@OneToMany(() => User, user => user.manager)
|
||||
subordinates: User[]
|
||||
```
|
||||
|
||||
Ask the user: "What database model would you like to create?"
|
||||
Reference in New Issue
Block a user