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

2,005 lines (1,588 loc) 46.7 kB
# Complete User Guide **Multi-Source ORM with Type-Safe Client, Row-Level Security, and Cross-Datasource Stitching** --- ## Table of Contents 1. [Introduction](#introduction) 2. [Installation & Setup](#installation--setup) 3. [Quick Start](#quick-start) 4. [Schema Definition (DSL)](#schema-definition-dsl) 5. [Type-Safe Client API](#type-safe-client-api) 6. [Query Operations](#query-operations) 7. [CRUD Operations](#crud-operations) 8. [Multi-Datasource Support](#multi-datasource-support) 9. [Row-Level Security & Middleware](#row-level-security--middleware) 10. [Authentication & Authorization](#authentication--authorization) 11. [Advanced Features](#advanced-features) 12. [Best Practices](#best-practices) 13. [Troubleshooting](#troubleshooting) 14. [API Reference](#api-reference) --- ## Introduction This ORM provides a **Prisma-like developer experience** with powerful features for building modern applications: - 🎯 **Type-Safe Client** - Full IntelliSense support like Prisma - 💾 **Multi-Datasource** - Query PostgreSQL, SQL Server, and HTTP APIs in one application - 🔐 **Row-Level Security** - Built-in middleware for tenant isolation, RBAC, and audit logging - 🔗 **Cross-Datasource Joins** - Stitch data from different sources automatically - **Smart Query Planning** - Automatic bulk fetching and concurrency control - 🔒 **Strict/Non-Strict Modes** - Control error handling behavior - 🪟 **Windows Authentication** - First-class support for SQL Server Windows Auth ### Architecture Overview ``` ┌─────────────────────────────────────────────────────────────┐ Your Application - Type-safe queries with IntelliSense - Prisma-like API (client.User.findMany()) └────────────────┬────────────────────────────────────────────┘ ┌────────────────▼────────────────────────────────────────────┐ ORM Middleware Layer - Row-level security (tenant isolation) - RBAC (role-based access control) - Audit logging - Performance monitoring └────────────────┬────────────────────────────────────────────┘ ┌────────────────▼────────────────────────────────────────────┐ Stitcher & Adapters - Cross-datasource joins - Bulk fetching optimization - Concurrency control └────────────────┬────────────────────────────────────────────┘ ┌────────────────▼────────────────────────────────────────────┐ Data Sources - PostgreSQL - MS SQL Server (Windows Auth / SQL Auth) - HTTP APIs (with OAuth 2.0) └─────────────────────────────────────────────────────────────┘ ``` --- ## Installation & Setup ### Prerequisites - Node.js 16+ (18+ recommended) - TypeScript 4.9+ - Database: PostgreSQL 12+ or SQL Server 2017+ - npm or yarn ### Install Dependencies ```bash npm install ``` ### Project Structure ``` your-project/ ├── lib/ └── orm/ ├── schema.qts # Your ORM schema └── client.ts # ORM client instance ├── database/ ├── schema.sql # Database schema └── setup-rls-*.sql # Row-level security setup ├── .env # Environment variables ├── .env.example # Example configuration └── package.json ``` --- ## Quick Start ### 1. Define Your Schema Create a `.qts` schema file (e.g., `lib/orm/schema.qts`): ```typescript // Config: Global settings config { maxIncludeDepth = 5 maxFanOut = 100 strictMode = false } // Datasource: Your database datasource main_db { provider = "sqlserver" url = env("DATABASE_URL") } // Model: Define your data structure model User @datasource(main_db) { id Int @id @default(autoincrement()) email String @unique name String? role String tenant_id Int created_at DateTime? // Relations orders Orders[] @relation(fields:[id],references:[user_id],strategy:"lookup") } model Order @datasource(main_db) { id Int @id @default(autoincrement()) user_id Int order_number String total_amount Float? status String? tenant_id Int created_at DateTime? // Relations user User @relation(fields:[user_id],references:[id],strategy:"lookup") } ``` ### 2. Generate Types ```bash npm run generate ``` This creates type definitions in `.qts/types.d.ts` with full IntelliSense support. ### 3. Create ORM Client Create `lib/orm/client.ts`: ```typescript import { ORMClient } from '@your-org/orm'; import * as fs from 'fs'; import * as path from 'path'; // Load schema const schemaPath = path.join(process.cwd(), 'lib/orm/schema.qts'); const schema = fs.readFileSync(schemaPath, 'utf-8'); // Create ORM instance const orm = new ORMClient(schema, { schemaPath }); // Generate type-safe client export const db = orm.generate(); export default db; ``` ### 4. Use in Your Application ```typescript import db from './lib/orm/client'; async function main() { // Query with type-safe API const users = await db.User.findMany({ where: { role: { eq: 'admin' } }, include: { orders: true }, take: 10 }); console.log('Users:', users); // Create new record const newUser = await db.User.create({ data: { email: 'john@example.com', name: 'John Doe', role: 'user', tenant_id: 1 } }); console.log('Created:', newUser); // Clean up await db.$disconnect(); } main().catch(console.error); ``` --- ## Schema Definition (DSL) ### Config Block Global configuration for the ORM runtime: ```typescript config { maxIncludeDepth = 5 // Max depth for nested includes maxFanOut = 100 // Max items to fetch per relation strictMode = false // Error handling mode maxConcurrentRequests = 10 // Parallel HTTP requests requestTimeoutMs = 10000 // HTTP timeout postFilterRowLimit = 10000 // Max rows for client-side filtering } ``` ### Datasources #### PostgreSQL ```typescript datasource pg_db { provider = "postgres" url = env("POSTGRES_URL") } ``` **Connection String:** ``` postgresql://user:password@localhost:5432/mydb?schema=public ``` #### SQL Server ```typescript datasource mssql_db { provider = "sqlserver" url = env("MSSQL_URL") } ``` **Connection Strings:** *Windows Authentication:* ``` Server=MYSERVER;Database=MyDB;Integrated Security=true;TrustServerCertificate=true ``` *SQL Server Authentication:* ``` Server=localhost;Database=MyDB;User Id=sa;Password=yourPassword;Encrypt=true ``` *Windows Auth with Domain:* ``` Server=MYSERVER;Database=MyDB;Integrated Security=true;Domain=MYDOMAIN;TrustServerCertificate=true ``` #### HTTP APIs ```typescript datasource api { provider = "http" baseUrl = "https://api.example.com" oauth = { script: "./auth/api_oauth.ts", cacheTtl: "55m", optional: false } } ``` ### Models Define your data models with attributes: ```typescript model Customer @datasource(main_db) { // Fields id Int @id @default(autoincrement()) name String email String? @unique phone String? tenant_id Int is_active Boolean created_at DateTime? // Relations orders Orders[] @relation(fields:[id],references:[customer_id],strategy:"lookup") } ``` ### Field Attributes - `@id` - Primary key field - `@default(autoincrement())` - Auto-increment ID - `@default(now())` - Current timestamp - `@unique` - Unique constraint - `@map("physical_name")` - Map to different column name ### Relation Attributes ```typescript // One-to-many relation orders Orders[] @relation( fields: [id], // Local field references: [customer_id], // Foreign field strategy: "lookup" // Join strategy ) // Many-to-one relation customer Customer @relation( fields: [customer_id], references: [id], strategy: "lookup" ) ``` **Strategies:** - `"lookup"` - Client-side join (fetch parent, then children) - `"pushdown"` - Server-side join (database JOIN clause) ### HTTP Endpoints For HTTP datasources, define endpoint mappings: ```typescript model Product @datasource(api) { id Int @id name String price Float category String? @endpoint(findMany: { method: "GET", path: "/products", query: { category: "$where.category.eq?", offset: "$skip", limit: "$take" }, response: { items: "$.data.items", total: "$.data.total" } }) @endpoint(findUnique: { method: "GET", path: "/products/{id}", response: { item: "$" } }) @endpoint(findManyBulkById: { method: "POST", path: "/products/bulk", body: { ids: "$where.id.in" }, response: { items: "$.data" } }) } ``` --- ## Type-Safe Client API After running `npm run generate`, you get full type safety: ```typescript import db from './lib/orm/client'; // Type-safe - IntelliSense shows available models const users = await db.User.findMany({ // Type-safe - IntelliSense shows User fields where: { email: { contains: '@example.com' }, role: { in: ['admin', 'user'] } }, // Type-safe - IntelliSense shows relation names include: { orders: { where: { status: { eq: 'completed' } } } } }); // Type-safe result - TypeScript knows the shape users.forEach(user => { console.log(user.email); // Known field console.log(user.orders.length); // Included relation // console.log(user.invalid); // TypeScript error }); ``` ### Generated Client Structure ```typescript // GeneratedClient interface interface GeneratedClient { // Model delegates User: UserDelegate; Order: OrderDelegate; Customer: CustomerDelegate; // Middleware methods use(middleware: Middleware): void; setContext(context: QueryContext): void; getContext(): QueryContext; // Connection management $disconnect(): Promise<void>; } // Model delegate methods interface UserDelegate { findMany(args: UserFindManyArgs): Promise<User[]>; findUnique(args: UserFindUniqueArgs): Promise<User | null>; findFirst(args: UserFindFirstArgs): Promise<User | null>; count(args: UserCountArgs): Promise<number>; create(args: UserCreateArgs): Promise<User>; createMany(args: UserCreateManyArgs): Promise<{ count: number }>; update(args: UserUpdateArgs): Promise<User>; updateMany(args: UserUpdateManyArgs): Promise<{ count: number }>; delete(args: UserDeleteArgs): Promise<User>; deleteMany(args: UserDeleteManyArgs): Promise<{ count: number }>; upsert(args: UserUpsertArgs): Promise<User>; } ``` --- ## Query Operations ### Finding Records #### findMany - Get Multiple Records ```typescript // Basic query const users = await db.User.findMany({}); // With filters const activeUsers = await db.User.findMany({ where: { is_active: { eq: true }, role: { in: ['admin', 'user'] } } }); // With pagination const page2 = await db.User.findMany({ skip: 20, take: 10 }); // With sorting const sorted = await db.User.findMany({ orderBy: { created_at: 'desc', name: 'asc' } }); // With field selection const minimal = await db.User.findMany({ select: { id: true, email: true, name: true } }); // With relations const usersWithOrders = await db.User.findMany({ include: { orders: { where: { status: { eq: 'completed' } }, take: 5 } } }); ``` #### findUnique - Get Single Record by Unique Field ```typescript // By primary key const user = await db.User.findUnique({ where: { id: 123 } }); // By unique field const userByEmail = await db.User.findUnique({ where: { email: 'john@example.com' } }); // With relations const userWithOrders = await db.User.findUnique({ where: { id: 123 }, include: { orders: true } }); ``` #### findFirst - Get First Matching Record ```typescript const firstAdmin = await db.User.findFirst({ where: { role: { eq: 'admin' } }, orderBy: { created_at: 'asc' } }); ``` #### count - Count Records ```typescript // Count all const total = await db.User.count({}); // Count with filter const activeCount = await db.User.count({ where: { is_active: { eq: true } } }); ``` ### Where Clause Operators #### Comparison Operators ```typescript await db.Product.findMany({ where: { price: { eq: 99.99 }, // Equal stock: { ne: 0 }, // Not equal price: { gt: 50 }, // Greater than price: { gte: 50 }, // Greater than or equal price: { lt: 100 }, // Less than price: { lte: 100 }, // Less than or equal category: { in: ['electronics', 'books'] }, // In array status: { notIn: ['deleted', 'archived'] } // Not in array } }); ``` #### String Operators ```typescript await db.Customer.findMany({ where: { email: { contains: '@example.com' }, // LIKE '%value%' name: { startsWith: 'John' }, // LIKE 'value%' phone: { endsWith: '1234' } // LIKE '%value' } }); ``` #### Null Checks ```typescript await db.User.findMany({ where: { name: { isNull: true }, // IS NULL email: { isNotNull: true } // IS NOT NULL } }); ``` #### Logical Operators ```typescript // AND (implicit) await db.User.findMany({ where: { is_active: { eq: true }, role: { eq: 'admin' } } }); // OR await db.User.findMany({ where: { OR: [ { role: { eq: 'admin' } }, { role: { eq: 'moderator' } } ] } }); // NOT await db.User.findMany({ where: { NOT: { status: { eq: 'banned' } } } }); // Complex nested await db.Order.findMany({ where: { AND: [ { status: { eq: 'pending' } }, { OR: [ { total_amount: { gte: 1000 } }, { priority: { eq: 'high' } } ] } ] } }); ``` --- ## CRUD Operations ### Create #### create - Insert Single Record ```typescript const user = await db.User.create({ data: { email: 'alice@example.com', name: 'Alice Smith', role: 'user', tenant_id: 1, is_active: true } }); console.log('Created user:', user); // { id: 123, email: 'alice@example.com', name: 'Alice Smith', ... } ``` #### create with select - Partial Return ```typescript const user = await db.User.create({ data: { email: 'bob@example.com', name: 'Bob Jones', role: 'user', tenant_id: 1 }, select: { id: true, email: true } }); console.log(user); // { id: 124, email: 'bob@example.com' } ``` #### createMany - Bulk Insert ```typescript const result = await db.User.createMany({ data: [ { email: 'user1@example.com', name: 'User 1', role: 'user', tenant_id: 1 }, { email: 'user2@example.com', name: 'User 2', role: 'user', tenant_id: 1 }, { email: 'user3@example.com', name: 'User 3', role: 'user', tenant_id: 1 } ] }); console.log(`Created ${result.count} users`); // Created 3 users ``` ### Update #### update - Update Single Record ```typescript const updated = await db.User.update({ where: { id: 123 }, data: { name: 'Alice Smith Updated', is_active: false } }); console.log('Updated user:', updated); ``` #### update with select ```typescript const updated = await db.User.update({ where: { id: 123 }, data: { is_active: false }, select: { id: true, is_active: true } }); ``` #### updateMany - Bulk Update ```typescript const result = await db.User.updateMany({ where: { role: { eq: 'user' }, is_active: { eq: false } }, data: { is_active: true } }); console.log(`Updated ${result.count} users`); ``` ### Delete #### delete - Delete Single Record ```typescript const deleted = await db.User.delete({ where: { id: 123 } }); console.log('Deleted user:', deleted); ``` #### delete with select ```typescript const deleted = await db.User.delete({ where: { id: 123 }, select: { id: true, email: true } }); ``` #### deleteMany - Bulk Delete ```typescript const result = await db.User.deleteMany({ where: { is_active: { eq: false }, created_at: { lt: '2023-01-01' } } }); console.log(`Deleted ${result.count} users`); ``` ### Upsert Insert if not exists, update if exists: ```typescript const user = await db.User.upsert({ where: { email: 'john@example.com' }, create: { email: 'john@example.com', name: 'John Doe', role: 'user', tenant_id: 1, is_active: true }, update: { name: 'John Doe Updated', is_active: true } }); ``` --- ## Multi-Datasource Support Query across multiple databases and APIs in one application. ### Schema with Multiple Datasources ```typescript config { maxIncludeDepth = 5 maxFanOut = 100 strictMode = false } // Database 1: PostgreSQL datasource postgres_db { provider = "postgres" url = env("POSTGRES_URL") } // Database 2: SQL Server datasource mssql_db { provider = "sqlserver" url = env("MSSQL_URL") } // External API datasource external_api { provider = "http" baseUrl = "https://jsonplaceholder.typicode.com" } // Model in PostgreSQL model User @datasource(postgres_db) { id Int @id @default(autoincrement()) email String name String? post_id Int? // Relation to external API post Post? @relation(fields:[post_id],references:[id],strategy:"lookup") } // Model in SQL Server model Order @datasource(mssql_db) { Id Int @id @default(autoincrement()) UserId Int Total Float Status String? } // Model in HTTP API model Post @datasource(external_api) { id Int @id title String body String userId Int @endpoint(findMany: { method: "GET", path: "/posts", response: { items: "$" } }) @endpoint(findUnique: { method: "GET", path: "/posts/{id}", response: { item: "$" } }) } ``` ### Cross-Datasource Queries ```typescript // Query User (PostgreSQL) with Post (HTTP API) const users = await db.User.findMany({ include: { post: true // Automatically fetches from external API } }); // Query Order (SQL Server) independently const orders = await db.Order.findMany({ where: { Status: { eq: 'pending' } } }); // Each model uses its appropriate datasource automatically ``` ### Smart Bulk Fetching The ORM automatically optimizes cross-datasource joins: ```typescript // Fetches 100 users, then automatically uses bulk endpoint for posts const users = await db.User.findMany({ take: 100, include: { post: true } }); // Behind the scenes: // 1. SELECT * FROM users LIMIT 100 (PostgreSQL) // 2. POST /posts/bulk { ids: [1,2,3,...] } (HTTP - one request) // OR GET /posts/1, GET /posts/2, ... (HTTP - parallel requests) ``` --- ## Row-Level Security & Middleware Built-in middleware system for securing multi-tenant applications. ### Setting Up Middleware ```typescript import { tenantIsolationMiddleware, rbacMiddleware, auditLoggingMiddleware } from '@your-org/orm/middleware/examples'; // Register middleware db.use(tenantIsolationMiddleware(['User', 'Order', 'Customer'])); db.use(rbacMiddleware({ 'User': ['admin'], 'Order': ['admin', 'user'], 'Customer': ['admin', 'sales'] })); db.use(auditLoggingMiddleware({ async log(entry) { console.log('Audit:', entry); } })); ``` ### Setting Query Context Set context at the beginning of each request: ```typescript // In API handler or middleware db.setContext({ user: { id: session.user.id, tenantId: session.user.tenantId, role: session.user.role, permissions: session.user.permissions }, metadata: { requestId: req.headers['x-request-id'], ip: req.ip, userAgent: req.headers['user-agent'] } }); // All subsequent queries use this context const customers = await db.Customer.findMany({}); // Automatically filtered: WHERE tenant_id = {session.user.tenantId} ``` ### Built-in Middleware #### 1. Tenant Isolation Automatically adds `tenant_id` filter to all queries: ```typescript db.use(tenantIsolationMiddleware(['Customer', 'Order', 'Invoice'])); db.setContext({ user: { tenantId: 123 } }); // Query automatically filtered const customers = await db.Customer.findMany({}); // SQL: SELECT * FROM customers WHERE tenant_id = 123 ``` #### 2. Role-Based Access Control (RBAC) Restrict model access by user role: ```typescript db.use(rbacMiddleware({ 'User': ['admin'], 'Order': ['admin', 'sales'], 'Customer': ['admin', 'sales', 'support'], 'Invoice': ['admin', 'finance'] })); db.setContext({ user: { role: 'sales' } }); await db.Order.findMany({}); // Allowed await db.Customer.findMany({}); // Allowed await db.Invoice.findMany({}); // Error: RBAC: Access denied ``` #### 3. User Ownership Filter records by `user_id`: ```typescript db.use(userOwnershipMiddleware(['Order', 'Cart'])); db.setContext({ user: { id: 456, role: 'user' } }); // Non-admin users only see their own records const orders = await db.Order.findMany({}); // SQL: SELECT * FROM orders WHERE user_id = 456 // Admins see all records db.setContext({ user: { id: 789, role: 'admin' } }); const allOrders = await db.Order.findMany({}); // SQL: SELECT * FROM orders (no filter) ``` #### 4. Audit Logging Track all mutations (create, update, delete): ```typescript db.use(auditLoggingMiddleware({ async log(entry) { await db.AuditLog.create({ data: { tenant_id: entry.context?.user?.tenantId, user_id: entry.context?.user?.id, model: entry.model, operation: entry.operation, record_id: entry.result?.id, old_values: JSON.stringify(entry.oldValues), new_values: JSON.stringify(entry.args.data), timestamp: entry.timestamp, ip_address: entry.context?.metadata?.ip } }); } })); // All mutations are logged await db.Customer.update({ where: { id: 123 }, data: { name: 'New Name' } }); // Audit log created automatically ``` #### 5. Performance Monitoring Detect and log slow queries: ```typescript db.use(performanceMonitoringMiddleware({ slowQueryThreshold: 1000 // Log queries over 1 second })); // Slow queries are automatically logged const result = await db.Customer.findMany({ include: { orders: { include: { items: true } } } }); // If query takes > 1s: "⚠️ Slow query detected: Customer.findMany (1234ms)" ``` #### 6. Soft Delete Convert delete operations to updates with `deleted_at`: ```typescript db.use(softDeleteMiddleware(['Customer', 'Order'])); // Delete becomes update await db.Customer.delete({ where: { id: 123 } }); // SQL: UPDATE customers SET deleted_at = NOW() WHERE id = 123 // Soft-deleted records are automatically filtered const customers = await db.Customer.findMany({}); // SQL: SELECT * FROM customers WHERE deleted_at IS NULL ``` #### 7. Data Sanitization Remove sensitive fields from results: ```typescript db.use(dataSanitizationMiddleware({ 'User': ['password_hash', 'ssn', 'credit_card'], 'Customer': ['internal_notes'] })); const users = await db.User.findMany({}); // Result: password_hash, ssn, credit_card fields are removed ``` ### Custom Middleware Create your own middleware: ```typescript import { Middleware } from '@your-org/orm/types/middleware'; const customMiddleware: Middleware = { name: 'custom-logger', beforeQuery: ({ model, operation, args, context }) => { console.log(`Before ${model}.${operation}`, args); // Modify query args if (operation === 'findMany') { args.take = Math.min(args.take || 100, 100); // Max 100 records } return args; }, afterQuery: ({ model, operation, result, context }) => { console.log(`After ${model}.${operation}`, result); // Transform result if (Array.isArray(result)) { return result.map(item => ({ ...item, _fetched_at: new Date() })); } return result; }, onError: ({ model, operation, error, context }) => { console.error(`Error in ${model}.${operation}`, error); // Don't re-throw - allow error to propagate } }; db.use(customMiddleware); ``` ### Middleware Order Middleware executes in registration order: ```typescript // 1. RBAC (most restrictive - check access first) db.use(rbacMiddleware({ ... })); // 2. Tenant isolation (filter by tenant) db.use(tenantIsolationMiddleware([...])); // 3. Audit logging (track operations) db.use(auditLoggingMiddleware({ ... })); // 4. Performance monitoring (measure performance) db.use(performanceMonitoringMiddleware({ ... })); ``` --- ## Authentication & Authorization ### Next.js Integration #### 1. Configure NextAuth ```typescript // app/api/auth/[...nextauth]/route.ts import NextAuth from 'next-auth'; import CredentialsProvider from 'next-auth/providers/credentials'; import db from '@/lib/orm/client'; const handler = NextAuth({ providers: [ CredentialsProvider({ name: 'Credentials', credentials: { email: { type: "email" }, password: { type: "password" } }, async authorize(credentials) { const user = await db.User.findFirst({ where: { email: credentials.email } }); if (!user) return null; // Verify password (use bcrypt) const isValid = await verifyPassword( credentials.password, user.password_hash ); if (!isValid) return null; return { id: user.id.toString(), email: user.email, name: user.name, tenantId: user.tenant_id, role: user.role }; } }) ], callbacks: { async jwt({ token, user }) { if (user) { token.id = user.id; token.tenantId = user.tenantId; token.role = user.role; } return token; }, async session({ session, token }) { if (session.user) { session.user.id = token.id; session.user.tenantId = token.tenantId; session.user.role = token.role; } return session; } } }); export { handler as GET, handler as POST }; ``` #### 2. Create Auth Helper ```typescript // lib/auth.ts import { getServerSession } from 'next-auth'; import db from './orm/client'; export async function requireAuth() { const session = await getServerSession(); if (!session?.user) { throw new Error('Unauthorized'); } // Set ORM context db.setContext({ user: { id: session.user.id, tenantId: session.user.tenantId, role: session.user.role } }); return session; } ``` #### 3. Use in API Routes ```typescript // app/api/customers/route.ts import { NextRequest, NextResponse } from 'next/server'; import db from '@/lib/orm/client'; import { requireAuth } from '@/lib/auth'; export async function GET(request: NextRequest) { try { const session = await requireAuth(); // Query with automatic tenant isolation const customers = await db.Customer.findMany({ where: { is_active: { eq: true } } }); return NextResponse.json({ customers }); } catch (error) { return NextResponse.json( { error: error.message }, { status: 401 } ); } } ``` ### Express Integration ```typescript // middleware/auth.ts import { Request, Response, NextFunction } from 'express'; import db from '../lib/orm/client'; export async function authMiddleware( req: Request, res: Response, next: NextFunction ) { const token = req.headers.authorization?.split(' ')[1]; if (!token) { return res.status(401).json({ error: 'Unauthorized' }); } try { const decoded = verifyToken(token); // Set ORM context db.setContext({ user: { id: decoded.userId, tenantId: decoded.tenantId, role: decoded.role }, metadata: { requestId: req.id, ip: req.ip, userAgent: req.headers['user-agent'] } }); req.user = decoded; next(); } catch (error) { return res.status(401).json({ error: 'Invalid token' }); } } // routes/customers.ts router.get('/customers', authMiddleware, async (req, res) => { const customers = await db.Customer.findMany({}); res.json({ customers }); }); ``` --- ## Custom SQL Queries Execute raw SQL queries directly on your datasources when you need more control than the ORM provides. ### generalQuery() - Query Any Datasource Execute SQL on the first available SQL datasource: ```typescript // Simple query const users = await db.generalQuery('SELECT * FROM users WHERE active = $1', [true]); // PostgreSQL-specific (uses $1, $2, etc.) const result = await db.generalQuery( 'SELECT * FROM users WHERE email = $1 AND role = $2', ['admin@example.com', 'admin'] ); // With INSERT/UPDATE/DELETE const created = await db.generalQuery( 'INSERT INTO users (email, name) VALUES ($1, $2) RETURNING *', ['john@example.com', 'John Doe'] ); ``` ### Datasource-Specific Queries Query a specific datasource by name (as defined in your schema): ```typescript // schema.qts datasource main_db { provider = "sqlserver" url = env("MSSQL_URL") } datasource analytics_db { provider = "postgres" url = env("POSTGRES_URL") } ``` ```typescript // Query SQL Server (uses @p1, @p2, etc.) const orders = await db.main_db.query( 'SELECT * FROM Orders WHERE TotalAmount > @p1', [1000] ); // Query PostgreSQL (uses $1, $2, etc.) const stats = await db.analytics_db.query( 'SELECT date_trunc($1, created_at) as period, count(*) FROM events GROUP BY period', ['day'] ); ``` ### Parameter Binding Always use parameterized queries to prevent SQL injection: ```typescript // SAFE - Parameterized const userId = req.params.id; const user = await db.generalQuery( 'SELECT * FROM users WHERE id = $1', [userId] ); // DANGEROUS - SQL Injection risk const user = await db.generalQuery( `SELECT * FROM users WHERE id = ${userId}` // NEVER DO THIS! ); ``` ### Complex Queries Custom queries are perfect for complex SQL operations: ```typescript // CTEs (Common Table Expressions) const report = await db.generalQuery(` WITH monthly_sales AS ( SELECT DATE_TRUNC('month', created_at) as month, SUM(total_amount) as revenue FROM orders WHERE status = 'completed' GROUP BY DATE_TRUNC('month', created_at) ) SELECT month, revenue, LAG(revenue) OVER (ORDER BY month) as prev_month_revenue FROM monthly_sales ORDER BY month DESC `); // Window functions const rankings = await db.generalQuery(` SELECT name, department, salary, RANK() OVER (PARTITION BY department ORDER BY salary DESC) as dept_rank FROM employees WHERE is_active = $1 `, [true]); ``` ### Important Limitations ⚠️ Custom queries bypass ORM features: 1. **No middleware** - Tenant isolation, RBAC, and audit logging are NOT applied 2. **No type safety** - Results are typed as `any[]` 3. **No cross-datasource joins** - Each query runs on a single datasource 4. **Manual security** - You must add tenant filters manually ```typescript // BAD - Bypasses tenant isolation const customers = await db.generalQuery('SELECT * FROM customers'); // Returns customers from ALL tenants! // GOOD - Manually add tenant filter const context = db.getContext(); const customers = await db.generalQuery( 'SELECT * FROM customers WHERE tenant_id = $1', [context.user?.tenantId] ); // BETTER - Use ORM when possible const customers = await db.Customer.findMany({}); // Tenant isolation applied automatically ``` ### When to Use Custom Queries **✅ Good use cases:** - Complex aggregations and reporting - Database-specific features (JSONB, full-text search) - Performance-critical queries - Legacy stored procedures **❌ Avoid when:** - Simple CRUD operations (use ORM methods) - Cross-datasource joins (use ORM includes) - Middleware is needed (tenant isolation, RBAC, audit) For complete examples and best practices, see **[Custom Queries Guide](./CUSTOM_QUERIES.md)**. --- ## Advanced Features ### SQL Query Logging Enable debug logging to see generated SQL: ```bash # Windows $env:LOG_LEVEL="debug" $env:LOG_MODULES="mssql,postgres" node dist/app.js # Linux/Mac LOG_LEVEL=debug LOG_MODULES=mssql,postgres node dist/app.js ``` Output: ``` 2025-11-17T10:15:30.123Z [MSSQL] 📊 SQL Query: SELECT TOP(?) Name, Email FROM [dbo].[Customers] WHERE Name LIKE ? 2025-11-17T10:15:30.125Z [MSSQL] 📊 Parameters: [10,"%John%"] ``` ### Field Mapping with @map Map TypeScript field names to different database column names: ```typescript model User @datasource(main_db) { id Int @id @default(autoincrement()) firstName String @map("FirstName") // Maps to FirstName column lastName String @map("LastName") // Maps to LastName column userId Int @map("user_id") // Maps to user_id column } // Use camelCase in TypeScript const user = await db.User.create({ data: { firstName: 'John', // Writes to FirstName column lastName: 'Doe', // Writes to LastName column userId: 123 // Writes to user_id column } }); ``` ### Computed Fields (Coming Soon) Define computed fields with resolvers: ```typescript model User @datasource(main_db) { id Int @id first_name String last_name String // Computed field displayName String @computed( resolver: "./resolvers/user.displayName.ts", async: false ) } // Resolver: resolvers/user.displayName.ts export default function displayName(ctx) { const { row } = ctx; return `${row.first_name} ${row.last_name}`; } ``` ### OAuth for HTTP APIs Define OAuth authentication for HTTP datasources: ```typescript datasource api { provider = "http" baseUrl = "https://api.example.com" oauth = { script: "./auth/api_oauth.ts", cacheTtl: "55m", optional: false } } // auth/api_oauth.ts export default async function auth(ctx) { const token = await getAccessToken(); return { headers: { Authorization: `Bearer ${token}` }, expiresAt: Date.now() + 3600000, refresh: async (reason) => { const newToken = await refreshToken(); return { headers: { Authorization: `Bearer ${newToken}` }, expiresAt: Date.now() + 3600000 }; } }; } ``` ### Strict Mode Control error handling behavior: ```typescript // Global config config { strictMode = true // Fail fast on errors } // Per-query override const users = await db.User.findMany({ include: { orders: true }, $options: { strict: false // Allow best-effort for this query } }); ``` **Strict Mode ON:** - Throws error on pushdown failure - Throws error on missing OAuth - Throws error on exceeded limits - Throws error on computed field timeout **Strict Mode OFF (default):** - Falls back to post-filtering - Continues without OAuth (with warning) - Returns partial results - Returns issues in metadata --- ## Best Practices ### 1. Always Generate Types ```bash npm run generate ``` Run this after changing your schema to get updated IntelliSense. ### 2. Set Context Per Request ```typescript // GOOD - Set context per request export async function handler(req, res) { const session = await getSession(req); db.setContext({ user: session.user }); const customers = await db.Customer.findMany({}); // Context is isolated to this request } // BAD - Global context causes race conditions db.setContext({ user: { tenantId: 123 } }); // Multiple requests will overwrite each other's context ``` ### 3. Use Defense in Depth Layer security at multiple levels: ```typescript // Layer 1: Database RLS (cannot be bypassed) -- Run setup-rls-postgres.sql or setup-rls-sqlserver.sql // Layer 2: ORM Middleware db.use(tenantIsolationMiddleware(['Customer', 'Order'])); db.use(rbacMiddleware({ 'Invoice': ['admin', 'finance'] })); // Layer 3: API Validation if (!user.hasPermission('read:customers')) { throw new Error('Forbidden'); } // Layer 4: Frontend (least trusted) {user.role === 'admin' && <AdminPanel />} ``` ### 4. Never Trust User Input for tenant_id ```typescript // BAD - User could pass any tenant_id const data = await req.json(); await db.Customer.create({ data: { tenant_id: data.tenant_id, // NEVER trust this! name: data.name } }); // GOOD - Use authenticated user's tenant const session = await requireAuth(); await db.Customer.create({ data: { tenant_id: session.user.tenantId, // From session name: data.name } }); ``` ### 5. Don't Expose ORM to Frontend ```typescript // BAD - Exposes database credentials import db from './lib/orm/client'; function MyComponent() { useEffect(() => { db.Customer.findMany({}).then(setCustomers); }, []); } // GOOD - Use API layer function MyComponent() { useEffect(() => { fetch('/api/customers') .then(res => res.json()) .then(data => setCustomers(data.customers)); }, []); } ``` ### 6. Clean Up Connections ```typescript async function main() { try { const users = await db.User.findMany({}); console.log(users); } finally { await db.$disconnect(); // Always disconnect } } ``` ### 7. Handle Errors Gracefully ```typescript try { const customers = await db.Customer.findMany({}); return NextResponse.json({ customers }); } catch (error) { // Middleware errors if (error.message.includes('RBAC:')) { return NextResponse.json( { error: 'Forbidden' }, { status: 403 } ); } // Tenant isolation errors if (error.message.includes('Tenant isolation:')) { return NextResponse.json( { error: 'Unauthorized' }, { status: 401 } ); } // Generic error console.error('Database error:', error); return NextResponse.json( { error: 'Internal server error' }, { status: 500 } ); } ``` --- ## Troubleshooting ### Common Issues #### 1. Types Not Updating **Problem:** IntelliSense shows old types after schema change. **Solution:** ```bash npm run generate # Restart TypeScript server: Cmd+Shift+P > "Restart TS Server" ``` #### 2. Connection Errors **Problem:** Cannot connect to database. **Solution:** - Check connection string in `.env` - For SQL Server Windows Auth: ensure `Integrated Security=true` - For PostgreSQL: ensure correct port and credentials - Test connection with database client first #### 3. Middleware Not Working **Problem:** Queries not filtered by middleware. **Solution:** ```typescript // Ensure middleware is registered db.use(tenantIsolationMiddleware([...])); // Ensure context is set db.setContext({ user: { tenantId: 123 } }); // Check middleware order (most restrictive first) ``` #### 4. Cross-Datasource Joins Failing **Problem:** Relations not loading across datasources. **Solution:** - Check relation definition has `strategy:"lookup"` - Verify both models have correct `fields` and `references` - Check foreign key values match #### 5. TypeScript Errors After Generate **Problem:** TypeScript shows errors in generated types. **Solution:** ```bash # Clean and regenerate rm -rf .qts npm run generate npm run build ``` ### Debug Logging Enable debug logging to troubleshoot: ```bash # See all logs $env:LOG_LEVEL="debug" node dist/app.js # See specific module $env:LOG_MODULES="mssql,stitcher" node dist/app.js # See SQL queries only $env:LOG_MODULES="mssql,postgres" node dist/app.js ``` --- ## API Reference ### ORMClient ```typescript class ORMClient { constructor(schema: string, options?: { schemaPath?: string; }); generate(): GeneratedClient; close(): Promise<void>; // Deprecated, use client.$disconnect() } ``` ### GeneratedClient ```typescript interface GeneratedClient { // Model delegates [modelName: string]: ModelDelegate; // Middleware methods use(middleware: Middleware): void; setContext(context: QueryContext): void; getContext(): QueryContext; // Connection management $disconnect(): Promise<void>; } ``` ### ModelDelegate ```typescript interface ModelDelegate<T> { findMany(args?: { where?: WhereInput; select?: SelectInput; include?: IncludeInput; orderBy?: OrderByInput; skip?: number; take?: number; }): Promise<T[]>; findUnique(args: { where: UniqueWhereInput; select?: SelectInput; include?: IncludeInput; }): Promise<T | null>; findFirst(args?: { where?: WhereInput; select?: SelectInput; include?: IncludeInput; orderBy?: OrderByInput; }): Promise<T | null>; count(args?: { where?: WhereInput; }): Promise<number>; create(args: { data: CreateInput; select?: SelectInput; }): Promise<T>; createMany(args: { data: CreateInput[]; skipDuplicates?: boolean; }): Promise<{ count: number }>; update(args: { where: UniqueWhereInput; data: UpdateInput; select?: SelectInput; }): Promise<T>; updateMany(args: { where?: WhereInput; data: UpdateInput; }): Promise<{ count: number }>; delete(args: { where: UniqueWhereInput; select?: SelectInput; }): Promise<T>; deleteMany(args?: { where?: WhereInput; }): Promise<{ count: number }>; upsert(args: { where: UniqueWhereInput; create: CreateInput; update: UpdateInput; select?: SelectInput; }): Promise<T>; } ``` ### Middleware ```typescript interface Middleware { name: string; beforeQuery?: (context: { model: string; operation: string; args: any; context: QueryContext; }) => any | Promise<any>; afterQuery?: (context: { model: string; operation: string; args: any; result: any; context: QueryContext; }) => any | Promise<any>; onError?: (context: { model: string; operation: string; args: any; error: Error; context: QueryContext; }) => void | Promise<void>; } ``` ### QueryContext ```typescript interface QueryContext { user?: { id: number | string; tenantId?: number | string; role?: string; permissions?: string[]; }; custom?: Record<string, any>; metadata?: { requestId?: string; ip?: string; userAgent?: string; }; } ``` --- ## Additional Resources - **[Hooks & Middleware Guide](./HOOKS_MIDDLEWARE_GUIDE.md)** - Complete middleware documentation - **[SQL Logging Guide](./SQL_LOGGING_GUIDE.md)** - Debug SQL queries - **[CRUD Guide](./CRUD_GUIDE.md)** - CRUD operations reference - **[Type Generator Guide](./TYPE_GENERATOR.md)** - Type generation documentation - **[AI Coding Guide](./AI_CODING_GUIDE.md)** - Build multi-tenant apps --- ## Support & Feedback For issues, questions, or feature requests: 1. Check existing documentation 2. Review example implementations in `Example/` directory 3. Enable debug logging to troubleshoot 4. Check test files for usage patterns --- **Version:** 1.0.0 **Last Updated:** November 17, 2025 **License:** MIT