@aradox/multi-orm
Version:
Type-safe ORM with multi-datasource support, row-level security, and Prisma-like API for PostgreSQL, SQL Server, and HTTP APIs
690 lines (531 loc) • 17.4 kB
Markdown
# Hooks & Middleware Guide
## Overview
The ORM provides a powerful middleware system for intercepting and transforming queries. This enables:
- **Row-Level Security (RLS)** - Tenant isolation, user ownership filtering
- **Role-Based Access Control (RBAC)** - Restrict model access by user role
- **Audit Logging** - Track all database mutations for compliance
- **Performance Monitoring** - Detect slow queries
- **Data Sanitization** - Remove sensitive fields from results
- **Soft Deletes** - Convert delete operations to updates
- **Caching** - Cache query results
## Quick Start
```typescript
import { client } from './.qts/client';
import { tenantIsolationMiddleware } from './src/middleware/examples';
// Register middleware
client.use(tenantIsolationMiddleware(['Customers', 'Orders', 'Products']));
// Set user context
client.setContext({
user: {
id: 123,
tenantId: 456,
role: 'user'
}
});
// All queries automatically filtered by tenant_id
const customers = await client.Customers.findMany({});
// SQL: SELECT * FROM Customers WHERE tenant_id = 456
```
## API Reference
### `client.use(middleware)`
Register a middleware for query interception. Middleware are executed in the order they are registered.
```typescript
client.use({
name: 'my-middleware', // Optional, for debugging
beforeQuery: async ({ model, operation, args, context }) => {
// Modify args before query execution
return args;
},
afterQuery: async ({ model, operation, args, result, context }) => {
// Transform results after query execution
return result;
},
onError: async ({ model, operation, args, error, context }) => {
// Handle errors
}
});
```
**Parameters:**
- `middleware`: `Middleware` object with `beforeQuery`, `afterQuery`, and/or `onError` hooks
### `client.setContext(context)`
Set the query context that will be passed to all middleware hooks.
```typescript
client.setContext({
user: {
id: 123,
tenantId: 456,
role: 'admin',
permissions: ['read:customers', 'write:orders']
},
custom: {
requestId: 'abc-123',
sessionId: 'xyz-789'
},
metadata: {
ip: '192.168.1.1',
userAgent: 'Mozilla/5.0...'
}
});
```
**Parameters:**
- `context`: `QueryContext` object with user info, custom data, and metadata
### `client.getContext()`
Get the current query context.
```typescript
const context = client.getContext();
console.log(context.user?.tenantId); // 456
```
## Built-in Middleware
The ORM provides 8 pre-built middleware patterns in `src/middleware/examples.ts`:
### 1. Tenant Isolation
Automatically inject `tenant_id` filter on all queries for multi-tenant applications.
```typescript
import { tenantIsolationMiddleware } from './src/middleware/examples';
client.use(tenantIsolationMiddleware([
'Customers',
'Orders',
'Products',
'Invoices'
]));
client.setContext({ user: { tenantId: 123 } });
// All queries on these models automatically filtered
const customers = await client.Customers.findMany({
where: { IsActive: true }
});
// SQL: WHERE IsActive = true AND tenant_id = 123
```
### 2. User Ownership
Ensure users can only access their own data.
```typescript
import { userOwnershipMiddleware } from './src/middleware/examples';
client.use(userOwnershipMiddleware(['Orders', 'Payments']));
client.setContext({ user: { id: 456 } });
const orders = await client.Orders.findMany({});
// SQL: WHERE user_id = 456
```
### 3. Role-Based Access Control (RBAC)
Restrict access to models based on user roles.
```typescript
import { rbacMiddleware } from './src/middleware/examples';
client.use(rbacMiddleware({
'Customers': ['user', 'admin', 'sales'],
'Orders': ['user', 'admin', 'sales'],
'Employees': ['admin', 'hr'],
'Payments': ['admin', 'finance'],
'Invoices': ['admin', 'finance']
}));
client.setContext({ user: { role: 'sales' } });
await client.Customers.findMany({}); // ✅ Allowed
await client.Payments.findMany({}); // ❌ Throws error
```
### 4. Audit Logging
Track all database mutations for compliance.
```typescript
import { auditLoggingMiddleware } from './src/middleware/examples';
client.use(auditLoggingMiddleware({
async log(entry) {
// Write to audit log table or external service
await db.auditLog.create({
data: {
model: entry.model,
operation: entry.operation,
userId: entry.userId,
timestamp: entry.timestamp,
args: JSON.stringify(entry.args),
result: JSON.stringify(entry.result)
}
});
}
}));
// Only mutations are logged (create, update, delete)
await client.Customers.create({ data: { Name: 'John' } });
// Audit log: { model: 'Customers', operation: 'create', userId: 123, ... }
```
### 5. Performance Monitoring
Detect and log slow queries.
```typescript
import { performanceMonitoringMiddleware } from './src/middleware/examples';
client.use(performanceMonitoringMiddleware({
slowQueryThreshold: 1000 // milliseconds
}));
// Logs warning if query takes > 1000ms
await client.Customers.findMany({ ... });
// Console: ⚠️ Slow query detected: Customers.findMany took 1523ms
```
### 6. Soft Deletes
Convert delete operations to updates with `deleted_at` timestamp.
```typescript
import { softDeleteMiddleware } from './src/middleware/examples';
client.use(softDeleteMiddleware(['Customers', 'Orders']));
// Automatically filters out deleted records
const customers = await client.Customers.findMany({});
// SQL: WHERE deleted_at IS NULL
// Delete converts to update
await client.Customers.delete({ where: { Id: 1 } });
// SQL: UPDATE Customers SET deleted_at = NOW() WHERE Id = 1
```
### 7. Data Sanitization
Remove sensitive fields from query results.
```typescript
import { dataSanitizationMiddleware } from './src/middleware/examples';
client.use(dataSanitizationMiddleware({
'Customers': ['ssn', 'credit_card'],
'Employees': ['salary', 'ssn', 'bank_account']
}));
const customers = await client.Customers.findMany({});
// Results will not include 'ssn' or 'credit_card' fields
```
### 8. Multiple Middleware Chaining
Combine multiple middleware for defense in depth:
```typescript
// 1. Check permissions (RBAC)
client.use(rbacMiddleware({ ... }));
// 2. Filter by tenant
client.use(tenantIsolationMiddleware([...]));
// 3. Log all mutations
client.use(auditLoggingMiddleware({ ... }));
// 4. Monitor performance
client.use(performanceMonitoringMiddleware({ ... }));
// Middleware execute in registration order
```
## Custom Middleware
Create your own middleware for specific use cases:
### Example: Request ID Injection
```typescript
client.use({
name: 'request-id-injector',
beforeQuery: ({ args, context }) => {
if (context.metadata?.requestId) {
// Add request ID to all create operations
if (args.data) {
args.data.request_id = context.metadata.requestId;
}
}
return args;
}
});
```
### Example: Query Result Caching
```typescript
const cache = new Map();
client.use({
name: 'query-cache',
beforeQuery: ({ model, operation, args }) => {
if (operation === 'findMany' || operation === 'findUnique') {
const key = `${model}:${JSON.stringify(args)}`;
if (cache.has(key)) {
// Return cached result (skip actual query)
throw new CachedResultError(cache.get(key));
}
}
return args;
},
afterQuery: ({ model, operation, args, result }) => {
if (operation === 'findMany' || operation === 'findUnique') {
const key = `${model}:${JSON.stringify(args)}`;
cache.set(key, result);
}
return result;
}
});
```
### Example: Field-Level Permissions
```typescript
client.use({
name: 'field-permissions',
afterQuery: ({ model, result, context }) => {
const role = context.user?.role;
// Remove salary field for non-admin users
if (model === 'Employees' && role !== 'admin') {
const sanitize = (emp: any) => {
const { salary, ...rest } = emp;
return rest;
};
return Array.isArray(result)
? result.map(sanitize)
: sanitize(result);
}
return result;
}
});
```
## Middleware Hook Details
### `beforeQuery` Hook
Called before query execution. Can modify query arguments or throw to prevent execution.
```typescript
beforeQuery: async ({ model, operation, args, context }) => {
// model: 'Customers'
// operation: 'findMany' | 'create' | 'update' | 'delete' | ...
// args: { where: {...}, select: {...}, ... }
// context: { user: {...}, custom: {...}, metadata: {...} }
// Modify args
args.where = { ...args.where, tenant_id: context.user.tenantId };
// Or throw to prevent query
if (!context.user?.permissions.includes('read:customers')) {
throw new Error('Permission denied');
}
return args; // Return modified args
}
```
### `afterQuery` Hook
Called after successful query execution. Can transform results.
```typescript
afterQuery: async ({ model, operation, args, result, context }) => {
// result: query result (array or single object)
// Transform results
if (Array.isArray(result)) {
return result.map(item => ({
...item,
full_name: `${item.FirstName} ${item.LastName}`
}));
}
return result;
}
```
### `onError` Hook
Called when query throws an error. Can log errors or perform cleanup.
```typescript
onError: async ({ model, operation, args, error, context }) => {
// Log error to monitoring service
await errorMonitor.log({
model,
operation,
error: error.message,
userId: context.user?.id,
timestamp: new Date()
});
// Note: Error is still thrown after all onError hooks run
}
```
## Integration with Next.js
### API Route Example
```typescript
// app/api/customers/route.ts
import { client } from '@/orm/client';
import { getServerSession } from 'next-auth';
import { tenantIsolationMiddleware, rbacMiddleware } from '@/orm/middleware/examples';
// Register middleware once (outside handler)
client.use(tenantIsolationMiddleware(['Customers', 'Orders']));
client.use(rbacMiddleware({
'Customers': ['user', 'admin'],
'Orders': ['user', 'admin']
}));
export async function GET(request: Request) {
const session = await getServerSession();
if (!session?.user) {
return Response.json({ error: 'Unauthorized' }, { status: 401 });
}
// Set context for this request
client.setContext({
user: {
id: session.user.id,
tenantId: session.user.tenantId,
role: session.user.role
},
metadata: {
requestId: request.headers.get('x-request-id'),
ip: request.headers.get('x-forwarded-for')
}
});
// Query automatically filtered by middleware
const customers = await client.Customers.findMany({
where: { IsActive: true },
take: 100
});
return Response.json(customers);
}
```
### Server Component Example
```typescript
// app/customers/page.tsx
import { client } from '@/orm/client';
import { getServerSession } from 'next-auth';
export default async function CustomersPage() {
const session = await getServerSession();
// Set context
client.setContext({
user: {
id: session.user.id,
tenantId: session.user.tenantId
}
});
// Fetch data with automatic RLS
const customers = await client.Customers.findMany({
take: 50,
orderBy: { Name: 'asc' }
});
return (
<div>
<h1>Customers</h1>
<ul>
{customers.map(c => (
<li key={c.Id}>{c.Name}</li>
))}
</ul>
</div>
);
}
```
## Integration with Express
```typescript
// server.ts
import express from 'express';
import { client } from './orm/client';
import { tenantIsolationMiddleware, auditLoggingMiddleware } from './orm/middleware/examples';
const app = express();
// Register ORM middleware once
client.use(tenantIsolationMiddleware(['Customers', 'Orders']));
client.use(auditLoggingMiddleware({ ... }));
// Express middleware to set ORM context
app.use((req, res, next) => {
if (req.user) {
client.setContext({
user: {
id: req.user.id,
tenantId: req.user.tenantId,
role: req.user.role
},
metadata: {
requestId: req.id,
ip: req.ip,
userAgent: req.get('user-agent')
}
});
}
next();
});
// Routes automatically use RLS
app.get('/api/customers', async (req, res) => {
const customers = await client.Customers.findMany({});
res.json(customers);
});
```
## Best Practices
### 1. Defense in Depth
Use multiple layers of security:
```typescript
// Layer 1: Database-level RLS (PostgreSQL/SQL Server)
-- CREATE POLICY tenant_isolation ON customers USING (tenant_id = current_user_id());
// Layer 2: ORM middleware
client.use(tenantIsolationMiddleware(['Customers']));
// Layer 3: API validation
if (!user.can('read:customers')) {
throw new ForbiddenError();
}
```
### 2. Context Management
Set context at the start of each request:
```typescript
// ❌ Bad: Global context (race conditions in concurrent requests)
client.setContext({ user: { id: 123 } });
// ✅ Good: Set context per request
app.use((req, res, next) => {
client.setContext({ user: req.user });
next();
});
```
### 3. Middleware Order
Order matters! More restrictive middleware should come first:
```typescript
// ✅ Good order
client.use(rbacMiddleware({ ... })); // 1. Check permissions
client.use(tenantIsolationMiddleware([...])); // 2. Apply filters
client.use(auditLoggingMiddleware({ ... })); // 3. Log results
// ❌ Bad order
client.use(auditLoggingMiddleware({ ... })); // 1. Logs before RBAC check
client.use(rbacMiddleware({ ... })); // 2. Too late
```
### 4. Error Handling
Handle middleware errors gracefully:
```typescript
try {
const customers = await client.Customers.findMany({});
} catch (error) {
if (error.message.includes('RBAC:')) {
return res.status(403).json({ error: 'Forbidden' });
}
if (error.message.includes('Tenant isolation:')) {
return res.status(401).json({ error: 'Unauthorized' });
}
throw error;
}
```
### 5. Never Expose ORM to Frontend
```typescript
// ❌ NEVER DO THIS
// Frontend code (client-side)
import { client } from './orm/client'; // Exposes database credentials!
// ✅ Always use API layer
// Frontend code
const response = await fetch('/api/customers');
const customers = await response.json();
```
## Performance Considerations
### Minimal Overhead
Middleware has near-zero overhead when not used:
```typescript
// Without middleware: ~0ms overhead
const customers = await client.Customers.findMany({});
// With 3 middleware: ~1-2ms overhead
client.use(rbacMiddleware({ ... }));
client.use(tenantIsolationMiddleware([...]));
client.use(auditLoggingMiddleware({ ... }));
const customers = await client.Customers.findMany({});
```
### Async Middleware
Middleware can be async, but avoid slow operations in hot paths:
```typescript
// ❌ Slow: External API call in beforeQuery
beforeQuery: async ({ context }) => {
const permissions = await fetch('https://auth-service.com/permissions'); // Adds 100-500ms!
return args;
}
// ✅ Fast: Cache permissions in context
client.setContext({
user: {
permissions: await fetchPermissions(user.id) // Fetch once per request
}
});
```
## Testing Middleware
```typescript
import { describe, it, expect } from 'vitest';
import { tenantIsolationMiddleware } from './middleware/examples';
describe('Tenant Isolation Middleware', () => {
it('should inject tenant_id filter', () => {
const middleware = tenantIsolationMiddleware(['Customers']);
const result = middleware.beforeQuery!({
model: 'Customers',
operation: 'findMany',
args: { where: { IsActive: true } },
context: { user: { tenantId: 123 } }
});
expect(result.where).toEqual({
AND: [
{ IsActive: true },
{ tenant_id: 123 }
]
});
});
it('should throw when tenantId missing', () => {
const middleware = tenantIsolationMiddleware(['Customers']);
expect(() => {
middleware.beforeQuery!({
model: 'Customers',
operation: 'findMany',
args: {},
context: { user: { id: 456 } } // No tenantId
});
}).toThrow('No tenantId found');
});
});
```
## Examples
See these files for complete examples:
- `Example/test-rls-middleware.ts` - Full RLS demo with 5 test scenarios
- `src/middleware/examples.ts` - 8 pre-built middleware patterns
- `src/types/middleware.ts` - TypeScript types and interfaces
## Related Documentation
- [SQL Logging Guide](./SQL_LOGGING_GUIDE.md)
- [Type Generation Guide](./TYPE_HINTS_GUIDE.md)
- [CRUD Operations Guide](./docs/done.md)