Files
metabuilder/docs/architecture/data/database.md

333 lines
7.8 KiB
Markdown

# Database Architecture
MetaBuilder uses **Prisma ORM** as the data access layer with support for **multi-tenant** architecture and **DBAL** (Database Abstraction Layer) for server-side operations.
## Architecture Overview
```
┌─────────────────────────────────────────────┐
│ React Components (Client) │
├─────────────────────────────────────────────┤
│ useDatabase() hooks / API Routes │
├─────────────────────────────────────────────┤
│ Database Layer │
│ ├─ Prisma ORM (Client-side/API) │
│ └─ DBAL (Server-side only) │
├─────────────────────────────────────────────┤
│ Database Driver │
│ ├─ SQLite (Development) │
│ └─ PostgreSQL (Production) │
├─────────────────────────────────────────────┤
│ Persistent Storage │
└─────────────────────────────────────────────┘
```
## Prisma ORM
**File**: `prisma/schema.prisma`
Prisma provides:
- Type-safe database access
- Automatic migrations
- Query builder
- Relationship management
### Schema Structure
```prisma
// Define tables as models
model User {
id String @id @default(cuid())
email String @unique
password String
level Int
tenant Tenant @relation(fields: [tenantId], references: [id])
tenantId String
workflows Workflow[]
@@unique([email, tenantId])
}
model Workflow {
id String @id @default(cuid())
name String
config String @db.Text
user User @relation(fields: [userId], references: [id])
userId String
tenant Tenant @relation(fields: [tenantId], references: [id])
tenantId String
}
model Tenant {
id String @id @default(cuid())
name String @unique
users User[]
workflows Workflow[]
createdAt DateTime @default(now())
}
```
## Multi-Tenant Design
All tables include `tenantId` for data isolation:
```typescript
// Always filter by tenant
const userWorkflows = await prisma.workflow.findMany({
where: {
tenantId: user.tenantId,
userId: user.id,
},
})
```
### Tenant Benefits
- Complete data isolation between customers
- Independent scaling per tenant
- Separate backups and recovery
- Regulatory compliance (data residency)
## DBAL (Database Abstraction Layer)
**File**: `src/lib/database-dbal.server.ts`
Advanced server-side database operations:
```typescript
// Server-side only - compile-time enforcement
import { dbalQuery } from '@/lib/database-dbal.server'
// Atomic operations with validation
const result = await dbalQuery({
operation: 'transfer_workflow',
from: user1Id,
to: user2Id,
})
```
### DBAL vs Prisma
| Feature | Prisma | DBAL |
|---------|--------|------|
| **Location** | Client + Server | Server only |
| **Use Case** | Standard CRUD | Complex operations |
| **Transactions** | Basic | Advanced with rollback |
| **Validation** | Schema | Custom rules |
| **Performance** | Good | Optimized |
### Primary Key Mapping
DBAL adapters normalize primary key lookups for Prisma models that use non-`id` keys. The adapter maps the entity name to its primary key field so standard `read`/`update`/`delete` calls work correctly.
Non-`id` primary keys in this repo:
- `Credential.username`
- `InstalledPackage.packageId`
- `PackageData.packageId`
- `PasswordResetToken.username`
- `SystemConfig.key`
## Database Operations
### Create (Insert)
```typescript
const newUser = await prisma.user.create({
data: {
email: 'user@example.com',
password: hashPassword('password'),
level: 2,
tenant: {
connect: { id: tenantId }
},
},
})
```
### Read (Query)
```typescript
// Single record
const user = await prisma.user.findUnique({
where: { id: userId },
})
// Multiple records with filters
const workflows = await prisma.workflow.findMany({
where: {
tenantId: user.tenantId,
status: 'active',
},
orderBy: { createdAt: 'desc' },
take: 10,
})
```
### Update
```typescript
const updated = await prisma.user.update({
where: { id: userId },
data: {
email: 'newemail@example.com',
level: 3,
},
})
```
### Delete
```typescript
await prisma.user.delete({
where: { id: userId },
})
```
### Relationships
```typescript
// Include related data
const user = await prisma.user.findUnique({
where: { id: userId },
include: {
workflows: true,
tenant: true,
},
})
// Access related data
user.workflows.forEach(workflow => {
console.log(workflow.name)
})
```
## Transactions
Atomic operations across multiple records:
```typescript
const result = await prisma.$transaction(async (tx) => {
// All operations succeed or all fail
const user = await tx.user.create({ data: userData })
const workflow = await tx.workflow.create({
data: { ...workflowData, userId: user.id }
})
return { user, workflow }
})
```
## Migrations
Schema changes require migrations:
```bash
# Create migration after schema change
npm run db:migrate
# Apply migrations
npm run db:push
# View database with UI
npm run db:studio
```
## Connection Management
### Development (SQLite)
```env
DATABASE_URL="file:./dev.db"
```
### Production (PostgreSQL)
```env
DATABASE_URL="postgresql://user:password@host:5432/metabuilder"
```
## Performance Optimization
### Indexing
```prisma
model User {
id String @id
email String @unique // Automatically indexed
tenantId String
@@index([tenantId]) // Composite index for queries
}
```
### Pagination
```typescript
const page = 1
const pageSize = 20
const workflows = await prisma.workflow.findMany({
where: { tenantId },
skip: (page - 1) * pageSize,
take: pageSize,
})
```
### Select Specific Fields
```typescript
// Only retrieve needed columns
const users = await prisma.user.findMany({
select: {
id: true,
email: true,
level: true,
},
})
```
## Error Handling
```typescript
import { Prisma } from '@prisma/client'
try {
await prisma.user.create({ data: userData })
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
if (error.code === 'P2002') {
// Unique constraint violation
throw new Error('Email already exists')
}
}
throw error
}
```
## Best Practices
**Do:**
- Always filter by `tenantId` for tenant data
- Use TypeScript for type safety
- Create migrations for schema changes
- Use transactions for related operations
- Validate input before database operations
- Cache frequently accessed data
- Use indexes for common queries
**Don't:**
- Store secrets in database
- Trust user input in queries
- Assume data is available (check null)
- Use `findMany()` without filters on large tables
- Forget `tenantId` in queries
- Execute raw SQL without escaping
## Related Files
- Schema: `prisma/schema.prisma`
- Migrations: `prisma/migrations/`
- Database wrapper: `src/lib/database.ts`
- DBAL (server-side): `src/lib/database-dbal.server.ts`
- Type definitions: `src/lib/level-types.ts`
See `/docs/` for more database patterns and guides.