@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
310 lines • 9.76 kB
JavaScript
;
/**
* Example Middleware for Row-Level Security (RLS)
*
* These middleware examples demonstrate common patterns for:
* - Tenant isolation (multi-tenancy)
* - User-based data filtering
* - Audit logging
* - Performance monitoring
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.tenantIsolationMiddleware = tenantIsolationMiddleware;
exports.userOwnershipMiddleware = userOwnershipMiddleware;
exports.auditLoggingMiddleware = auditLoggingMiddleware;
exports.rbacMiddleware = rbacMiddleware;
exports.performanceMonitoringMiddleware = performanceMonitoringMiddleware;
exports.softDeleteMiddleware = softDeleteMiddleware;
exports.dataSanitizationMiddleware = dataSanitizationMiddleware;
/**
* Tenant Isolation Middleware
*
* Automatically injects tenant_id filter on all queries.
* Ensures users can only access data from their own tenant.
*
* Usage:
* ```typescript
* const client = await orm.generate();
* client.use(tenantIsolationMiddleware(['Customers', 'Orders', 'Products']));
* client.setContext({ user: { tenantId: 123 } });
*
* // All queries automatically filtered by tenant_id
* const customers = await client.Customers.findMany({});
* // WHERE tenant_id = 123
* ```
*/
function tenantIsolationMiddleware(models) {
return {
name: 'tenant-isolation',
beforeQuery: ({ model, args, context }) => {
// Only apply to specified models
if (!models.includes(model)) {
return args;
}
// Check if user has tenantId
if (!context.user?.tenantId) {
throw new Error(`Tenant isolation: No tenantId found in context for model ${model}`);
}
// Inject tenant_id filter
const tenantFilter = { tenant_id: context.user.tenantId };
if (args.where) {
// Merge with existing where clause
args.where = {
AND: [
args.where,
tenantFilter
]
};
}
else {
// Set new where clause
args.where = tenantFilter;
}
return args;
}
};
}
/**
* User Ownership Middleware
*
* Ensures users can only access their own data.
* Injects user_id filter on specified models.
*
* Usage:
* ```typescript
* client.use(userOwnershipMiddleware(['Orders', 'Payments']));
* client.setContext({ user: { id: 456 } });
*
* const orders = await client.Orders.findMany({});
* // WHERE user_id = 456
* ```
*/
function userOwnershipMiddleware(models) {
return {
name: 'user-ownership',
beforeQuery: ({ model, args, context, operation }) => {
// Only apply to specified models and read operations
if (!models.includes(model)) {
return args;
}
// Skip for create operations (user_id will be set in data)
if (operation === 'create' || operation === 'createMany') {
return args;
}
if (!context.user?.id) {
throw new Error(`User ownership: No user id found in context for model ${model}`);
}
// Inject user_id filter
const ownerFilter = { user_id: context.user.id };
if (args.where) {
args.where = {
AND: [
args.where,
ownerFilter
]
};
}
else {
args.where = ownerFilter;
}
return args;
}
};
}
/**
* Audit Logging Middleware
*
* Logs all mutations (create, update, delete) for compliance.
*
* Usage:
* ```typescript
* client.use(auditLoggingMiddleware({
* async log(entry) {
* await db.auditLog.create({ data: entry });
* }
* }));
* ```
*/
function auditLoggingMiddleware(options) {
return {
name: 'audit-logging',
afterQuery: async ({ model, operation, args, result, context }) => {
// Only log mutations
if (['create', 'createMany', 'update', 'updateMany', 'delete', 'deleteMany', 'upsert'].includes(operation)) {
await options.log({
model,
operation,
userId: context.user?.id,
timestamp: new Date(),
args,
result
});
}
return result;
}
};
}
/**
* Role-Based Access Control (RBAC) Middleware
*
* Restricts access based on user roles.
*
* Usage:
* ```typescript
* client.use(rbacMiddleware({
* Employees: ['admin', 'hr'],
* Payments: ['admin', 'finance'],
* Invoices: ['admin', 'finance']
* }));
*
* client.setContext({ user: { role: 'hr' } });
* await client.Employees.findMany({}); // ✅ Allowed
* await client.Payments.findMany({}); // ❌ Forbidden
* ```
*/
function rbacMiddleware(modelRoles) {
return {
name: 'rbac',
beforeQuery: ({ model, context }) => {
const allowedRoles = modelRoles[model];
// No restrictions for this model
if (!allowedRoles) {
return {};
}
const userRole = context.user?.role;
if (!userRole) {
throw new Error(`RBAC: No role found in context for model ${model}`);
}
if (!allowedRoles.includes(userRole)) {
throw new Error(`RBAC: Role "${userRole}" is not allowed to access model ${model}. Required roles: ${allowedRoles.join(', ')}`);
}
return {};
}
};
}
/**
* Performance Monitoring Middleware
*
* Tracks query execution time and logs slow queries.
*
* Usage:
* ```typescript
* client.use(performanceMonitoringMiddleware({
* slowQueryThreshold: 1000 // ms
* }));
* ```
*/
function performanceMonitoringMiddleware(options) {
const timings = new Map();
return {
name: 'performance-monitoring',
beforeQuery: ({ model, operation }) => {
const key = `${model}.${operation}.${Date.now()}`;
timings.set(key, Date.now());
return {};
},
afterQuery: ({ model, operation, result }) => {
// Find the timing entry (last one for this model.operation)
const entries = Array.from(timings.entries())
.filter(([k]) => k.startsWith(`${model}.${operation}`));
if (entries.length > 0) {
const [key, startTime] = entries[entries.length - 1];
const duration = Date.now() - startTime;
timings.delete(key);
if (duration > options.slowQueryThreshold) {
console.warn(`⚠️ Slow query detected: ${model}.${operation} took ${duration}ms`);
}
}
return result;
}
};
}
/**
* Soft Delete Middleware
*
* Automatically filters out deleted records and converts delete to update.
*
* Usage:
* ```typescript
* client.use(softDeleteMiddleware(['Customers', 'Orders']));
*
* // findMany automatically filters deleted_at IS NULL
* const customers = await client.Customers.findMany({});
*
* // delete() converts to update with deleted_at = NOW()
* await client.Customers.delete({ where: { Id: 1 } });
* ```
*/
function softDeleteMiddleware(models) {
return {
name: 'soft-delete',
beforeQuery: ({ model, operation, args }) => {
if (!models.includes(model)) {
return args;
}
// For read operations, filter out soft-deleted records
if (['findMany', 'findUnique', 'count'].includes(operation)) {
const notDeletedFilter = { deleted_at: null };
if (args.where) {
args.where = {
AND: [
args.where,
notDeletedFilter
]
};
}
else {
args.where = notDeletedFilter;
}
}
// For delete operations, convert to update
if (operation === 'delete' || operation === 'deleteMany') {
// Note: This requires special handling in the adapter
// For now, just add a flag
args._softDelete = true;
args.data = { deleted_at: new Date() };
}
return args;
}
};
}
/**
* Data Sanitization Middleware
*
* Removes sensitive fields from query results.
*
* Usage:
* ```typescript
* client.use(dataSanitizationMiddleware({
* Customers: ['ssn', 'credit_card'],
* Employees: ['salary', 'ssn']
* }));
* ```
*/
function dataSanitizationMiddleware(modelFields) {
return {
name: 'data-sanitization',
afterQuery: ({ model, result }) => {
const fieldsToRemove = modelFields[model];
if (!fieldsToRemove) {
return result;
}
const sanitize = (obj) => {
if (!obj)
return obj;
const sanitized = { ...obj };
for (const field of fieldsToRemove) {
delete sanitized[field];
}
return sanitized;
};
// Handle arrays
if (Array.isArray(result)) {
return result.map(sanitize);
}
// Handle single object
return sanitize(result);
}
};
}
//# sourceMappingURL=examples.js.map