Initial commit
This commit is contained in:
13
.claude-plugin/plugin.json
Normal file
13
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "data-quality",
|
||||
"description": "Data validation and quality assurance tools using Pydantic v2, schema validation, data contracts, and quality monitoring for PlanetScale PostgreSQL databases",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "Grey Haven Studio"
|
||||
},
|
||||
"skills": [
|
||||
"./skills/database-conventions",
|
||||
"./skills/data-modeling",
|
||||
"./skills/data-validation"
|
||||
]
|
||||
}
|
||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# data-quality
|
||||
|
||||
Data validation and quality assurance tools using Pydantic v2, schema validation, data contracts, and quality monitoring for PlanetScale PostgreSQL databases
|
||||
145
plugin.lock.json
Normal file
145
plugin.lock.json
Normal file
@@ -0,0 +1,145 @@
|
||||
{
|
||||
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||
"pluginId": "gh:greyhaven-ai/claude-code-config:grey-haven-plugins/data-quality",
|
||||
"normalized": {
|
||||
"repo": null,
|
||||
"ref": "refs/tags/v20251128.0",
|
||||
"commit": "7265592fee44be60c2f716a9cbab82773f093454",
|
||||
"treeHash": "0fc0729719008a1b481d54dde60cd736f6a8080d3f7eff0686b645c546d3b75d",
|
||||
"generatedAt": "2025-11-28T10:17:05.676196Z",
|
||||
"toolVersion": "publish_plugins.py@0.2.0"
|
||||
},
|
||||
"origin": {
|
||||
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||
"branch": "master",
|
||||
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||
},
|
||||
"manifest": {
|
||||
"name": "data-quality",
|
||||
"description": "Data validation and quality assurance tools using Pydantic v2, schema validation, data contracts, and quality monitoring for PlanetScale PostgreSQL databases",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"content": {
|
||||
"files": [
|
||||
{
|
||||
"path": "README.md",
|
||||
"sha256": "1e3b9c48bc67d8b8a31853e9f0b429acee1131c0b111098deb964fcfe41a346e"
|
||||
},
|
||||
{
|
||||
"path": ".claude-plugin/plugin.json",
|
||||
"sha256": "348326ccd423923fccddbb6446f54671b0fb5ddedf6a9709f6d966dd40d763a9"
|
||||
},
|
||||
{
|
||||
"path": "skills/database-conventions/SKILL.md",
|
||||
"sha256": "9267de5d10193db61485f04711a686becb73ebd5b8f38e57039dbceb2d4f55cf"
|
||||
},
|
||||
{
|
||||
"path": "skills/database-conventions/checklists/schema-checklist.md",
|
||||
"sha256": "4447fa7407a948c8c2aefeaaded3932cddccf9a2d1771cd66aee5abd1dddc682"
|
||||
},
|
||||
{
|
||||
"path": "skills/database-conventions/checklists/migration-checklist.md",
|
||||
"sha256": "9d84612667c40d6cffd1b673e7482575f7bbe037c1dd644389fe1f1c14253d4d"
|
||||
},
|
||||
{
|
||||
"path": "skills/database-conventions/examples/migrations.md",
|
||||
"sha256": "f71a9a8e72471270ac8eb383aa5784b9f58dad340d87c9b251a15b7aa38ac62a"
|
||||
},
|
||||
{
|
||||
"path": "skills/database-conventions/examples/rls-policies.md",
|
||||
"sha256": "25fd8768d47128debe21a293e4358b96634039b6f466a8276b28b90e0cbf646d"
|
||||
},
|
||||
{
|
||||
"path": "skills/database-conventions/examples/INDEX.md",
|
||||
"sha256": "ce65a1bb03dcb476b9e25c0ffc0a3b139518d4ebfa81452334e6a62e3ef14af7"
|
||||
},
|
||||
{
|
||||
"path": "skills/database-conventions/examples/drizzle-schemas.md",
|
||||
"sha256": "d2074fcfe660d6156f874e1780f4be1305b3b26f0ffd7248d50639d2c1083812"
|
||||
},
|
||||
{
|
||||
"path": "skills/database-conventions/examples/sqlmodel-schemas.md",
|
||||
"sha256": "9eb5141dc2fa3a32777b0af3060ed11a3c53eb7d6de26a717f9a0b071d7f25ea"
|
||||
},
|
||||
{
|
||||
"path": "skills/database-conventions/templates/sqlmodel-table.py",
|
||||
"sha256": "b3a14e3d8ecdf791ba941a2c93dcdda1c7b2e6fe793f483bdeae73131ca34414"
|
||||
},
|
||||
{
|
||||
"path": "skills/database-conventions/templates/drizzle-table.ts",
|
||||
"sha256": "70f1b982ab54fc33f1d956ce447aa49cc9c86135f32ebf81951767997ae6a7a6"
|
||||
},
|
||||
{
|
||||
"path": "skills/database-conventions/reference/relationships.md",
|
||||
"sha256": "94e46cfe658e1e08d727628743a96e0628cff65e651fd56b8b2ae02b22ae388e"
|
||||
},
|
||||
{
|
||||
"path": "skills/database-conventions/reference/indexing.md",
|
||||
"sha256": "dfc2104ce360cff92b2c6e915eaa2b7c8d2a65e7df6b6a9cc26cc82d7e077953"
|
||||
},
|
||||
{
|
||||
"path": "skills/database-conventions/reference/INDEX.md",
|
||||
"sha256": "a2fe3b16b6ccb5673d4847fcbd7e6b38731fdf43b558f20a9e6ec132254d0f6a"
|
||||
},
|
||||
{
|
||||
"path": "skills/database-conventions/reference/field-naming.md",
|
||||
"sha256": "b1fadcb6da008ac59ff523a103e22ce509d34bd7302617571adc8b34839cc8c0"
|
||||
},
|
||||
{
|
||||
"path": "skills/data-validation/SKILL.md",
|
||||
"sha256": "7c82d446df5aa209038b761940f32971b7b503b43fed8ba04a0cf091c61b825e"
|
||||
},
|
||||
{
|
||||
"path": "skills/data-validation/checklists/data-validation-checklist.md",
|
||||
"sha256": "0182c2917e06ff7d9865b27158b5190fe061be01d6f7eeff80468eafbd70586b"
|
||||
},
|
||||
{
|
||||
"path": "skills/data-validation/examples/INDEX.md",
|
||||
"sha256": "abdb3bd664bb60e0c4cb866d3590980eb3fd77ce5c591a27a5246d0cdf56d283"
|
||||
},
|
||||
{
|
||||
"path": "skills/data-validation/templates/fastapi-endpoint.py",
|
||||
"sha256": "8b9c87c05d7d2f5f09f4e777b78247fe1eb72fc150e981a229c6016fcab8ac0d"
|
||||
},
|
||||
{
|
||||
"path": "skills/data-validation/templates/sqlmodel-model.py",
|
||||
"sha256": "94302377fa376bda7255cee5506dbd50a2bc6bf8b56ac65b630be94608e520ff"
|
||||
},
|
||||
{
|
||||
"path": "skills/data-validation/templates/INDEX.md",
|
||||
"sha256": "7e072e14578813609169cf8211e4de8ff6eb5b5bd2b524358c482ccda2638c29"
|
||||
},
|
||||
{
|
||||
"path": "skills/data-validation/templates/pydantic-model.py",
|
||||
"sha256": "a6446a36d64ffc74623bfb6a7365c790e85d218885ef8b9ee874dc8ebc8ddfdf"
|
||||
},
|
||||
{
|
||||
"path": "skills/data-validation/reference/INDEX.md",
|
||||
"sha256": "60c31c768cdd8023c16e400692ed8313fdc854bc016b974e1e31b33f356c809b"
|
||||
},
|
||||
{
|
||||
"path": "skills/data-modeling/SKILL.md",
|
||||
"sha256": "91b8f644b534d15be16513fd75dee317521e035b22a21216338af123ebc23b0b"
|
||||
},
|
||||
{
|
||||
"path": "skills/data-modeling/checklists/schema-checklist.md",
|
||||
"sha256": "f8d934c9fbd2d274a9c0ceec3bf6d6b487bcb5b9753ff84a265cc6ad6330f627"
|
||||
},
|
||||
{
|
||||
"path": "skills/data-modeling/examples/INDEX.md",
|
||||
"sha256": "a9d31a8f1588bf32ec7bde7077646dd4f9321f323c09b50d9fb92092e9f99636"
|
||||
},
|
||||
{
|
||||
"path": "skills/data-modeling/reference/INDEX.md",
|
||||
"sha256": "a64733c85c87a5adb64dba6086a928c5936cbf4f6f7daf3ed786c591c1f8c9b7"
|
||||
}
|
||||
],
|
||||
"dirSha256": "0fc0729719008a1b481d54dde60cd736f6a8080d3f7eff0686b645c546d3b75d"
|
||||
},
|
||||
"security": {
|
||||
"scannedAt": null,
|
||||
"scannerVersion": null,
|
||||
"flags": []
|
||||
}
|
||||
}
|
||||
289
skills/data-modeling/SKILL.md
Normal file
289
skills/data-modeling/SKILL.md
Normal file
@@ -0,0 +1,289 @@
|
||||
---
|
||||
name: grey-haven-data-modeling
|
||||
description: Design database schemas for Grey Haven multi-tenant SaaS - SQLModel models, Drizzle schema, multi-tenant isolation with tenant_id and RLS, timestamp fields, foreign keys, indexes, migrations, and relationships. Use when creating database tables.
|
||||
---
|
||||
|
||||
# Grey Haven Data Modeling Standards
|
||||
|
||||
Design **database schemas** for Grey Haven Studio's multi-tenant SaaS applications using SQLModel (FastAPI) and Drizzle ORM (TanStack Start) with PostgreSQL and RLS.
|
||||
|
||||
## Multi-Tenant Principles
|
||||
|
||||
### CRITICAL: Every Table Requires tenant_id
|
||||
|
||||
```typescript
|
||||
// ✅ CORRECT - Drizzle
|
||||
export const users = pgTable("users", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
tenant_id: uuid("tenant_id").notNull(), // REQUIRED!
|
||||
created_at: timestamp("created_at").defaultNow().notNull(),
|
||||
updated_at: timestamp("updated_at").defaultNow().notNull(),
|
||||
// ... other fields
|
||||
});
|
||||
```
|
||||
|
||||
```python
|
||||
# ✅ CORRECT - SQLModel
|
||||
class User(SQLModel, table=True):
|
||||
__tablename__ = "users"
|
||||
|
||||
id: UUID = Field(default_factory=uuid4, primary_key=True)
|
||||
tenant_id: UUID = Field(foreign_key="tenants.id", index=True) # REQUIRED!
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
# ... other fields
|
||||
```
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
**ALWAYS use snake_case** (never camelCase):
|
||||
|
||||
```typescript
|
||||
// ✅ CORRECT
|
||||
email_address: text("email_address")
|
||||
created_at: timestamp("created_at")
|
||||
is_active: boolean("is_active")
|
||||
tenant_id: uuid("tenant_id")
|
||||
|
||||
// ❌ WRONG
|
||||
emailAddress: text("emailAddress") // WRONG!
|
||||
createdAt: timestamp("createdAt") // WRONG!
|
||||
```
|
||||
|
||||
### Standard Fields (Required on All Tables)
|
||||
|
||||
```typescript
|
||||
// Every table should have:
|
||||
id: uuid("id").primaryKey().defaultRandom()
|
||||
created_at: timestamp("created_at").defaultNow().notNull()
|
||||
updated_at: timestamp("updated_at").defaultNow().notNull()
|
||||
tenant_id: uuid("tenant_id").notNull()
|
||||
deleted_at: timestamp("deleted_at") // For soft deletes (optional)
|
||||
```
|
||||
|
||||
## Core Tables
|
||||
|
||||
### 1. Tenants Table (Root)
|
||||
|
||||
```typescript
|
||||
// Drizzle
|
||||
export const tenants = pgTable("tenants", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
name: text("name").notNull(),
|
||||
slug: text("slug").notNull().unique(),
|
||||
is_active: boolean("is_active").default(true).notNull(),
|
||||
created_at: timestamp("created_at").defaultNow().notNull(),
|
||||
updated_at: timestamp("updated_at").defaultNow().notNull(),
|
||||
});
|
||||
```
|
||||
|
||||
```python
|
||||
# SQLModel
|
||||
class Tenant(SQLModel, table=True):
|
||||
__tablename__ = "tenants"
|
||||
|
||||
id: UUID = Field(default_factory=uuid4, primary_key=True)
|
||||
name: str = Field(max_length=255)
|
||||
slug: str = Field(max_length=100, unique=True)
|
||||
is_active: bool = Field(default=True)
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
```
|
||||
|
||||
### 2. Users Table (With Tenant Isolation)
|
||||
|
||||
```typescript
|
||||
// Drizzle
|
||||
export const users = pgTable("users", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
tenant_id: uuid("tenant_id").notNull(),
|
||||
email_address: text("email_address").notNull().unique(),
|
||||
full_name: text("full_name").notNull(),
|
||||
is_active: boolean("is_active").default(true).notNull(),
|
||||
created_at: timestamp("created_at").defaultNow().notNull(),
|
||||
updated_at: timestamp("updated_at").defaultNow().notNull(),
|
||||
deleted_at: timestamp("deleted_at"),
|
||||
});
|
||||
|
||||
// Index for tenant_id
|
||||
export const usersTenantIndex = index("users_tenant_id_idx").on(users.tenant_id);
|
||||
```
|
||||
|
||||
```python
|
||||
# SQLModel
|
||||
class User(SQLModel, table=True):
|
||||
__tablename__ = "users"
|
||||
|
||||
id: UUID = Field(default_factory=uuid4, primary_key=True)
|
||||
tenant_id: UUID = Field(foreign_key="tenants.id", index=True)
|
||||
email_address: str = Field(max_length=255, unique=True)
|
||||
full_name: str = Field(max_length=255)
|
||||
is_active: bool = Field(default=True)
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
deleted_at: Optional[datetime] = None
|
||||
```
|
||||
|
||||
## Relationships
|
||||
|
||||
### One-to-Many
|
||||
|
||||
```typescript
|
||||
// Drizzle - User has many Posts
|
||||
export const posts = pgTable("posts", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
tenant_id: uuid("tenant_id").notNull(),
|
||||
user_id: uuid("user_id").notNull(),
|
||||
title: text("title").notNull(),
|
||||
// ... other fields
|
||||
});
|
||||
|
||||
// Relations
|
||||
export const usersRelations = relations(users, ({ many }) => ({
|
||||
posts: many(posts),
|
||||
}));
|
||||
|
||||
export const postsRelations = relations(posts, ({ one }) => ({
|
||||
user: one(users, {
|
||||
fields: [posts.user_id],
|
||||
references: [users.id],
|
||||
}),
|
||||
}));
|
||||
```
|
||||
|
||||
### Many-to-Many
|
||||
|
||||
```typescript
|
||||
// Drizzle - User has many Roles through UserRoles
|
||||
export const user_roles = pgTable("user_roles", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
tenant_id: uuid("tenant_id").notNull(),
|
||||
user_id: uuid("user_id").notNull(),
|
||||
role_id: uuid("role_id").notNull(),
|
||||
created_at: timestamp("created_at").defaultNow().notNull(),
|
||||
});
|
||||
|
||||
// Indexes for join table
|
||||
export const userRolesUserIndex = index("user_roles_user_id_idx").on(user_roles.user_id);
|
||||
export const userRolesRoleIndex = index("user_roles_role_id_idx").on(user_roles.role_id);
|
||||
```
|
||||
|
||||
## RLS Policies
|
||||
|
||||
### Enable RLS on All Tables
|
||||
|
||||
```sql
|
||||
-- Enable RLS
|
||||
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Tenant isolation policy
|
||||
CREATE POLICY "tenant_isolation"
|
||||
ON users
|
||||
FOR ALL
|
||||
USING (tenant_id = current_setting('app.tenant_id')::uuid);
|
||||
|
||||
-- Admin override policy
|
||||
CREATE POLICY "admin_override"
|
||||
ON users
|
||||
FOR ALL
|
||||
TO admin_role
|
||||
USING (true);
|
||||
```
|
||||
|
||||
## Indexes
|
||||
|
||||
### Required Indexes
|
||||
|
||||
```typescript
|
||||
// ALWAYS index tenant_id
|
||||
export const usersTenantIndex = index("users_tenant_id_idx").on(users.tenant_id);
|
||||
|
||||
// Index foreign keys
|
||||
export const postsUserIndex = index("posts_user_id_idx").on(posts.user_id);
|
||||
|
||||
// Composite indexes for common queries
|
||||
export const postsCompositeIndex = index("posts_tenant_user_idx")
|
||||
.on(posts.tenant_id, posts.user_id);
|
||||
```
|
||||
|
||||
## Migrations
|
||||
|
||||
### Drizzle Kit
|
||||
|
||||
```bash
|
||||
# Generate migration
|
||||
bun run db:generate
|
||||
|
||||
# Apply migration
|
||||
bun run db:migrate
|
||||
|
||||
# Rollback migration (manual)
|
||||
```
|
||||
|
||||
### Alembic (SQLModel)
|
||||
|
||||
```bash
|
||||
# Generate migration
|
||||
alembic revision --autogenerate -m "add users table"
|
||||
|
||||
# Apply migration
|
||||
alembic upgrade head
|
||||
|
||||
# Rollback migration
|
||||
alembic downgrade -1
|
||||
```
|
||||
|
||||
## Supporting Documentation
|
||||
|
||||
All supporting files are under 500 lines per Anthropic best practices:
|
||||
|
||||
- **[examples/](examples/)** - Complete schema examples
|
||||
- [drizzle-models.md](examples/drizzle-models.md) - Drizzle schema examples
|
||||
- [sqlmodel-models.md](examples/sqlmodel-models.md) - SQLModel examples
|
||||
- [relationships.md](examples/relationships.md) - Relationship patterns
|
||||
- [rls-policies.md](examples/rls-policies.md) - RLS policy examples
|
||||
- [INDEX.md](examples/INDEX.md) - Examples navigation
|
||||
|
||||
- **[reference/](reference/)** - Data modeling references
|
||||
- [naming-conventions.md](reference/naming-conventions.md) - Field naming rules
|
||||
- [indexes.md](reference/indexes.md) - Index strategies
|
||||
- [migrations.md](reference/migrations.md) - Migration patterns
|
||||
- [INDEX.md](reference/INDEX.md) - Reference navigation
|
||||
|
||||
- **[templates/](templates/)** - Copy-paste ready templates
|
||||
- [drizzle-table.ts](templates/drizzle-table.ts) - Drizzle table template
|
||||
- [sqlmodel-table.py](templates/sqlmodel-table.py) - SQLModel table template
|
||||
|
||||
- **[checklists/](checklists/)** - Schema checklists
|
||||
- [schema-checklist.md](checklists/schema-checklist.md) - Pre-PR schema validation
|
||||
|
||||
## When to Apply This Skill
|
||||
|
||||
Use this skill when:
|
||||
- Creating new database tables
|
||||
- Designing multi-tenant data models
|
||||
- Adding relationships between tables
|
||||
- Creating RLS policies
|
||||
- Generating database migrations
|
||||
- Refactoring existing schemas
|
||||
- Implementing soft deletes
|
||||
- Adding indexes for performance
|
||||
|
||||
## Template Reference
|
||||
|
||||
These patterns are from Grey Haven's production templates:
|
||||
- **cvi-template**: Drizzle ORM + PostgreSQL + RLS
|
||||
- **cvi-backend-template**: SQLModel + PostgreSQL + Alembic
|
||||
|
||||
## Critical Reminders
|
||||
|
||||
1. **tenant_id**: Required on EVERY table (no exceptions!)
|
||||
2. **snake_case**: All fields use snake_case (NEVER camelCase)
|
||||
3. **Timestamps**: created_at and updated_at on all tables
|
||||
4. **Indexes**: Always index tenant_id and foreign keys
|
||||
5. **RLS policies**: Enable RLS on all tables for tenant isolation
|
||||
6. **Soft deletes**: Use deleted_at instead of hard deletes
|
||||
7. **Foreign keys**: Explicitly define relationships
|
||||
8. **Migrations**: Test both up and down migrations
|
||||
9. **Email fields**: Name as email_address (not email)
|
||||
10. **Boolean fields**: Use is_/has_/can_ prefix
|
||||
60
skills/data-modeling/checklists/schema-checklist.md
Normal file
60
skills/data-modeling/checklists/schema-checklist.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# Database Schema Checklist
|
||||
|
||||
**Use before creating PR for new database schemas.**
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
- [ ] All field names use snake_case (NOT camelCase)
|
||||
- [ ] Boolean fields use is_/has_/can_ prefix
|
||||
- [ ] Timestamp fields use _at suffix
|
||||
- [ ] Foreign keys use _id suffix
|
||||
- [ ] Email field named email_address (not email)
|
||||
- [ ] Phone field named phone_number (not phone)
|
||||
|
||||
## Multi-Tenant Requirements
|
||||
|
||||
- [ ] tenant_id field present on ALL tables
|
||||
- [ ] tenant_id has NOT NULL constraint
|
||||
- [ ] tenant_id has index for performance
|
||||
- [ ] tenant_id has foreign key to tenants table
|
||||
- [ ] RLS policy created for tenant isolation
|
||||
- [ ] Test cases verify tenant isolation
|
||||
|
||||
## Standard Fields
|
||||
|
||||
- [ ] id field (UUID primary key)
|
||||
- [ ] created_at timestamp (NOT NULL, default now())
|
||||
- [ ] updated_at timestamp (NOT NULL, auto-update)
|
||||
- [ ] tenant_id (NOT NULL, indexed)
|
||||
- [ ] deleted_at timestamp (for soft delete, optional)
|
||||
|
||||
## Indexes
|
||||
|
||||
- [ ] tenant_id indexed
|
||||
- [ ] Foreign keys indexed
|
||||
- [ ] Unique fields indexed
|
||||
- [ ] Frequently queried fields indexed
|
||||
- [ ] Composite indexes for common query patterns
|
||||
|
||||
## Relationships
|
||||
|
||||
- [ ] Foreign keys defined explicitly
|
||||
- [ ] Relationships documented in schema
|
||||
- [ ] Cascade delete configured appropriately
|
||||
- [ ] Join tables for many-to-many
|
||||
|
||||
## Migrations
|
||||
|
||||
- [ ] Migration generated (Drizzle Kit or Alembic)
|
||||
- [ ] Migration tested locally
|
||||
- [ ] Migration reversible (has downgrade)
|
||||
- [ ] Migration reviewed by teammate
|
||||
- [ ] No breaking changes without coordination
|
||||
|
||||
## Row Level Security
|
||||
|
||||
- [ ] RLS enabled on table
|
||||
- [ ] Tenant isolation policy created
|
||||
- [ ] Admin override policy (if needed)
|
||||
- [ ] Anonymous policy (if public data)
|
||||
- [ ] RLS tested with different roles
|
||||
36
skills/data-modeling/examples/INDEX.md
Normal file
36
skills/data-modeling/examples/INDEX.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Data Modeling Examples
|
||||
|
||||
Complete database schema examples for multi-tenant SaaS.
|
||||
|
||||
## Available Examples
|
||||
|
||||
### [drizzle-models.md](drizzle-models.md)
|
||||
Drizzle ORM schema examples (TypeScript).
|
||||
- Basic table definitions
|
||||
- Relationships (one-to-many, many-to-many)
|
||||
- Indexes and constraints
|
||||
|
||||
### [sqlmodel-models.md](sqlmodel-models.md)
|
||||
SQLModel schema examples (Python).
|
||||
- Model definitions
|
||||
- Relationships and foreign keys
|
||||
- Type hints and validation
|
||||
|
||||
### [relationships.md](relationships.md)
|
||||
Relationship patterns for both ORMs.
|
||||
- One-to-many relationships
|
||||
- Many-to-many with join tables
|
||||
- Self-referential relationships
|
||||
|
||||
### [rls-policies.md](rls-policies.md)
|
||||
Row Level Security policy examples.
|
||||
- Tenant isolation policies
|
||||
- Admin override policies
|
||||
- Public data policies
|
||||
|
||||
## Quick Reference
|
||||
|
||||
**Need Drizzle?** → [drizzle-models.md](drizzle-models.md)
|
||||
**Need SQLModel?** → [sqlmodel-models.md](sqlmodel-models.md)
|
||||
**Need relationships?** → [relationships.md](relationships.md)
|
||||
**Need RLS?** → [rls-policies.md](rls-policies.md)
|
||||
32
skills/data-modeling/reference/INDEX.md
Normal file
32
skills/data-modeling/reference/INDEX.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Data Modeling Reference
|
||||
|
||||
Configuration and pattern references.
|
||||
|
||||
## Available References
|
||||
|
||||
### [naming-conventions.md](naming-conventions.md)
|
||||
Field naming rules and conventions.
|
||||
- snake_case requirements
|
||||
- Boolean field prefixes
|
||||
- Timestamp field suffixes
|
||||
- Foreign key naming
|
||||
|
||||
### [indexes.md](indexes.md)
|
||||
Index strategies and optimization.
|
||||
- When to create indexes
|
||||
- Composite indexes
|
||||
- Unique constraints
|
||||
- Performance considerations
|
||||
|
||||
### [migrations.md](migrations.md)
|
||||
Migration patterns and best practices.
|
||||
- Drizzle Kit workflow
|
||||
- Alembic workflow
|
||||
- Rollback strategies
|
||||
- Testing migrations
|
||||
|
||||
## Quick Reference
|
||||
|
||||
**Need naming?** → [naming-conventions.md](naming-conventions.md)
|
||||
**Need indexes?** → [indexes.md](indexes.md)
|
||||
**Need migrations?** → [migrations.md](migrations.md)
|
||||
25
skills/data-validation/SKILL.md
Normal file
25
skills/data-validation/SKILL.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Data Validation Skill
|
||||
|
||||
Comprehensive data validation using Pydantic v2 with data quality monitoring and schema alignment for PlanetScale PostgreSQL.
|
||||
|
||||
## Description
|
||||
|
||||
Implement robust data validation, quality monitoring, and schema contracts using Pydantic v2 models.
|
||||
|
||||
## What's Included
|
||||
|
||||
- **Examples**: Pydantic v2 models, validation patterns
|
||||
- **Reference**: Schema design, validation strategies
|
||||
- **Templates**: Pydantic model templates
|
||||
|
||||
## Use When
|
||||
|
||||
- API request/response validation
|
||||
- Database schema alignment
|
||||
- Data quality assurance
|
||||
|
||||
## Related Agents
|
||||
|
||||
- `data-validator`
|
||||
|
||||
**Skill Version**: 1.0
|
||||
346
skills/data-validation/checklists/data-validation-checklist.md
Normal file
346
skills/data-validation/checklists/data-validation-checklist.md
Normal file
@@ -0,0 +1,346 @@
|
||||
# Data Validation Checklist
|
||||
|
||||
Comprehensive checklist for implementing robust data validation in TypeScript (Zod) and Python (Pydantic) applications.
|
||||
|
||||
## Pre-Validation Setup
|
||||
|
||||
- [ ] **Identify all input sources** (API requests, forms, file uploads, external APIs)
|
||||
- [ ] **Choose validation library** (Zod for TypeScript, Pydantic for Python)
|
||||
- [ ] **Define validation strategy** (fail-fast vs collect all errors)
|
||||
- [ ] **Set up error handling** (consistent error response format)
|
||||
- [ ] **Document validation requirements** (business rules, constraints)
|
||||
|
||||
## TypeScript + Zod Validation
|
||||
|
||||
### Schema Definition
|
||||
|
||||
- [ ] **All API endpoints have Zod schemas** defined
|
||||
- [ ] **Schema types exported** for use in frontend
|
||||
- [ ] **Schemas colocated** with route handlers or in shared location
|
||||
- [ ] **Schema composition used** (z.object, z.array, z.union)
|
||||
- [ ] **Reusable schemas extracted** (common patterns like email, UUID)
|
||||
|
||||
### Basic Validations
|
||||
|
||||
- [ ] **String validations** applied:
|
||||
- [ ] `.min()` for minimum length
|
||||
- [ ] `.max()` for maximum length
|
||||
- [ ] `.email()` for email addresses
|
||||
- [ ] `.url()` for URLs
|
||||
- [ ] `.uuid()` for UUIDs
|
||||
- [ ] `.regex()` for custom patterns
|
||||
- [ ] `.trim()` to remove whitespace
|
||||
|
||||
- [ ] **Number validations** applied:
|
||||
- [ ] `.int()` for integers
|
||||
- [ ] `.positive()` for positive numbers
|
||||
- [ ] `.min()` / `.max()` for ranges
|
||||
- [ ] `.finite()` to exclude Infinity/NaN
|
||||
|
||||
- [ ] **Array validations** applied:
|
||||
- [ ] `.min()` for minimum items
|
||||
- [ ] `.max()` for maximum items
|
||||
- [ ] `.nonempty()` for required arrays
|
||||
|
||||
- [ ] **Date validations** applied:
|
||||
- [ ] `.min()` for earliest date
|
||||
- [ ] `.max()` for latest date
|
||||
- [ ] Proper date parsing (z.coerce.date())
|
||||
|
||||
### Advanced Validations
|
||||
|
||||
- [ ] **Custom refinements** for complex rules:
|
||||
```typescript
|
||||
z.object({
|
||||
password: z.string(),
|
||||
confirmPassword: z.string()
|
||||
}).refine(data => data.password === data.confirmPassword, {
|
||||
message: "Passwords don't match",
|
||||
path: ["confirmPassword"]
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Conditional validations** with `.superRefine()`
|
||||
- [ ] **Transform validations** to normalize data (`.transform()`)
|
||||
- [ ] **Discriminated unions** for polymorphic data
|
||||
- [ ] **Branded types** for domain-specific values
|
||||
|
||||
### Error Handling
|
||||
|
||||
- [ ] **Validation errors caught** and formatted consistently
|
||||
- [ ] **Error messages user-friendly** (not technical jargon)
|
||||
- [ ] **Field-level errors** returned (which field failed)
|
||||
- [ ] **Multiple errors collected** (not just first error)
|
||||
- [ ] **Error codes standardized** (e.g., "INVALID_EMAIL")
|
||||
|
||||
### Multi-Tenant Context
|
||||
|
||||
- [ ] **tenant_id validated** on all requests requiring tenant context
|
||||
- [ ] **UUID format verified** for tenant_id
|
||||
- [ ] **Tenant existence checked** (tenant must exist in database)
|
||||
- [ ] **User-tenant relationship verified** (user belongs to tenant)
|
||||
- [ ] **Admin permissions validated** for admin-only operations
|
||||
|
||||
## Python + Pydantic Validation
|
||||
|
||||
### Model Definition
|
||||
|
||||
- [ ] **All API request models** inherit from BaseModel
|
||||
- [ ] **All response models** defined with Pydantic
|
||||
- [ ] **SQLModel used** for database models (includes Pydantic)
|
||||
- [ ] **Field validators** used for custom validation
|
||||
- [ ] **Model validators** used for cross-field validation
|
||||
|
||||
### Basic Field Validators
|
||||
|
||||
- [ ] **EmailStr** used for email fields
|
||||
- [ ] **HttpUrl** used for URL fields
|
||||
- [ ] **UUID4** used for UUID fields
|
||||
- [ ] **Field()** used with constraints:
|
||||
- [ ] `min_length` / `max_length` for strings
|
||||
- [ ] `ge` / `le` for number ranges (greater/less than or equal)
|
||||
- [ ] `gt` / `lt` for strict ranges
|
||||
- [ ] `regex` for pattern matching
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel, EmailStr, Field
|
||||
|
||||
class UserCreate(BaseModel):
|
||||
email: EmailStr
|
||||
name: str = Field(..., min_length=1, max_length=100)
|
||||
age: int = Field(..., ge=0, le=150)
|
||||
```
|
||||
|
||||
### Advanced Validation
|
||||
|
||||
- [ ] **@field_validator** for single field custom validation:
|
||||
```python
|
||||
@field_validator('password')
|
||||
@classmethod
|
||||
def validate_password(cls, v):
|
||||
if len(v) < 8:
|
||||
raise ValueError('Password must be at least 8 characters')
|
||||
return v
|
||||
```
|
||||
|
||||
- [ ] **@model_validator** for cross-field validation:
|
||||
```python
|
||||
@model_validator(mode='after')
|
||||
def check_passwords_match(self):
|
||||
if self.password != self.confirm_password:
|
||||
raise ValueError('Passwords do not match')
|
||||
return self
|
||||
```
|
||||
|
||||
- [ ] **Custom validators** handle edge cases
|
||||
- [ ] **Mode='before'** used for preprocessing
|
||||
- [ ] **Mode='after'** used for post-validation checks
|
||||
|
||||
### Error Handling
|
||||
|
||||
- [ ] **ValidationError caught** in API endpoints
|
||||
- [ ] **Errors formatted** to match frontend expectations
|
||||
- [ ] **HTTPException raised** with 422 status for validation errors
|
||||
- [ ] **Error details included** in response body
|
||||
- [ ] **Logging added** for validation failures (security monitoring)
|
||||
|
||||
### Multi-Tenant Context
|
||||
|
||||
- [ ] **tenant_id field** on all multi-tenant request models
|
||||
- [ ] **Tenant UUID validated** before database queries
|
||||
- [ ] **Repository pattern enforces** tenant filtering
|
||||
- [ ] **Admin flag validated** for privileged operations
|
||||
- [ ] **RLS policies configured** on database tables
|
||||
|
||||
## Database Constraints
|
||||
|
||||
### Schema Constraints
|
||||
|
||||
- [ ] **NOT NULL constraints** on required fields
|
||||
- [ ] **UNIQUE constraints** on unique fields (email, username)
|
||||
- [ ] **CHECK constraints** for value ranges
|
||||
- [ ] **FOREIGN KEY constraints** for relationships
|
||||
- [ ] **Default values** defined where appropriate
|
||||
|
||||
### Index Support
|
||||
|
||||
- [ ] **Indexes created** on frequently queried fields
|
||||
- [ ] **Composite indexes** for multi-field queries
|
||||
- [ ] **Partial indexes** for filtered queries (WHERE clauses)
|
||||
- [ ] **tenant_id indexed** on all multi-tenant tables
|
||||
|
||||
## File Upload Validation
|
||||
|
||||
- [ ] **File size limits** enforced (e.g., 10MB max)
|
||||
- [ ] **File type validation** (MIME type checking)
|
||||
- [ ] **File extension validation** (whitelist allowed extensions)
|
||||
- [ ] **Virus scanning** (if handling untrusted uploads)
|
||||
- [ ] **Content validation** (parse and validate file content)
|
||||
|
||||
### Image Uploads
|
||||
|
||||
- [ ] **Image dimensions validated** (max width/height)
|
||||
- [ ] **Image format verified** (PNG, JPEG, etc.)
|
||||
- [ ] **EXIF data stripped** (security concern)
|
||||
- [ ] **Thumbnails generated** for large images
|
||||
|
||||
### CSV/JSON Uploads
|
||||
|
||||
- [ ] **Parse errors handled gracefully**
|
||||
- [ ] **Schema validation** applied to each row/object
|
||||
- [ ] **Batch validation** with error collection
|
||||
- [ ] **Maximum rows/objects** limit enforced
|
||||
|
||||
## External API Integration
|
||||
|
||||
- [ ] **Response schemas defined** for external APIs
|
||||
- [ ] **Validation applied** to external data
|
||||
- [ ] **Graceful degradation** when validation fails
|
||||
- [ ] **Retry logic** for transient failures
|
||||
- [ ] **Timeout limits** configured
|
||||
|
||||
## Security Validations
|
||||
|
||||
### Input Sanitization
|
||||
|
||||
- [ ] **HTML/script tags stripped** from text inputs
|
||||
- [ ] **SQL injection prevented** (use ORM, not raw SQL)
|
||||
- [ ] **XSS prevention** (escape output in templates)
|
||||
- [ ] **Path traversal prevented** (validate file paths)
|
||||
- [ ] **Command injection prevented** (no shell execution of user input)
|
||||
|
||||
### Authentication & Authorization
|
||||
|
||||
- [ ] **JWT tokens validated** (signature, expiration)
|
||||
- [ ] **Session tokens verified** against database
|
||||
- [ ] **User existence checked** before operations
|
||||
- [ ] **Permissions verified** for protected resources
|
||||
- [ ] **Rate limiting applied** to prevent abuse
|
||||
|
||||
### Sensitive Data
|
||||
|
||||
- [ ] **Passwords never logged** or returned in responses
|
||||
- [ ] **Credit card numbers validated** with Luhn algorithm
|
||||
- [ ] **SSN/Tax ID formats validated**
|
||||
- [ ] **PII handling compliant** with regulations (GDPR, CCPA)
|
||||
- [ ] **Encryption applied** to sensitive stored data
|
||||
|
||||
## Testing Validation Logic
|
||||
|
||||
### Unit Tests
|
||||
|
||||
- [ ] **Valid inputs pass** validation
|
||||
- [ ] **Invalid inputs fail** with correct error messages
|
||||
- [ ] **Edge cases tested** (empty strings, null, undefined)
|
||||
- [ ] **Boundary values tested** (min/max lengths, ranges)
|
||||
- [ ] **Error messages verified** (correct field, message)
|
||||
|
||||
### Integration Tests
|
||||
|
||||
- [ ] **API endpoints validated** in integration tests
|
||||
- [ ] **Database constraints tested** (violate constraint, expect error)
|
||||
- [ ] **Multi-tenant isolation tested** (cross-tenant access blocked)
|
||||
- [ ] **File upload validation tested**
|
||||
- [ ] **External API mocking** with invalid responses
|
||||
|
||||
### Test Coverage
|
||||
|
||||
- [ ] **Validation logic 100% covered**
|
||||
- [ ] **Error paths tested** (not just happy path)
|
||||
- [ ] **Custom validators tested** independently
|
||||
- [ ] **Refinements tested** with failing cases
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- [ ] **Validation performance measured** (avoid expensive validations in hot paths)
|
||||
- [ ] **Async validation** for I/O-bound checks (database lookups)
|
||||
- [ ] **Caching applied** to repeated validations (e.g., tenant existence)
|
||||
- [ ] **Batch validation** for arrays/lists
|
||||
- [ ] **Early returns** for fail-fast scenarios
|
||||
|
||||
## Documentation
|
||||
|
||||
- [ ] **Validation rules documented** in API docs
|
||||
- [ ] **Error responses documented** (status codes, error formats)
|
||||
- [ ] **Examples provided** (valid and invalid requests)
|
||||
- [ ] **Schema exported** for frontend consumption (TypeScript types)
|
||||
- [ ] **Changelog updated** when validation changes
|
||||
|
||||
## Grey Haven Specific
|
||||
|
||||
### TanStack Start (Frontend)
|
||||
|
||||
- [ ] **Form validation** with Zod + TanStack Form
|
||||
- [ ] **Server function validation** (all server functions validate input)
|
||||
- [ ] **Type safety** maintained (Zod.infer<> for types)
|
||||
- [ ] **Error display** in UI components
|
||||
- [ ] **Client-side validation** mirrors server-side
|
||||
|
||||
### FastAPI (Backend)
|
||||
|
||||
- [ ] **Request models** use Pydantic
|
||||
- [ ] **Response models** use Pydantic
|
||||
- [ ] **Repository methods** validate before database operations
|
||||
- [ ] **Service layer** handles business rule validation
|
||||
- [ ] **Dependency injection** for validation context (tenant_id)
|
||||
|
||||
### Database (Drizzle/SQLModel)
|
||||
|
||||
- [ ] **Drizzle schemas** include validation constraints
|
||||
- [ ] **SQLModel fields** use Pydantic validators
|
||||
- [ ] **Migration scripts** add database constraints
|
||||
- [ ] **Indexes support** validation queries
|
||||
|
||||
## Monitoring & Alerting
|
||||
|
||||
- [ ] **Validation failure metrics** tracked
|
||||
- [ ] **High failure rate alerts** configured
|
||||
- [ ] **Unusual validation patterns** logged (potential attacks)
|
||||
- [ ] **Performance metrics** for validation operations
|
||||
- [ ] **Error logs** structured for analysis
|
||||
|
||||
## Scoring
|
||||
|
||||
- **80+ items checked**: Excellent - Comprehensive validation ✅
|
||||
- **60-79 items**: Good - Most validation covered ⚠️
|
||||
- **40-59 items**: Fair - Significant gaps exist 🔴
|
||||
- **<40 items**: Poor - Inadequate validation ❌
|
||||
|
||||
## Priority Items
|
||||
|
||||
Address these first:
|
||||
1. **All API endpoints validated** - Prevent invalid data entry
|
||||
2. **Multi-tenant isolation** - Security-critical
|
||||
3. **SQL injection prevention** - Use ORM, not raw SQL
|
||||
4. **File upload validation** - Common attack vector
|
||||
5. **Error handling** - User experience and debugging
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
❌ **Don't:**
|
||||
- Trust client-side validation alone (always validate server-side)
|
||||
- Use overly complex regex (hard to maintain, performance issues)
|
||||
- Return technical error messages to users
|
||||
- Skip validation on internal endpoints (defense in depth)
|
||||
- Log sensitive data in validation errors
|
||||
|
||||
✅ **Do:**
|
||||
- Validate at boundaries (API endpoints, file uploads, external APIs)
|
||||
- Use standard validators (email, URL, UUID) from libraries
|
||||
- Provide clear, actionable error messages
|
||||
- Test validation logic thoroughly
|
||||
- Document validation requirements
|
||||
|
||||
## Related Resources
|
||||
|
||||
- [Zod Documentation](https://zod.dev)
|
||||
- [Pydantic Documentation](https://docs.pydantic.dev)
|
||||
- [OWASP Input Validation](https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html)
|
||||
- [data-validation skill](../SKILL.md)
|
||||
|
||||
---
|
||||
|
||||
**Total Items**: 120+ validation checks
|
||||
**Critical Items**: API validation, Multi-tenant, Security, File uploads
|
||||
**Coverage**: TypeScript, Python, Database, Security
|
||||
**Last Updated**: 2025-11-10
|
||||
104
skills/data-validation/examples/INDEX.md
Normal file
104
skills/data-validation/examples/INDEX.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# Data Validation Examples
|
||||
|
||||
Complete examples for Pydantic v2 validation, database schema alignment, and data quality monitoring.
|
||||
|
||||
## Examples Overview
|
||||
|
||||
### User Validation Example
|
||||
**File**: [user-validation-example.md](user-validation-example.md)
|
||||
|
||||
Complete user validation workflow with:
|
||||
- Pydantic v2 model with field validators
|
||||
- Email, username, age validation
|
||||
- Cross-field validation (admin age requirement)
|
||||
- SQLModel database alignment
|
||||
- FastAPI integration
|
||||
- Validation error formatting
|
||||
- Pytest test suite
|
||||
|
||||
**Use when**: Building user registration, profile management, or authentication.
|
||||
|
||||
---
|
||||
|
||||
### Order Validation Example
|
||||
**File**: [order-validation-example.md](order-validation-example.md)
|
||||
|
||||
Complex order validation with:
|
||||
- Nested object validation (order items, shipping address)
|
||||
- Currency and pricing validation
|
||||
- Inventory quantity checks
|
||||
- Multi-tenant validation (tenant_id)
|
||||
- Custom validators for business rules
|
||||
- Database transaction validation
|
||||
|
||||
**Use when**: Building e-commerce, order management, or invoicing systems.
|
||||
|
||||
---
|
||||
|
||||
### Nested Validation Example
|
||||
**File**: [nested-validation.md](nested-validation.md)
|
||||
|
||||
Advanced nested validation patterns:
|
||||
- Nested Pydantic models
|
||||
- List validation with min/max items
|
||||
- Discriminated unions for polymorphic data
|
||||
- Recursive validation (tree structures)
|
||||
- Forward references
|
||||
- Validation context passing
|
||||
|
||||
**Use when**: Working with complex JSON structures, API payloads, or hierarchical data.
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Example | Complexity | Key Patterns |
|
||||
|---------|-----------|--------------|
|
||||
| **User** | Basic | Field validators, cross-field validation |
|
||||
| **Order** | Intermediate | Nested objects, business rules, transactions |
|
||||
| **Nested** | Advanced | Discriminated unions, recursion, context |
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Basic Validation
|
||||
```python
|
||||
from pydantic import BaseModel, Field, EmailStr
|
||||
|
||||
class User(BaseModel):
|
||||
email: EmailStr
|
||||
age: int = Field(ge=13, le=120)
|
||||
```
|
||||
|
||||
### Field Validator
|
||||
```python
|
||||
from pydantic import field_validator
|
||||
|
||||
class User(BaseModel):
|
||||
username: str
|
||||
|
||||
@field_validator('username')
|
||||
@classmethod
|
||||
def validate_username(cls, v: str) -> str:
|
||||
if len(v) < 3:
|
||||
raise ValueError('Username too short')
|
||||
return v
|
||||
```
|
||||
|
||||
### Model Validator
|
||||
```python
|
||||
from pydantic import model_validator
|
||||
|
||||
class User(BaseModel):
|
||||
role: str
|
||||
age: int
|
||||
|
||||
@model_validator(mode='after')
|
||||
def check_admin_age(self):
|
||||
if self.role == 'admin' and self.age < 18:
|
||||
raise ValueError('Admins must be 18+')
|
||||
return self
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Return to [main agent](../data-validator.md)
|
||||
84
skills/data-validation/reference/INDEX.md
Normal file
84
skills/data-validation/reference/INDEX.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# Data Validation Reference
|
||||
|
||||
Comprehensive reference documentation for Pydantic v2, SQLModel, and data quality patterns.
|
||||
|
||||
## Reference Materials
|
||||
|
||||
### Pydantic v2 Reference
|
||||
**File**: [pydantic-v2-reference.md](pydantic-v2-reference.md)
|
||||
|
||||
Complete Pydantic v2 guide covering:
|
||||
- Core concepts and migration from v1
|
||||
- Field types and constraints (EmailStr, constr, conint, HttpUrl)
|
||||
- Configuration with model_config
|
||||
- Serialization modes (mode='json', mode='python')
|
||||
- Error handling and custom error messages
|
||||
- Performance optimization tips
|
||||
|
||||
**Use when**: Need comprehensive Pydantic v2 documentation or migrating from v1.
|
||||
|
||||
---
|
||||
|
||||
### Validators Reference
|
||||
**File**: [validators-reference.md](validators-reference.md)
|
||||
|
||||
Complete guide to Pydantic validators:
|
||||
- @field_validator for single-field validation
|
||||
- @model_validator for cross-field validation
|
||||
- Validator modes ('before', 'after', 'wrap')
|
||||
- Accessing other field values with ValidationInfo
|
||||
- Reusable validator functions
|
||||
- Common validation patterns
|
||||
|
||||
**Use when**: Implementing custom validation logic beyond built-in constraints.
|
||||
|
||||
---
|
||||
|
||||
### SQLModel Alignment
|
||||
**File**: [sqlmodel-alignment.md](sqlmodel-alignment.md)
|
||||
|
||||
Ensuring Pydantic schemas align with database models:
|
||||
- Schema alignment validation patterns
|
||||
- Type mapping (Pydantic ↔ SQLModel ↔ PostgreSQL)
|
||||
- Multi-tenant patterns (tenant_id, RLS)
|
||||
- Migration strategies
|
||||
- Testing alignment
|
||||
- Common misalignment issues
|
||||
|
||||
**Use when**: Building APIs with database persistence, ensuring data contracts match.
|
||||
|
||||
---
|
||||
|
||||
### Data Quality Monitoring
|
||||
**File**: [data-quality-monitoring.md](data-quality-monitoring.md)
|
||||
|
||||
Monitoring data quality in production:
|
||||
- Validation metrics tracking
|
||||
- Error rate monitoring
|
||||
- Data profiling patterns
|
||||
- Alerting on validation failures
|
||||
- Quality dashboards
|
||||
- Integration with observability tools
|
||||
|
||||
**Use when**: Setting up production monitoring for data validation.
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Topic | Key Concepts | Common Use Cases |
|
||||
|-------|-------------|------------------|
|
||||
| **Pydantic v2** | Fields, validators, config | Request/response schemas |
|
||||
| **Validators** | @field_validator, @model_validator | Custom business rules |
|
||||
| **SQLModel Alignment** | Type mapping, migrations | Database persistence |
|
||||
| **Data Quality** | Metrics, monitoring, alerts | Production reliability |
|
||||
|
||||
## Navigation
|
||||
|
||||
- **Examples**: [Examples Index](../examples/INDEX.md)
|
||||
- **Templates**: [Templates Index](../templates/INDEX.md)
|
||||
- **Main Agent**: [data-validator.md](../data-validator.md)
|
||||
|
||||
---
|
||||
|
||||
Return to [main agent](../data-validator.md)
|
||||
66
skills/data-validation/templates/INDEX.md
Normal file
66
skills/data-validation/templates/INDEX.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# Data Validation Templates
|
||||
|
||||
Copy-paste templates for common data validation patterns.
|
||||
|
||||
## Available Templates
|
||||
|
||||
### Pydantic Model Template
|
||||
**File**: [pydantic-model.py](pydantic-model.py)
|
||||
|
||||
Complete Pydantic v2 model template with:
|
||||
- Field definitions with constraints
|
||||
- Custom validators (@field_validator, @model_validator)
|
||||
- model_config configuration
|
||||
- Nested models
|
||||
- Documentation
|
||||
|
||||
**Use when**: Starting a new API request/response schema.
|
||||
|
||||
---
|
||||
|
||||
### SQLModel Template
|
||||
**File**: [sqlmodel-model.py](sqlmodel-model.py)
|
||||
|
||||
Complete SQLModel database template with:
|
||||
- Table configuration
|
||||
- Field definitions with PostgreSQL types
|
||||
- Multi-tenant pattern (tenant_id)
|
||||
- Timestamps (created_at, updated_at)
|
||||
- Indexes and constraints
|
||||
- Relationships
|
||||
|
||||
**Use when**: Creating a new database table.
|
||||
|
||||
---
|
||||
|
||||
### FastAPI Endpoint Template
|
||||
**File**: [fastapi-endpoint.py](fastapi-endpoint.py)
|
||||
|
||||
Complete FastAPI endpoint template with:
|
||||
- Router configuration
|
||||
- Pydantic request/response schemas
|
||||
- Dependency injection (session, tenant_id, user_id)
|
||||
- Validation error handling
|
||||
- Database operations
|
||||
- Multi-tenant isolation
|
||||
|
||||
**Use when**: Creating a new API endpoint with validation.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. **Copy template** to your project
|
||||
2. **Rename** model/endpoint appropriately
|
||||
3. **Customize** fields and validators
|
||||
4. **Test** with comprehensive test cases
|
||||
|
||||
## Navigation
|
||||
|
||||
- **Examples**: [Examples Index](../examples/INDEX.md)
|
||||
- **Reference**: [Reference Index](../reference/INDEX.md)
|
||||
- **Main Agent**: [data-validator.md](../data-validator.md)
|
||||
|
||||
---
|
||||
|
||||
Return to [main agent](../data-validator.md)
|
||||
311
skills/data-validation/templates/fastapi-endpoint.py
Normal file
311
skills/data-validation/templates/fastapi-endpoint.py
Normal file
@@ -0,0 +1,311 @@
|
||||
"""
|
||||
FastAPI Endpoint Template
|
||||
|
||||
Copy this template to create new API endpoints with validation.
|
||||
Replace {ModelName}, {endpoint_prefix}, and {table_name} with your actual values.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Depends, status
|
||||
from sqlmodel import Session, select
|
||||
from pydantic import ValidationError
|
||||
from typing import List
|
||||
from uuid import UUID
|
||||
|
||||
# Import your models and schemas
|
||||
from app.models.{model_name} import {ModelName}
|
||||
from app.schemas.{model_name} import (
|
||||
{ModelName}CreateSchema,
|
||||
{ModelName}UpdateSchema,
|
||||
{ModelName}ResponseSchema
|
||||
)
|
||||
from app.database import get_session
|
||||
from app.auth import get_current_user, get_current_tenant_id
|
||||
|
||||
|
||||
# Create router
|
||||
# -------------
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/{endpoint_prefix}",
|
||||
tags=["{endpoint_prefix}"]
|
||||
)
|
||||
|
||||
|
||||
# Create Endpoint
|
||||
# --------------
|
||||
|
||||
@router.post(
|
||||
"/",
|
||||
response_model={ModelName}ResponseSchema,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
summary="Create new {ModelName}",
|
||||
description="Create a new {ModelName} with validation"
|
||||
)
|
||||
async def create_{model_name}(
|
||||
data: {ModelName}CreateSchema,
|
||||
session: Session = Depends(get_session),
|
||||
user_id: UUID = Depends(get_current_user),
|
||||
tenant_id: UUID = Depends(get_current_tenant_id)
|
||||
):
|
||||
"""
|
||||
Create new {ModelName}.
|
||||
|
||||
Validates:
|
||||
- Request data against Pydantic schema
|
||||
- Business rules (if any)
|
||||
- Uniqueness constraints
|
||||
- Multi-tenant isolation
|
||||
|
||||
Returns:
|
||||
{ModelName}ResponseSchema: Created {ModelName}
|
||||
|
||||
Raises:
|
||||
HTTPException 409: If duplicate exists
|
||||
HTTPException 422: If validation fails
|
||||
"""
|
||||
|
||||
# Check for duplicates (if applicable)
|
||||
existing = session.exec(
|
||||
select({ModelName})
|
||||
.where({ModelName}.email == data.email)
|
||||
.where({ModelName}.tenant_id == tenant_id)
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="{ ModelName} with this email already exists"
|
||||
)
|
||||
|
||||
# Create instance
|
||||
instance = {ModelName}(
|
||||
**data.model_dump(),
|
||||
user_id=user_id,
|
||||
tenant_id=tenant_id
|
||||
)
|
||||
|
||||
session.add(instance)
|
||||
session.commit()
|
||||
session.refresh(instance)
|
||||
|
||||
return {ModelName}ResponseSchema.model_validate(instance)
|
||||
|
||||
|
||||
# Read Endpoints
|
||||
# -------------
|
||||
|
||||
@router.get(
|
||||
"/{id}",
|
||||
response_model={ModelName}ResponseSchema,
|
||||
summary="Get {ModelName} by ID"
|
||||
)
|
||||
async def get_{model_name}(
|
||||
id: UUID,
|
||||
session: Session = Depends(get_session),
|
||||
tenant_id: UUID = Depends(get_current_tenant_id)
|
||||
):
|
||||
"""
|
||||
Get {ModelName} by ID with tenant isolation.
|
||||
|
||||
Returns:
|
||||
{ModelName}ResponseSchema: Found {ModelName}
|
||||
|
||||
Raises:
|
||||
HTTPException 404: If not found
|
||||
"""
|
||||
|
||||
instance = session.exec(
|
||||
select({ModelName})
|
||||
.where({ModelName}.id == id)
|
||||
.where({ModelName}.tenant_id == tenant_id)
|
||||
).first()
|
||||
|
||||
if not instance:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="{ModelName} not found"
|
||||
)
|
||||
|
||||
return {ModelName}ResponseSchema.model_validate(instance)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/",
|
||||
response_model=List[{ModelName}ResponseSchema],
|
||||
summary="List {ModelName}s"
|
||||
)
|
||||
async def list_{model_name}s(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
status: str | None = None,
|
||||
session: Session = Depends(get_session),
|
||||
tenant_id: UUID = Depends(get_current_tenant_id)
|
||||
):
|
||||
"""
|
||||
List {ModelName}s with pagination and filtering.
|
||||
|
||||
Args:
|
||||
skip: Number of records to skip (default 0)
|
||||
limit: Maximum records to return (default 100, max 1000)
|
||||
status: Filter by status (optional)
|
||||
|
||||
Returns:
|
||||
List[{ModelName}ResponseSchema]: List of {ModelName}s
|
||||
"""
|
||||
|
||||
# Build query
|
||||
statement = (
|
||||
select({ModelName})
|
||||
.where({ModelName}.tenant_id == tenant_id)
|
||||
.offset(skip)
|
||||
.limit(min(limit, 1000))
|
||||
)
|
||||
|
||||
# Apply filters
|
||||
if status:
|
||||
statement = statement.where({ModelName}.status == status)
|
||||
|
||||
# Execute
|
||||
results = session.exec(statement).all()
|
||||
|
||||
return [
|
||||
{ModelName}ResponseSchema.model_validate(item)
|
||||
for item in results
|
||||
]
|
||||
|
||||
|
||||
# Update Endpoint
|
||||
# --------------
|
||||
|
||||
@router.patch(
|
||||
"/{id}",
|
||||
response_model={ModelName}ResponseSchema,
|
||||
summary="Update {ModelName}"
|
||||
)
|
||||
async def update_{model_name}(
|
||||
id: UUID,
|
||||
data: {ModelName}UpdateSchema,
|
||||
session: Session = Depends(get_session),
|
||||
tenant_id: UUID = Depends(get_current_tenant_id)
|
||||
):
|
||||
"""
|
||||
Update {ModelName} with partial data.
|
||||
|
||||
Only provided fields are updated.
|
||||
|
||||
Returns:
|
||||
{ModelName}ResponseSchema: Updated {ModelName}
|
||||
|
||||
Raises:
|
||||
HTTPException 404: If not found
|
||||
HTTPException 422: If validation fails
|
||||
"""
|
||||
|
||||
# Get existing
|
||||
instance = session.exec(
|
||||
select({ModelName})
|
||||
.where({ModelName}.id == id)
|
||||
.where({ModelName}.tenant_id == tenant_id)
|
||||
).first()
|
||||
|
||||
if not instance:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="{ModelName} not found"
|
||||
)
|
||||
|
||||
# Update fields
|
||||
update_data = data.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(instance, field, value)
|
||||
|
||||
# Update timestamp
|
||||
instance.updated_at = datetime.utcnow()
|
||||
|
||||
session.add(instance)
|
||||
session.commit()
|
||||
session.refresh(instance)
|
||||
|
||||
return {ModelName}ResponseSchema.model_validate(instance)
|
||||
|
||||
|
||||
# Delete Endpoint
|
||||
# --------------
|
||||
|
||||
@router.delete(
|
||||
"/{id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
summary="Delete {ModelName}"
|
||||
)
|
||||
async def delete_{model_name}(
|
||||
id: UUID,
|
||||
session: Session = Depends(get_session),
|
||||
tenant_id: UUID = Depends(get_current_tenant_id)
|
||||
):
|
||||
"""
|
||||
Delete {ModelName}.
|
||||
|
||||
Soft delete by setting is_active = False.
|
||||
For hard delete, use session.delete() instead.
|
||||
|
||||
Raises:
|
||||
HTTPException 404: If not found
|
||||
"""
|
||||
|
||||
instance = session.exec(
|
||||
select({ModelName})
|
||||
.where({ModelName}.id == id)
|
||||
.where({ModelName}.tenant_id == tenant_id)
|
||||
).first()
|
||||
|
||||
if not instance:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="{ModelName} not found"
|
||||
)
|
||||
|
||||
# Soft delete
|
||||
instance.is_active = False
|
||||
instance.updated_at = datetime.utcnow()
|
||||
|
||||
session.add(instance)
|
||||
session.commit()
|
||||
|
||||
# For hard delete:
|
||||
# session.delete(instance)
|
||||
# session.commit()
|
||||
|
||||
|
||||
# Error Handling
|
||||
# -------------
|
||||
|
||||
@router.exception_handler(ValidationError)
|
||||
async def validation_exception_handler(request, exc: ValidationError):
|
||||
"""Handle Pydantic validation errors."""
|
||||
errors = {}
|
||||
|
||||
for error in exc.errors():
|
||||
field = '.'.join(str(loc) for loc in error['loc'])
|
||||
message = error['msg']
|
||||
|
||||
if field not in errors:
|
||||
errors[field] = []
|
||||
errors[field].append(message)
|
||||
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
content={
|
||||
'success': False,
|
||||
'error': 'validation_error',
|
||||
'message': 'Request validation failed',
|
||||
'errors': errors
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# Register Router
|
||||
# --------------
|
||||
|
||||
# In your main FastAPI app:
|
||||
# from app.api.{endpoint_prefix} import router as {model_name}_router
|
||||
# app.include_router({model_name}_router)
|
||||
231
skills/data-validation/templates/pydantic-model.py
Normal file
231
skills/data-validation/templates/pydantic-model.py
Normal file
@@ -0,0 +1,231 @@
|
||||
"""
|
||||
Pydantic Model Template
|
||||
|
||||
Copy this template to create new Pydantic validation models.
|
||||
Replace {ModelName}, {field_name}, and {FieldType} with your actual values.
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator, model_validator, EmailStr, HttpUrl
|
||||
from typing import Optional, List, Literal
|
||||
from datetime import datetime, date
|
||||
from decimal import Decimal
|
||||
from uuid import UUID
|
||||
from enum import Enum
|
||||
|
||||
|
||||
# Optional: Define enums for categorical fields
|
||||
class {ModelName}Status(str, Enum):
|
||||
"""Status enum for {ModelName}."""
|
||||
ACTIVE = 'active'
|
||||
INACTIVE = 'inactive'
|
||||
PENDING = 'pending'
|
||||
|
||||
|
||||
class {ModelName}CreateSchema(BaseModel):
|
||||
"""
|
||||
Schema for creating a {ModelName}.
|
||||
|
||||
This schema defines the data contract for API requests.
|
||||
All validation rules are enforced here.
|
||||
"""
|
||||
|
||||
# Required string field with length constraints
|
||||
name: str = Field(
|
||||
...,
|
||||
min_length=1,
|
||||
max_length=100,
|
||||
description="Display name",
|
||||
examples=["Example Name"]
|
||||
)
|
||||
|
||||
# Email field with built-in validation
|
||||
email: EmailStr = Field(
|
||||
...,
|
||||
description="Email address"
|
||||
)
|
||||
|
||||
# Optional string field
|
||||
description: Optional[str] = Field(
|
||||
None,
|
||||
max_length=500,
|
||||
description="Optional description"
|
||||
)
|
||||
|
||||
# Integer with range constraints
|
||||
quantity: int = Field(
|
||||
...,
|
||||
ge=1,
|
||||
le=999,
|
||||
description="Quantity (1-999)"
|
||||
)
|
||||
|
||||
# Decimal for currency (recommended over float)
|
||||
price: Decimal = Field(
|
||||
...,
|
||||
gt=0,
|
||||
max_digits=10,
|
||||
decimal_places=2,
|
||||
description="Price in USD"
|
||||
)
|
||||
|
||||
# Date field
|
||||
start_date: date = Field(
|
||||
...,
|
||||
description="Start date"
|
||||
)
|
||||
|
||||
# Enum field
|
||||
status: {ModelName}Status = Field(
|
||||
default={ModelName}Status.PENDING,
|
||||
description="Current status"
|
||||
)
|
||||
|
||||
# List field with constraints
|
||||
tags: List[str] = Field(
|
||||
default_factory=list,
|
||||
max_length=10,
|
||||
description="Associated tags"
|
||||
)
|
||||
|
||||
# Literal type (inline enum)
|
||||
priority: Literal['low', 'medium', 'high'] = Field(
|
||||
default='medium',
|
||||
description="Priority level"
|
||||
)
|
||||
|
||||
|
||||
# Field Validators
|
||||
# ---------------
|
||||
|
||||
@field_validator('name')
|
||||
@classmethod
|
||||
def validate_name(cls, v: str) -> str:
|
||||
"""Validate name format."""
|
||||
if not v.strip():
|
||||
raise ValueError('Name cannot be empty')
|
||||
return v.strip()
|
||||
|
||||
@field_validator('tags')
|
||||
@classmethod
|
||||
def validate_unique_tags(cls, v: List[str]) -> List[str]:
|
||||
"""Ensure tags are unique."""
|
||||
if len(v) != len(set(v)):
|
||||
raise ValueError('Duplicate tags not allowed')
|
||||
return [tag.lower() for tag in v]
|
||||
|
||||
|
||||
# Model Validators (cross-field validation)
|
||||
# ----------------------------------------
|
||||
|
||||
@model_validator(mode='after')
|
||||
def validate_business_rules(self):
|
||||
"""Validate business rules across fields."""
|
||||
# Example: Ensure high-priority items have descriptions
|
||||
if self.priority == 'high' and not self.description:
|
||||
raise ValueError('High priority items must have description')
|
||||
|
||||
return self
|
||||
|
||||
|
||||
# Configuration
|
||||
# ------------
|
||||
|
||||
model_config = {
|
||||
# Strip whitespace from strings
|
||||
'str_strip_whitespace': True,
|
||||
|
||||
# Validate on assignment
|
||||
'validate_assignment': True,
|
||||
|
||||
# JSON schema examples
|
||||
'json_schema_extra': {
|
||||
'examples': [{
|
||||
'name': 'Example {ModelName}',
|
||||
'email': 'example@company.com',
|
||||
'description': 'An example description',
|
||||
'quantity': 5,
|
||||
'price': '99.99',
|
||||
'start_date': '2024-01-01',
|
||||
'status': 'active',
|
||||
'tags': ['tag1', 'tag2'],
|
||||
'priority': 'medium'
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class {ModelName}UpdateSchema(BaseModel):
|
||||
"""
|
||||
Schema for updating a {ModelName}.
|
||||
|
||||
All fields are optional for partial updates.
|
||||
"""
|
||||
|
||||
name: Optional[str] = Field(None, min_length=1, max_length=100)
|
||||
email: Optional[EmailStr] = None
|
||||
description: Optional[str] = Field(None, max_length=500)
|
||||
quantity: Optional[int] = Field(None, ge=1, le=999)
|
||||
price: Optional[Decimal] = Field(None, gt=0, max_digits=10, decimal_places=2)
|
||||
status: Optional[{ModelName}Status] = None
|
||||
tags: Optional[List[str]] = Field(None, max_length=10)
|
||||
priority: Optional[Literal['low', 'medium', 'high']] = None
|
||||
|
||||
model_config = {
|
||||
'str_strip_whitespace': True,
|
||||
'validate_assignment': True
|
||||
}
|
||||
|
||||
|
||||
class {ModelName}ResponseSchema(BaseModel):
|
||||
"""
|
||||
Schema for {ModelName} responses.
|
||||
|
||||
Includes all fields plus auto-generated ones (id, timestamps).
|
||||
"""
|
||||
|
||||
id: UUID
|
||||
name: str
|
||||
email: str
|
||||
description: Optional[str]
|
||||
quantity: int
|
||||
price: Decimal
|
||||
start_date: date
|
||||
status: str
|
||||
tags: List[str]
|
||||
priority: str
|
||||
|
||||
# Auto-generated fields
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
model_config = {
|
||||
# Enable ORM mode for SQLModel compatibility
|
||||
'from_attributes': True
|
||||
}
|
||||
|
||||
|
||||
# Usage Example
|
||||
# -------------
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Valid data
|
||||
data = {
|
||||
'name': 'Test Item',
|
||||
'email': 'test@example.com',
|
||||
'quantity': 10,
|
||||
'price': '49.99',
|
||||
'start_date': '2024-01-01',
|
||||
'status': 'active',
|
||||
'tags': ['electronics', 'featured']
|
||||
}
|
||||
|
||||
# Create instance
|
||||
item = {ModelName}CreateSchema(**data)
|
||||
print(f"Created: {item.model_dump_json()}")
|
||||
|
||||
# Validation will raise errors for invalid data
|
||||
try:
|
||||
invalid_data = {**data, 'quantity': 0} # Invalid quantity
|
||||
{ModelName}CreateSchema(**invalid_data)
|
||||
except ValidationError as e:
|
||||
print(f"Validation error: {e.errors()}")
|
||||
246
skills/data-validation/templates/sqlmodel-model.py
Normal file
246
skills/data-validation/templates/sqlmodel-model.py
Normal file
@@ -0,0 +1,246 @@
|
||||
"""
|
||||
SQLModel Database Model Template
|
||||
|
||||
Copy this template to create new database models.
|
||||
Replace {ModelName} and {table_name} with your actual values.
|
||||
"""
|
||||
|
||||
from sqlmodel import SQLModel, Field, Relationship
|
||||
from datetime import datetime
|
||||
from uuid import UUID, uuid4
|
||||
from decimal import Decimal
|
||||
from typing import Optional, List
|
||||
from enum import Enum
|
||||
|
||||
|
||||
# Optional: Define enum for status field
|
||||
class {ModelName}Status(str, Enum):
|
||||
"""Status enum for {ModelName}."""
|
||||
ACTIVE = 'active'
|
||||
INACTIVE = 'inactive'
|
||||
PENDING = 'pending'
|
||||
|
||||
|
||||
class {ModelName}(SQLModel, table=True):
|
||||
"""
|
||||
{ModelName} database model.
|
||||
|
||||
Represents {table_name} table in PostgreSQL.
|
||||
Includes multi-tenant isolation via tenant_id.
|
||||
"""
|
||||
|
||||
__tablename__ = '{table_name}'
|
||||
|
||||
# Primary Key
|
||||
# -----------
|
||||
id: UUID = Field(
|
||||
default_factory=uuid4,
|
||||
primary_key=True,
|
||||
description="Unique identifier"
|
||||
)
|
||||
|
||||
# Multi-Tenant Isolation (REQUIRED)
|
||||
# ---------------------------------
|
||||
tenant_id: UUID = Field(
|
||||
foreign_key="tenants.id",
|
||||
index=True,
|
||||
description="Tenant for RLS isolation"
|
||||
)
|
||||
|
||||
# Foreign Keys
|
||||
# -----------
|
||||
user_id: UUID = Field(
|
||||
foreign_key="users.id",
|
||||
index=True,
|
||||
description="Owner user ID"
|
||||
)
|
||||
|
||||
# Data Fields
|
||||
# ----------
|
||||
|
||||
# String fields with length constraints
|
||||
name: str = Field(
|
||||
max_length=100,
|
||||
index=True,
|
||||
description="Display name"
|
||||
)
|
||||
|
||||
email: str = Field(
|
||||
max_length=255,
|
||||
unique=True,
|
||||
index=True,
|
||||
description="Email address"
|
||||
)
|
||||
|
||||
# Optional text field
|
||||
description: Optional[str] = Field(
|
||||
default=None,
|
||||
max_length=500,
|
||||
description="Optional description"
|
||||
)
|
||||
|
||||
# Integer field
|
||||
quantity: int = Field(
|
||||
description="Quantity"
|
||||
)
|
||||
|
||||
# Decimal for currency
|
||||
price: Decimal = Field(
|
||||
max_digits=10,
|
||||
decimal_places=2,
|
||||
description="Price in USD"
|
||||
)
|
||||
|
||||
# Enum/Status field
|
||||
status: str = Field(
|
||||
default='pending',
|
||||
max_length=20,
|
||||
index=True,
|
||||
description="Current status"
|
||||
)
|
||||
|
||||
# Boolean field
|
||||
is_active: bool = Field(
|
||||
default=True,
|
||||
description="Active flag"
|
||||
)
|
||||
|
||||
# JSON field (stored as JSONB in PostgreSQL)
|
||||
metadata: Optional[dict] = Field(
|
||||
default=None,
|
||||
sa_column_kwargs={"type_": "JSONB"},
|
||||
description="Additional metadata"
|
||||
)
|
||||
|
||||
# Timestamps (REQUIRED)
|
||||
# --------------------
|
||||
created_at: datetime = Field(
|
||||
default_factory=datetime.utcnow,
|
||||
nullable=False,
|
||||
description="Creation timestamp"
|
||||
)
|
||||
|
||||
updated_at: datetime = Field(
|
||||
default_factory=datetime.utcnow,
|
||||
nullable=False,
|
||||
description="Last update timestamp"
|
||||
)
|
||||
|
||||
# Relationships
|
||||
# ------------
|
||||
|
||||
# One-to-many: This model has many related items
|
||||
# items: List["RelatedItem"] = Relationship(back_populates="{model_name}")
|
||||
|
||||
# Many-to-one: This model belongs to a user
|
||||
# user: Optional["User"] = Relationship(back_populates="{model_name}s")
|
||||
|
||||
|
||||
# Related model example
|
||||
# ---------------------
|
||||
|
||||
class {ModelName}Item(SQLModel, table=True):
|
||||
"""Related items for {ModelName}."""
|
||||
|
||||
__tablename__ = '{table_name}_items'
|
||||
|
||||
id: UUID = Field(default_factory=uuid4, primary_key=True)
|
||||
|
||||
# Foreign key to parent
|
||||
{model_name}_id: UUID = Field(
|
||||
foreign_key="{table_name}.id",
|
||||
index=True
|
||||
)
|
||||
|
||||
# Item fields
|
||||
name: str = Field(max_length=100)
|
||||
quantity: int = Field(gt=0)
|
||||
unit_price: Decimal = Field(max_digits=10, decimal_places=2)
|
||||
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
|
||||
# Relationship back to parent
|
||||
{model_name}: Optional[{ModelName}] = Relationship(back_populates="items")
|
||||
|
||||
|
||||
# Update parent model with relationship
|
||||
{ModelName}.items = Relationship(back_populates="{model_name}")
|
||||
|
||||
|
||||
# Database Migration
|
||||
# ------------------
|
||||
|
||||
"""
|
||||
After creating this model, generate a migration:
|
||||
|
||||
```bash
|
||||
# Using Alembic
|
||||
alembic revision --autogenerate -m "create {table_name} table"
|
||||
alembic upgrade head
|
||||
```
|
||||
|
||||
Migration will create:
|
||||
- Table {table_name}
|
||||
- Indexes on tenant_id, user_id, name, email, status
|
||||
- Unique constraint on email
|
||||
- Foreign key constraints
|
||||
- Timestamps with defaults
|
||||
"""
|
||||
|
||||
|
||||
# Row-Level Security (RLS)
|
||||
# ------------------------
|
||||
|
||||
"""
|
||||
Enable RLS for multi-tenant isolation:
|
||||
|
||||
```sql
|
||||
-- Enable RLS
|
||||
ALTER TABLE {table_name} ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Create policy
|
||||
CREATE POLICY tenant_isolation ON {table_name}
|
||||
USING (tenant_id = current_setting('app.tenant_id')::UUID);
|
||||
|
||||
-- Grant access
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON {table_name} TO app_user;
|
||||
```
|
||||
"""
|
||||
|
||||
|
||||
# Usage Example
|
||||
# -------------
|
||||
|
||||
if __name__ == '__main__':
|
||||
from sqlmodel import Session, create_engine, select
|
||||
|
||||
# Create engine
|
||||
engine = create_engine('postgresql://user:pass@localhost/db')
|
||||
|
||||
# Create tables
|
||||
SQLModel.metadata.create_all(engine)
|
||||
|
||||
# Insert data
|
||||
with Session(engine) as session:
|
||||
item = {ModelName}(
|
||||
tenant_id=uuid4(),
|
||||
user_id=uuid4(),
|
||||
name='Test Item',
|
||||
email='test@example.com',
|
||||
quantity=10,
|
||||
price=Decimal('49.99'),
|
||||
status='active'
|
||||
)
|
||||
|
||||
session.add(item)
|
||||
session.commit()
|
||||
session.refresh(item)
|
||||
|
||||
print(f"Created {ModelName}: {item.id}")
|
||||
|
||||
# Query data
|
||||
with Session(engine) as session:
|
||||
statement = select({ModelName}).where({ModelName}.status == 'active')
|
||||
results = session.exec(statement).all()
|
||||
|
||||
print(f"Found {len(results)} active items")
|
||||
278
skills/database-conventions/SKILL.md
Normal file
278
skills/database-conventions/SKILL.md
Normal file
@@ -0,0 +1,278 @@
|
||||
---
|
||||
name: grey-haven-database-conventions
|
||||
description: "Apply Grey Haven database conventions - snake_case fields, multi-tenant with tenant_id and RLS, proper indexing, migrations for Drizzle (TypeScript) and SQLModel (Python). Use when designing schemas, writing database code, creating migrations, setting up RLS policies, or when user mentions 'database', 'schema', 'Drizzle', 'SQLModel', 'migration', 'RLS', 'tenant_id', 'snake_case', 'indexes', or 'foreign keys'."
|
||||
---
|
||||
|
||||
# Grey Haven Database Conventions
|
||||
|
||||
**Database schema standards for Drizzle ORM (TypeScript) and SQLModel (Python).**
|
||||
|
||||
Follow these conventions for all Grey Haven multi-tenant database schemas.
|
||||
|
||||
## Supporting Documentation
|
||||
|
||||
- **[examples/](examples/)** - Complete schema examples (all files <500 lines)
|
||||
- [drizzle-schemas.md](examples/drizzle-schemas.md) - TypeScript/Drizzle examples
|
||||
- [sqlmodel-schemas.md](examples/sqlmodel-schemas.md) - Python/SQLModel examples
|
||||
- [migrations.md](examples/migrations.md) - Migration patterns
|
||||
- [rls-policies.md](examples/rls-policies.md) - Row Level Security
|
||||
- **[reference/](reference/)** - Detailed references (all files <500 lines)
|
||||
- [field-naming.md](reference/field-naming.md) - Naming conventions
|
||||
- [indexing.md](reference/indexing.md) - Index patterns
|
||||
- [relationships.md](reference/relationships.md) - Foreign keys and relations
|
||||
- **[templates/](templates/)** - Copy-paste schema templates
|
||||
- **[checklists/](checklists/)** - Schema validation checklists
|
||||
|
||||
## Critical Rules
|
||||
|
||||
### 1. snake_case Fields (ALWAYS)
|
||||
|
||||
**Database columns MUST use snake_case, never camelCase.**
|
||||
|
||||
```typescript
|
||||
// ✅ CORRECT
|
||||
export const users = pgTable("users", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
created_at: timestamp("created_at").defaultNow().notNull(),
|
||||
tenant_id: uuid("tenant_id").notNull(),
|
||||
email_address: text("email_address").notNull(),
|
||||
});
|
||||
|
||||
// ❌ WRONG - Don't use camelCase
|
||||
export const users = pgTable("users", {
|
||||
createdAt: timestamp("createdAt"), // WRONG!
|
||||
tenantId: uuid("tenantId"), // WRONG!
|
||||
});
|
||||
```
|
||||
|
||||
### 2. tenant_id Required (Multi-Tenant)
|
||||
|
||||
**Every table MUST include tenant_id for data isolation.**
|
||||
|
||||
```typescript
|
||||
// TypeScript - Drizzle
|
||||
export const organizations = pgTable("organizations", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
tenant_id: uuid("tenant_id").notNull(), // REQUIRED
|
||||
name: text("name").notNull(),
|
||||
});
|
||||
```
|
||||
|
||||
```python
|
||||
# Python - SQLModel
|
||||
class Organization(SQLModel, table=True):
|
||||
id: UUID = Field(default_factory=uuid4, primary_key=True)
|
||||
tenant_id: UUID = Field(foreign_key="tenants.id", index=True) # REQUIRED
|
||||
name: str = Field(max_length=255)
|
||||
```
|
||||
|
||||
**See [examples/drizzle-schemas.md](examples/drizzle-schemas.md) and [examples/sqlmodel-schemas.md](examples/sqlmodel-schemas.md) for complete examples.**
|
||||
|
||||
### 3. Standard Timestamps
|
||||
|
||||
**All tables must have created_at and updated_at.**
|
||||
|
||||
```typescript
|
||||
// TypeScript - Reusable timestamps
|
||||
export const baseTimestamps = {
|
||||
created_at: timestamp("created_at").defaultNow().notNull(),
|
||||
updated_at: timestamp("updated_at").defaultNow().notNull().$onUpdate(() => new Date()),
|
||||
};
|
||||
|
||||
export const teams = pgTable("teams", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
...baseTimestamps, // Spread operator
|
||||
tenant_id: uuid("tenant_id").notNull(),
|
||||
name: text("name").notNull(),
|
||||
});
|
||||
```
|
||||
|
||||
```python
|
||||
# Python - Mixin pattern
|
||||
class TimestampMixin:
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
updated_at: datetime = Field(default_factory=datetime.utcnow, sa_column_kwargs={"onupdate": datetime.utcnow})
|
||||
|
||||
class Team(TimestampMixin, SQLModel, table=True):
|
||||
id: UUID = Field(default_factory=uuid4, primary_key=True)
|
||||
tenant_id: UUID = Field(index=True)
|
||||
name: str = Field(max_length=255)
|
||||
```
|
||||
|
||||
### 4. Row Level Security (RLS)
|
||||
|
||||
**Enable RLS on all tables with tenant_id.**
|
||||
|
||||
```sql
|
||||
-- Enable RLS
|
||||
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Tenant isolation policy
|
||||
CREATE POLICY "tenant_isolation" ON users
|
||||
FOR ALL TO authenticated
|
||||
USING (tenant_id = (current_setting('request.jwt.claims')::json->>'tenant_id')::uuid);
|
||||
```
|
||||
|
||||
**See [examples/rls-policies.md](examples/rls-policies.md) for complete RLS patterns.**
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Field Naming Patterns
|
||||
|
||||
**Boolean fields:** Prefix with `is_`, `has_`, `can_`
|
||||
```typescript
|
||||
is_active: boolean("is_active")
|
||||
has_access: boolean("has_access")
|
||||
can_edit: boolean("can_edit")
|
||||
```
|
||||
|
||||
**Timestamp fields:** Suffix with `_at`
|
||||
```typescript
|
||||
created_at: timestamp("created_at")
|
||||
updated_at: timestamp("updated_at")
|
||||
deleted_at: timestamp("deleted_at")
|
||||
last_login_at: timestamp("last_login_at")
|
||||
```
|
||||
|
||||
**Foreign keys:** Suffix with `_id`
|
||||
```typescript
|
||||
tenant_id: uuid("tenant_id")
|
||||
user_id: uuid("user_id")
|
||||
organization_id: uuid("organization_id")
|
||||
```
|
||||
|
||||
**See [reference/field-naming.md](reference/field-naming.md) for complete naming guide.**
|
||||
|
||||
### Indexing Patterns
|
||||
|
||||
**Always index:**
|
||||
- `tenant_id` (for multi-tenant queries)
|
||||
- Foreign keys (for joins)
|
||||
- Unique constraints (email, slug)
|
||||
- Frequently queried fields
|
||||
|
||||
```typescript
|
||||
// Composite index for tenant + lookup
|
||||
export const usersIndex = index("users_tenant_email_idx").on(
|
||||
users.tenant_id,
|
||||
users.email_address
|
||||
);
|
||||
```
|
||||
|
||||
**See [reference/indexing.md](reference/indexing.md) for index strategies.**
|
||||
|
||||
### Relationships
|
||||
|
||||
**One-to-many:**
|
||||
```typescript
|
||||
export const usersRelations = relations(users, ({ many }) => ({
|
||||
posts: many(posts), // User has many posts
|
||||
}));
|
||||
|
||||
export const postsRelations = relations(posts, ({ one }) => ({
|
||||
author: one(users, { fields: [posts.user_id], references: [users.id] }),
|
||||
}));
|
||||
```
|
||||
|
||||
**See [reference/relationships.md](reference/relationships.md) for all relationship patterns.**
|
||||
|
||||
## Drizzle ORM (TypeScript)
|
||||
|
||||
**Installation:**
|
||||
```bash
|
||||
bun add drizzle-orm postgres
|
||||
bun add -d drizzle-kit
|
||||
```
|
||||
|
||||
**Basic schema:**
|
||||
```typescript
|
||||
// db/schema.ts
|
||||
import { pgTable, uuid, text, timestamp, boolean } from "drizzle-orm/pg-core";
|
||||
|
||||
export const users = pgTable("users", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
created_at: timestamp("created_at").defaultNow().notNull(),
|
||||
updated_at: timestamp("updated_at").defaultNow().notNull(),
|
||||
tenant_id: uuid("tenant_id").notNull(),
|
||||
email_address: text("email_address").notNull().unique(),
|
||||
is_active: boolean("is_active").default(true).notNull(),
|
||||
});
|
||||
```
|
||||
|
||||
**Generate migration:**
|
||||
```bash
|
||||
bun run drizzle-kit generate:pg
|
||||
bun run drizzle-kit push:pg
|
||||
```
|
||||
|
||||
**See [examples/migrations.md](examples/migrations.md) for migration workflow.**
|
||||
|
||||
## SQLModel (Python)
|
||||
|
||||
**Installation:**
|
||||
```bash
|
||||
pip install sqlmodel psycopg2-binary
|
||||
```
|
||||
|
||||
**Basic model:**
|
||||
```python
|
||||
# app/models/user.py
|
||||
from sqlmodel import Field, SQLModel
|
||||
from uuid import UUID, uuid4
|
||||
from datetime import datetime
|
||||
|
||||
class User(SQLModel, table=True):
|
||||
__tablename__ = "users"
|
||||
|
||||
id: UUID = Field(default_factory=uuid4, primary_key=True)
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
tenant_id: UUID = Field(foreign_key="tenants.id", index=True)
|
||||
email_address: str = Field(unique=True, index=True, max_length=255)
|
||||
is_active: bool = Field(default=True)
|
||||
```
|
||||
|
||||
**Generate migration:**
|
||||
```bash
|
||||
alembic revision --autogenerate -m "Add users table"
|
||||
alembic upgrade head
|
||||
```
|
||||
|
||||
**See [examples/migrations.md](examples/migrations.md) for Alembic setup.**
|
||||
|
||||
## When to Apply This Skill
|
||||
|
||||
Use this skill when:
|
||||
- ✅ Designing new database schemas
|
||||
- ✅ Creating Drizzle or SQLModel models
|
||||
- ✅ Writing database migrations
|
||||
- ✅ Setting up RLS policies
|
||||
- ✅ Adding indexes for performance
|
||||
- ✅ Defining table relationships
|
||||
- ✅ Reviewing database code in PRs
|
||||
- ✅ User mentions: "database", "schema", "Drizzle", "SQLModel", "migration", "RLS", "tenant_id", "snake_case"
|
||||
|
||||
## Template References
|
||||
|
||||
- **TypeScript**: `cvi-template` (Drizzle ORM + PlanetScale)
|
||||
- **Python**: `cvi-backend-template` (SQLModel + PostgreSQL)
|
||||
|
||||
## Critical Reminders
|
||||
|
||||
1. **snake_case** - ALL database fields use snake_case (never camelCase)
|
||||
2. **tenant_id** - Required on all tables for multi-tenant isolation
|
||||
3. **Timestamps** - created_at and updated_at on all tables
|
||||
4. **RLS policies** - Enable on all tables with tenant_id
|
||||
5. **Indexing** - Index tenant_id, foreign keys, and unique fields
|
||||
6. **Migrations** - Always use migrations (Drizzle Kit or Alembic)
|
||||
7. **Field naming** - Booleans use is_/has_/can_ prefix, timestamps use _at suffix
|
||||
8. **No raw SQL** - Use ORM for queries (prevents SQL injection)
|
||||
9. **Soft deletes** - Use deleted_at timestamp, not hard deletes
|
||||
10. **Foreign keys** - Always define relationships explicitly
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **Need examples?** See [examples/](examples/) for Drizzle and SQLModel schemas
|
||||
- **Need references?** See [reference/](reference/) for naming, indexing, relationships
|
||||
- **Need templates?** See [templates/](templates/) for copy-paste schema starters
|
||||
- **Need checklists?** Use [checklists/](checklists/) for schema validation
|
||||
@@ -0,0 +1,28 @@
|
||||
# Migration Checklist
|
||||
|
||||
**Use before applying database migrations.**
|
||||
|
||||
## Before Migration
|
||||
|
||||
- [ ] Backup database
|
||||
- [ ] Test migration in development
|
||||
- [ ] Test migration rollback
|
||||
- [ ] Review generated SQL
|
||||
- [ ] Check for breaking changes
|
||||
- [ ] Coordinate with team if breaking
|
||||
|
||||
## Migration Quality
|
||||
|
||||
- [ ] Migration is reversible (has downgrade)
|
||||
- [ ] No data loss
|
||||
- [ ] Preserves existing data
|
||||
- [ ] Handles NULL values correctly
|
||||
- [ ] Default values provided for NOT NULL
|
||||
|
||||
## After Migration
|
||||
|
||||
- [ ] Migration applied successfully
|
||||
- [ ] Application tested
|
||||
- [ ] Rollback tested
|
||||
- [ ] Performance verified
|
||||
- [ ] No errors in logs
|
||||
91
skills/database-conventions/checklists/schema-checklist.md
Normal file
91
skills/database-conventions/checklists/schema-checklist.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# Database Schema Checklist
|
||||
|
||||
**Use before creating PR for new database schemas.**
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
- [ ] All field names use snake_case (NOT camelCase)
|
||||
- [ ] Boolean fields use is_/has_/can_ prefix
|
||||
- [ ] Timestamp fields use _at suffix
|
||||
- [ ] Foreign keys use _id suffix
|
||||
- [ ] Email field named `email_address` (not `email`)
|
||||
- [ ] Phone field named `phone_number` (not `phone`)
|
||||
|
||||
## Multi-Tenant Requirements
|
||||
|
||||
- [ ] tenant_id field present on all tables
|
||||
- [ ] tenant_id has NOT NULL constraint
|
||||
- [ ] tenant_id has index for performance
|
||||
- [ ] tenant_id has foreign key to tenants table
|
||||
- [ ] RLS policy created for tenant isolation
|
||||
- [ ] Test cases verify tenant isolation
|
||||
|
||||
## Standard Fields
|
||||
|
||||
- [ ] id field (UUID primary key)
|
||||
- [ ] created_at timestamp (NOT NULL, default now())
|
||||
- [ ] updated_at timestamp (NOT NULL, auto-update)
|
||||
- [ ] tenant_id (NOT NULL, indexed)
|
||||
- [ ] deleted_at timestamp (for soft delete)
|
||||
|
||||
## Indexes
|
||||
|
||||
- [ ] tenant_id indexed
|
||||
- [ ] Foreign keys indexed
|
||||
- [ ] Unique fields indexed
|
||||
- [ ] Frequently queried fields indexed
|
||||
- [ ] Composite indexes for common query patterns
|
||||
|
||||
## Relationships
|
||||
|
||||
- [ ] Foreign keys defined explicitly
|
||||
- [ ] Relationships documented in schema
|
||||
- [ ] Cascade delete configured appropriately
|
||||
- [ ] Join tables for many-to-many
|
||||
|
||||
## Migrations
|
||||
|
||||
- [ ] Migration generated (Drizzle Kit or Alembic)
|
||||
- [ ] Migration tested locally
|
||||
- [ ] Migration reversible (has downgrade)
|
||||
- [ ] Migration reviewed by teammate
|
||||
- [ ] No breaking changes without coordination
|
||||
|
||||
## Row Level Security
|
||||
|
||||
- [ ] RLS enabled on table
|
||||
- [ ] Tenant isolation policy created
|
||||
- [ ] Admin override policy (if needed)
|
||||
- [ ] Anonymous policy (if public data)
|
||||
- [ ] RLS tested with different roles
|
||||
|
||||
## Performance
|
||||
|
||||
- [ ] Appropriate indexes created
|
||||
- [ ] No N+1 query patterns
|
||||
- [ ] Large text fields use TEXT (not VARCHAR)
|
||||
- [ ] Consider partitioning for large tables
|
||||
|
||||
## Security
|
||||
|
||||
- [ ] No sensitive data in plain text
|
||||
- [ ] Passwords hashed (never store plain)
|
||||
- [ ] PII properly handled
|
||||
- [ ] Input validation at model level
|
||||
- [ ] SQL injection prevented (using ORM)
|
||||
|
||||
## Testing
|
||||
|
||||
- [ ] Model tests written
|
||||
- [ ] Migration tested
|
||||
- [ ] Relationships tested
|
||||
- [ ] Tenant isolation tested
|
||||
- [ ] Unique constraints tested
|
||||
- [ ] Foreign key constraints tested
|
||||
|
||||
## Documentation
|
||||
|
||||
- [ ] Schema documented in code comments
|
||||
- [ ] README updated if needed
|
||||
- [ ] ERD diagram updated (if exists)
|
||||
- [ ] Migration notes added
|
||||
8
skills/database-conventions/examples/INDEX.md
Normal file
8
skills/database-conventions/examples/INDEX.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Database Convention Examples Index
|
||||
|
||||
**All files under 500 lines for optimal loading.**
|
||||
|
||||
- **[drizzle-schemas.md](drizzle-schemas.md)** - TypeScript/Drizzle ORM examples
|
||||
- **[sqlmodel-schemas.md](sqlmodel-schemas.md)** - Python/SQLModel examples
|
||||
- **[migrations.md](migrations.md)** - Migration patterns for both stacks
|
||||
- **[rls-policies.md](rls-policies.md)** - Row Level Security examples
|
||||
87
skills/database-conventions/examples/drizzle-schemas.md
Normal file
87
skills/database-conventions/examples/drizzle-schemas.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# Drizzle Schema Examples
|
||||
|
||||
**Complete TypeScript/Drizzle ORM schema patterns.**
|
||||
|
||||
## Basic Table with Multi-Tenant
|
||||
|
||||
```typescript
|
||||
// db/schema/users.ts
|
||||
import { pgTable, uuid, text, timestamp, boolean, index } from "drizzle-orm/pg-core";
|
||||
|
||||
export const users = pgTable("users", {
|
||||
// Primary key
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
|
||||
// Timestamps (required on all tables)
|
||||
created_at: timestamp("created_at").defaultNow().notNull(),
|
||||
updated_at: timestamp("updated_at").defaultNow().notNull().$onUpdate(() => new Date()),
|
||||
|
||||
// Multi-tenant (required on all tables)
|
||||
tenant_id: uuid("tenant_id").notNull(),
|
||||
|
||||
// User fields
|
||||
email_address: text("email_address").notNull().unique(),
|
||||
full_name: text("full_name").notNull(),
|
||||
hashed_password: text("hashed_password").notNull(),
|
||||
is_active: boolean("is_active").default(true).notNull(),
|
||||
is_verified: boolean("is_verified").default(false).notNull(),
|
||||
last_login_at: timestamp("last_login_at"),
|
||||
|
||||
// Soft delete
|
||||
deleted_at: timestamp("deleted_at"),
|
||||
});
|
||||
|
||||
// Indexes
|
||||
export const usersIndex = index("users_tenant_id_idx").on(users.tenant_id);
|
||||
export const usersEmailIndex = index("users_email_idx").on(users.email_address);
|
||||
```
|
||||
|
||||
## Reusable Timestamp Pattern
|
||||
|
||||
```typescript
|
||||
// db/schema/base.ts
|
||||
export const baseTimestamps = {
|
||||
created_at: timestamp("created_at").defaultNow().notNull(),
|
||||
updated_at: timestamp("updated_at").defaultNow().notNull().$onUpdate(() => new Date()),
|
||||
};
|
||||
|
||||
// Use in tables
|
||||
export const teams = pgTable("teams", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
...baseTimestamps,
|
||||
tenant_id: uuid("tenant_id").notNull(),
|
||||
name: text("name").notNull(),
|
||||
});
|
||||
```
|
||||
|
||||
## One-to-Many Relationships
|
||||
|
||||
```typescript
|
||||
// db/schema/posts.ts
|
||||
import { relations } from "drizzle-orm";
|
||||
|
||||
export const posts = pgTable("posts", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
created_at: timestamp("created_at").defaultNow().notNull(),
|
||||
updated_at: timestamp("updated_at").defaultNow().notNull(),
|
||||
tenant_id: uuid("tenant_id").notNull(),
|
||||
|
||||
// Foreign key to users
|
||||
author_id: uuid("author_id").notNull(),
|
||||
|
||||
title: text("title").notNull(),
|
||||
content: text("content").notNull(),
|
||||
is_published: boolean("is_published").default(false).notNull(),
|
||||
});
|
||||
|
||||
// Define relations
|
||||
export const usersRelations = relations(users, ({ many }) => ({
|
||||
posts: many(posts), // User has many posts
|
||||
}));
|
||||
|
||||
export const postsRelations = relations(posts, ({ one }) => ({
|
||||
author: one(users, { fields: [posts.author_id], references: [users.id] }),
|
||||
}));
|
||||
```
|
||||
|
||||
**See [../templates/drizzle-table.ts](../templates/drizzle-table.ts) for complete template.**
|
||||
31
skills/database-conventions/examples/migrations.md
Normal file
31
skills/database-conventions/examples/migrations.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Migration Examples
|
||||
|
||||
**Migration patterns for Drizzle (TypeScript) and Alembic (Python).**
|
||||
|
||||
## Drizzle Migrations (TypeScript)
|
||||
|
||||
```bash
|
||||
# Generate migration
|
||||
bun run drizzle-kit generate:pg
|
||||
|
||||
# Apply migration
|
||||
bun run drizzle-kit push:pg
|
||||
|
||||
# Check migration status
|
||||
bun run drizzle-kit check:pg
|
||||
```
|
||||
|
||||
## Alembic Migrations (Python)
|
||||
|
||||
```bash
|
||||
# Generate migration
|
||||
alembic revision --autogenerate -m "Add users table"
|
||||
|
||||
# Apply migration
|
||||
alembic upgrade head
|
||||
|
||||
# Rollback
|
||||
alembic downgrade -1
|
||||
```
|
||||
|
||||
**See [../reference/migrations.md](../reference/migrations.md) for complete setup.**
|
||||
27
skills/database-conventions/examples/rls-policies.md
Normal file
27
skills/database-conventions/examples/rls-policies.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Row Level Security Examples
|
||||
|
||||
**RLS policy patterns for multi-tenant isolation.**
|
||||
|
||||
## Enable RLS
|
||||
|
||||
```sql
|
||||
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
|
||||
```
|
||||
|
||||
## Tenant Isolation Policy
|
||||
|
||||
```sql
|
||||
CREATE POLICY "tenant_isolation" ON users
|
||||
FOR ALL TO authenticated
|
||||
USING (tenant_id = (current_setting('request.jwt.claims')::json->>'tenant_id')::uuid);
|
||||
```
|
||||
|
||||
## Admin Override Policy
|
||||
|
||||
```sql
|
||||
CREATE POLICY "admin_access" ON users
|
||||
FOR ALL TO admin
|
||||
USING (true);
|
||||
```
|
||||
|
||||
**See [../reference/rls-policies.md](../reference/rls-policies.md) for complete RLS guide.**
|
||||
102
skills/database-conventions/examples/sqlmodel-schemas.md
Normal file
102
skills/database-conventions/examples/sqlmodel-schemas.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# SQLModel Schema Examples
|
||||
|
||||
**Complete Python/SQLModel schema patterns.**
|
||||
|
||||
## Basic Model with Multi-Tenant
|
||||
|
||||
```python
|
||||
# app/models/user.py
|
||||
from sqlmodel import Field, SQLModel
|
||||
from uuid import UUID, uuid4
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
class User(SQLModel, table=True):
|
||||
"""User model with multi-tenant isolation."""
|
||||
|
||||
__tablename__ = "users"
|
||||
|
||||
# Primary key
|
||||
id: UUID = Field(default_factory=uuid4, primary_key=True)
|
||||
|
||||
# Timestamps (required on all tables)
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
updated_at: datetime = Field(
|
||||
default_factory=datetime.utcnow,
|
||||
sa_column_kwargs={"onupdate": datetime.utcnow}
|
||||
)
|
||||
|
||||
# Multi-tenant (required on all tables)
|
||||
tenant_id: UUID = Field(foreign_key="tenants.id", index=True)
|
||||
|
||||
# User fields
|
||||
email_address: str = Field(unique=True, index=True, max_length=255)
|
||||
full_name: str = Field(max_length=255)
|
||||
hashed_password: str = Field(max_length=255)
|
||||
is_active: bool = Field(default=True)
|
||||
is_verified: bool = Field(default=False)
|
||||
last_login_at: Optional[datetime] = None
|
||||
|
||||
# Soft delete
|
||||
deleted_at: Optional[datetime] = None
|
||||
```
|
||||
|
||||
## Reusable Timestamp Mixin
|
||||
|
||||
```python
|
||||
# app/models/base.py
|
||||
from sqlmodel import Field
|
||||
from datetime import datetime
|
||||
|
||||
class TimestampMixin:
|
||||
"""Mixin for created_at and updated_at timestamps."""
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
updated_at: datetime = Field(
|
||||
default_factory=datetime.utcnow,
|
||||
sa_column_kwargs={"onupdate": datetime.utcnow}
|
||||
)
|
||||
|
||||
# Use in models
|
||||
class Team(TimestampMixin, SQLModel, table=True):
|
||||
__tablename__ = "teams"
|
||||
id: UUID = Field(default_factory=uuid4, primary_key=True)
|
||||
tenant_id: UUID = Field(foreign_key="tenants.id", index=True)
|
||||
name: str = Field(max_length=255)
|
||||
```
|
||||
|
||||
## One-to-Many Relationships
|
||||
|
||||
```python
|
||||
# app/models/post.py
|
||||
from sqlmodel import Field, SQLModel, Relationship
|
||||
from uuid import UUID, uuid4
|
||||
from datetime import datetime
|
||||
from typing import Optional, List
|
||||
|
||||
class Post(SQLModel, table=True):
|
||||
"""Post model with author relationship."""
|
||||
|
||||
__tablename__ = "posts"
|
||||
|
||||
id: UUID = Field(default_factory=uuid4, primary_key=True)
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
tenant_id: UUID = Field(foreign_key="tenants.id", index=True)
|
||||
|
||||
# Foreign key to users
|
||||
author_id: UUID = Field(foreign_key="users.id", index=True)
|
||||
|
||||
title: str = Field(max_length=255)
|
||||
content: str
|
||||
is_published: bool = Field(default=False)
|
||||
|
||||
# Relationship
|
||||
author: Optional["User"] = Relationship(back_populates="posts")
|
||||
|
||||
# Add to User model
|
||||
class User(TimestampMixin, SQLModel, table=True):
|
||||
# ... (previous fields)
|
||||
posts: List["Post"] = Relationship(back_populates="author")
|
||||
```
|
||||
|
||||
**See [../templates/sqlmodel-table.py](../templates/sqlmodel-table.py) for complete template.**
|
||||
5
skills/database-conventions/reference/INDEX.md
Normal file
5
skills/database-conventions/reference/INDEX.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Database Conventions Reference Index
|
||||
|
||||
- **[field-naming.md](field-naming.md)** - Complete naming conventions
|
||||
- **[indexing.md](indexing.md)** - Index patterns and strategies
|
||||
- **[relationships.md](relationships.md)** - Foreign keys and relations
|
||||
7
skills/database-conventions/reference/field-naming.md
Normal file
7
skills/database-conventions/reference/field-naming.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Field Naming Conventions
|
||||
|
||||
**Boolean:** `is_active`, `has_access`, `can_edit`
|
||||
**Timestamp:** `created_at`, `updated_at`, `deleted_at`
|
||||
**Foreign Key:** `user_id`, `tenant_id`, `organization_id`
|
||||
**Email:** `email_address` (not `email`)
|
||||
**Phone:** `phone_number` (not `phone`)
|
||||
12
skills/database-conventions/reference/indexing.md
Normal file
12
skills/database-conventions/reference/indexing.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# Indexing Strategies
|
||||
|
||||
**Always index:**
|
||||
- tenant_id
|
||||
- Foreign keys
|
||||
- Unique constraints
|
||||
- Frequently queried fields
|
||||
|
||||
**Composite indexes:**
|
||||
```typescript
|
||||
index("users_tenant_email_idx").on(users.tenant_id, users.email_address)
|
||||
```
|
||||
7
skills/database-conventions/reference/relationships.md
Normal file
7
skills/database-conventions/reference/relationships.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Relationships Guide
|
||||
|
||||
**One-to-Many:** User has many Posts
|
||||
**Many-to-One:** Post belongs to User
|
||||
**Many-to-Many:** User has many Roles through UserRoles
|
||||
|
||||
See examples for complete patterns.
|
||||
28
skills/database-conventions/templates/drizzle-table.ts
Normal file
28
skills/database-conventions/templates/drizzle-table.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
// Grey Haven Studio - Drizzle Table Template
|
||||
// Copy this template for new database tables
|
||||
|
||||
import { pgTable, uuid, text, timestamp, boolean, index } from "drizzle-orm/pg-core";
|
||||
|
||||
// TODO: Update table name
|
||||
export const resources = pgTable("resources", {
|
||||
// Primary key
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
|
||||
// Timestamps (REQUIRED on all tables)
|
||||
created_at: timestamp("created_at").defaultNow().notNull(),
|
||||
updated_at: timestamp("updated_at").defaultNow().notNull().$onUpdate(() => new Date()),
|
||||
|
||||
// Multi-tenant (REQUIRED on all tables)
|
||||
tenant_id: uuid("tenant_id").notNull(),
|
||||
|
||||
// TODO: Add your fields here (use snake_case!)
|
||||
name: text("name").notNull(),
|
||||
description: text("description"),
|
||||
is_active: boolean("is_active").default(true).notNull(),
|
||||
|
||||
// Soft delete (optional but recommended)
|
||||
deleted_at: timestamp("deleted_at"),
|
||||
});
|
||||
|
||||
// Indexes (REQUIRED for tenant_id)
|
||||
export const resourcesIndex = index("resources_tenant_id_idx").on(resources.tenant_id);
|
||||
36
skills/database-conventions/templates/sqlmodel-table.py
Normal file
36
skills/database-conventions/templates/sqlmodel-table.py
Normal file
@@ -0,0 +1,36 @@
|
||||
# Grey Haven Studio - SQLModel Table Template
|
||||
# Copy this template for new database tables
|
||||
|
||||
from sqlmodel import Field, SQLModel
|
||||
from uuid import UUID, uuid4
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
# Reusable timestamp mixin
|
||||
class TimestampMixin:
|
||||
"""Mixin for created_at and updated_at timestamps."""
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
updated_at: datetime = Field(
|
||||
default_factory=datetime.utcnow,
|
||||
sa_column_kwargs={"onupdate": datetime.utcnow}
|
||||
)
|
||||
|
||||
# TODO: Update class and table name
|
||||
class Resource(TimestampMixin, SQLModel, table=True):
|
||||
"""Resource model with multi-tenant isolation."""
|
||||
|
||||
__tablename__ = "resources"
|
||||
|
||||
# Primary key
|
||||
id: UUID = Field(default_factory=uuid4, primary_key=True)
|
||||
|
||||
# Multi-tenant (REQUIRED on all tables)
|
||||
tenant_id: UUID = Field(foreign_key="tenants.id", index=True)
|
||||
|
||||
# TODO: Add your fields here (use snake_case!)
|
||||
name: str = Field(max_length=255)
|
||||
description: Optional[str] = None
|
||||
is_active: bool = Field(default=True)
|
||||
|
||||
# Soft delete (optional but recommended)
|
||||
deleted_at: Optional[datetime] = None
|
||||
Reference in New Issue
Block a user