Files
2025-11-29 18:47:58 +08:00

456 lines
9.7 KiB
Markdown

# 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?"