Initial commit
This commit is contained in:
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