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