TypeScript Edition¶
OrmAI is available as a TypeScript/Node.js package with full feature parity to the Python version.
Installation¶
Supported ORMs¶
| ORM | Status | Package |
|---|---|---|
| Prisma | Production | Built-in |
| Drizzle | Production | Built-in |
| TypeORM | Production | Built-in |
Quick Start¶
With Prisma¶
import { PrismaClient } from '@prisma/client';
import { mountPrisma, Principal, RunContext } from 'ormai';
const prisma = new PrismaClient();
// Define policy
const policy = {
models: {
User: {
allowed: true,
fields: {
id: { action: 'allow' },
email: { action: 'mask' },
name: { action: 'allow' },
},
scoping: { tenantId: 'principal.tenantId' },
},
Order: {
allowed: true,
fields: {
id: { action: 'allow' },
status: { action: 'allow' },
total: { action: 'allow' },
},
scoping: { tenantId: 'principal.tenantId' },
},
},
};
// Mount toolset
const toolset = mountPrisma(prisma, policy);
// Create context
const ctx: RunContext = {
principal: {
tenantId: 'acme-corp',
userId: 'user-123',
roles: ['member'],
},
};
// Query
const result = await toolset.query(ctx, {
model: 'Order',
filters: [{ field: 'status', op: 'eq', value: 'pending' }],
limit: 10,
});
console.log(result.rows);
With Drizzle¶
import { drizzle } from 'drizzle-orm/node-postgres';
import { mountDrizzle } from 'ormai';
import * as schema from './schema';
const db = drizzle(pool, { schema });
const toolset = mountDrizzle(db, schema, policy);
const result = await toolset.query(ctx, {
model: 'orders',
filters: [{ field: 'status', op: 'eq', value: 'pending' }],
});
With TypeORM¶
import { DataSource } from 'typeorm';
import { mountTypeORM } from 'ormai';
import { User, Order } from './entities';
const dataSource = new DataSource({
type: 'postgres',
url: process.env.DATABASE_URL,
entities: [User, Order],
});
await dataSource.initialize();
const toolset = mountTypeORM(dataSource, policy);
Policy Configuration¶
Zod Schema¶
Policies are validated with Zod:
import { z } from 'zod';
import { PolicySchema, FieldAction, WriteAction } from 'ormai';
const policy = PolicySchema.parse({
models: {
User: {
allowed: true,
fields: {
id: { action: FieldAction.Allow },
email: { action: FieldAction.Mask },
password: { action: FieldAction.Deny },
},
scoping: { tenantId: 'principal.tenantId' },
writePolicy: {
create: WriteAction.Allow,
update: WriteAction.Allow,
delete: WriteAction.Deny,
},
},
},
budget: {
maxRows: 1000,
maxIncludeDepth: 3,
},
});
Type Safety¶
Full TypeScript support:
import type { Policy, ModelPolicy, FieldPolicy, Principal, RunContext } from 'ormai';
const modelPolicy: ModelPolicy = {
allowed: true,
fields: {
id: { action: 'allow' },
},
};
const policy: Policy = {
models: {
User: modelPolicy,
},
};
API Reference¶
Query¶
interface QueryOptions {
model: string;
filters?: FilterClause[];
select?: string[];
order?: OrderClause[];
include?: IncludeClause[];
limit?: number;
cursor?: string;
}
const result = await toolset.query(ctx, {
model: 'Order',
filters: [
{ field: 'status', op: 'eq', value: 'pending' },
{ field: 'total', op: 'gte', value: 1000 },
],
select: ['id', 'status', 'total'],
order: [{ field: 'createdAt', direction: 'desc' }],
include: [{ relation: 'user', select: ['id', 'name'] }],
limit: 20,
});
Get¶
const result = await toolset.get(ctx, {
model: 'Order',
id: 123,
include: [{ relation: 'items' }],
});
Aggregate¶
const result = await toolset.aggregate(ctx, {
model: 'Order',
filters: [{ field: 'status', op: 'eq', value: 'completed' }],
aggregations: [
{ function: 'count', alias: 'totalOrders' },
{ function: 'sum', field: 'total', alias: 'revenue' },
{ function: 'avg', field: 'total', alias: 'avgOrder' },
],
groupBy: ['status'],
});
Create¶
const result = await toolset.create(ctx, {
model: 'Order',
data: {
status: 'pending',
total: 5000,
userId: 'user-123',
},
});
Update¶
const result = await toolset.update(ctx, {
model: 'Order',
id: 123,
data: {
status: 'confirmed',
},
});
Delete¶
Express Integration¶
import express from 'express';
import { mountPrisma, Principal, RunContext } from 'ormai';
const app = express();
app.use(express.json());
const toolset = mountPrisma(prisma, policy);
// Middleware to extract context
function getContext(req: express.Request): RunContext {
return {
principal: {
tenantId: req.headers['x-tenant-id'] as string,
userId: req.headers['x-user-id'] as string,
roles: (req.headers['x-roles'] as string)?.split(',') || [],
},
};
}
// Query endpoint
app.post('/api/query', async (req, res) => {
try {
const ctx = getContext(req);
const result = await toolset.query(ctx, req.body);
res.json(result);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// Get endpoint
app.post('/api/get', async (req, res) => {
try {
const ctx = getContext(req);
const result = await toolset.get(ctx, req.body);
if (!result.success) {
return res.status(404).json({ error: 'Not found' });
}
res.json(result.data);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
app.listen(3000);
Next.js Integration¶
API Routes¶
// app/api/query/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { toolset } from '@/lib/ormai';
export async function POST(request: NextRequest) {
const body = await request.json();
const ctx = {
principal: {
tenantId: request.headers.get('x-tenant-id')!,
userId: request.headers.get('x-user-id')!,
},
};
try {
const result = await toolset.query(ctx, body);
return NextResponse.json(result);
} catch (error) {
return NextResponse.json(
{ error: error.message },
{ status: 400 }
);
}
}
Server Actions¶
// app/actions.ts
'use server';
import { toolset } from '@/lib/ormai';
import { auth } from '@/lib/auth';
export async function queryOrders(filters: FilterClause[]) {
const session = await auth();
const ctx = {
principal: {
tenantId: session.user.orgId,
userId: session.user.id,
},
};
return toolset.query(ctx, {
model: 'Order',
filters,
limit: 50,
});
}
Audit Logging¶
import { JsonlAuditStore, AuditMiddleware } from 'ormai';
const store = new JsonlAuditStore('./audit.jsonl');
const middleware = new AuditMiddleware(store, {
includeSnapshots: true,
});
const auditedToolset = middleware.wrap(toolset);
Error Handling¶
import {
OrmAIError,
ModelNotAllowedError,
QueryBudgetExceededError,
} from 'ormai';
try {
await toolset.query(ctx, { model: 'SecretModel' });
} catch (error) {
if (error instanceof ModelNotAllowedError) {
console.log('Model access denied');
} else if (error instanceof QueryBudgetExceededError) {
console.log('Query too expensive');
} else if (error instanceof OrmAIError) {
console.log(`OrmAI error: ${error.code}`);
}
}
Testing¶
import { describe, it, expect, beforeEach } from 'vitest';
import { mountPrisma } from 'ormai';
import { prismaMock } from './mocks';
describe('OrmAI Integration', () => {
const toolset = mountPrisma(prismaMock, policy);
const ctx = {
principal: {
tenantId: 'test-tenant',
userId: 'test-user',
},
};
it('should query with tenant scope', async () => {
const result = await toolset.query(ctx, {
model: 'Order',
limit: 10,
});
expect(result.success).toBe(true);
// Verify tenant filter was applied
expect(prismaMock.order.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({
tenantId: 'test-tenant',
}),
})
);
});
it('should reject forbidden model', async () => {
await expect(
toolset.query(ctx, { model: 'SecretModel' })
).rejects.toThrow(ModelNotAllowedError);
});
});
Feature Comparison¶
| Feature | Python | TypeScript |
|---|---|---|
| Query DSL | ✅ | ✅ |
| Policies | ✅ | ✅ |
| Field Actions | ✅ | ✅ |
| Scoping | ✅ | ✅ |
| Write Operations | ✅ | ✅ |
| Audit Logging | ✅ | ✅ |
| Deferred Execution | ✅ | ✅ |
| MCP Server | ✅ | ✅ |
| Code Generation | ✅ | ⚠️ Partial |
| Eval Framework | ✅ | ⚠️ Partial |
Migration from Python¶
The TypeScript API mirrors Python closely:
# Python
result = await toolset.query(
ctx,
model="Order",
filters=[{"field": "status", "op": "eq", "value": "pending"}],
limit=10,
)
// TypeScript
const result = await toolset.query(ctx, {
model: 'Order',
filters: [{ field: 'status', op: 'eq', value: 'pending' }],
limit: 10,
});
Next Steps¶
- FastAPI Integration - Python web integration
- LangGraph Integration - AI agent integration
- Multi-Tenant Setup - Tenant isolation