UNPKG

@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
# 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)