@tsdiapi/prisma
v0.4.2
Published
TSDIAPI-Prisma is a plugin for the TSDIAPI-Server framework that integrates Prisma ORM with event and hook functionality for database operations.
Maintainers
Readme
@tsdiapi/prisma: Prisma Integration Plugin for TSDIAPI-Server
@tsdiapi/prisma seamlessly integrates Prisma with TSDIAPI-Server, providing hooks, events, and lifecycle management to enhance database operations.
Features
- Prisma Client Integration: Automatically sets up and manages Prisma Client.
- Event System: Listen to events before and after Prisma operations.
- Hook Support: Dynamically modify queries using hooks.
- Request Context: Access Fastify request context in Prisma hooks for authentication and authorization.
- Lifecycle Management: Handles Prisma Client initialization and cleanup during app lifecycle.
- Configurable Options: Define query timeouts and load custom Prisma-related files via glob patterns.
- Full TypeScript Support: Complete type safety for all operations.
Installation
Required Dependencies (Prisma 7.0+)
npm install @tsdiapi/prisma @prisma/client @prisma/adapter-pg pg
npm install --save-dev prisma @types/pgUsing TSDIAPI CLI
Or use the CLI to add the plugin with all dependencies:
tsdiapi plugins add prismaThe CLI will:
- Install all required packages
- Create
prisma/prisma.config.tsfor CLI configuration - Set up the adapter-based client in
src/main.ts - Configure environment variables (
DATABASE_URL,SHADOW_DATABASE_URL)
📌 Code Generation
The TSDIAPI-Prisma plugin includes code generators to streamline the creation of Prisma event listeners and hooks. Use the tsdiapi CLI to generate these files automatically.
| Name | Description |
| ------- | --------------------------------------------------------------------- |
| event | Generates a Prisma event listener for a specific model and operation. |
| hook | Generates a Prisma hook to modify query arguments before execution. |
📌 Generate a Prisma Event Listener
This generator creates an event listener that triggers before and after a specified Prisma operation. It will prompt for:
- Model Name (e.g.,
User,Post) - Prisma Operation (e.g.,
FindUnique,Create,Update)
Run the command:
tsdiapi generate prisma event📌 Generate a Prisma Hook
This generator creates a Prisma hook that allows modification of query arguments before execution. It will prompt for:
- Model Name (e.g.,
User,Order) - Prisma Operation (e.g.,
Create,Update,FindMany)
Run the command:
tsdiapi generate prisma hook✅ Easily generate Prisma event listeners and hooks with just one command!
Usage
Register the Plugin (Prisma 7.0+)
Important: Prisma 7.0+ requires adapters for database connections. The plugin now uses PostgreSQL adapter by default.
Basic Setup with Adapter
import { createApp } from "@tsdiapi/server";
import prismaPlugin from "@tsdiapi/prisma";
import { PrismaClient } from "@generated/prisma/client.js";
import { PrismaPg } from "@prisma/adapter-pg";
import { Pool } from "pg";
// Create PrismaClient with adapter for Prisma 7.0+
function createPrismaClient(): PrismaClient {
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) {
throw new Error("DATABASE_URL is not set");
}
const pool = new Pool({ connectionString: databaseUrl });
const adapter = new PrismaPg(pool);
return new PrismaClient({ adapter });
}
const prismaClient = createPrismaClient();
createApp({
plugins: [
prismaPlugin({
client: prismaClient, // Pass the instantiated client
prismaOptions: {
transactionOptions: { timeout: 15000 },
},
}),
],
});Prisma Configuration Split (Prisma 7.0+)
- prisma/prisma.config.ts - CLI configuration:
import 'dotenv/config';
import { defineConfig, env } from "prisma/config";
export default defineConfig({
schema: 'prisma/schema.prisma',
migrations: {
path: 'prisma/migrations',
},
datasource: {
url: env("DATABASE_URL"),
shadowDatabaseUrl: env("SHADOW_DATABASE_URL"), // For safe migrations
},
});- src/main.ts - Runtime client with adapter (shown above)
Note: The runtime client does NOT read prisma.config.ts. Configuration is split between CLI commands and runtime.
Access Prisma Client
You can access the Prisma client in several ways:
1. Via the client export:
import { client } from "@tsdiapi/prisma";
const users = await client.user.findMany();
console.log(users);2. Via the usePrisma() function (recommended):
import { usePrisma } from "@tsdiapi/prisma";
import { PrismaClient } from "@generated/prisma/client.js";
const prisma = usePrisma<PrismaClient>();
const users = await prisma.user.findMany();
console.log(users);3. Via Fastify instance decoration:
// Inside your route handlers or plugins
const users = await fastify.prisma.user.findMany();Event Handling
Define Prisma Event Listeners
Use functions to respond to specific database events:
import { onBeforeHook, onAfterHook, PrismaOperation } from "@tsdiapi/prisma";
import { Prisma } from "@generated/prisma/client.js";
// Listen to events before a query
onBeforeHook(Prisma.ModelName.User, PrismaOperation.Create, (payload) => {
console.log(`Before creating user:`, payload.args);
});
// Listen to events after a query
onAfterHook(Prisma.ModelName.User, PrismaOperation.Create, (payload) => {
console.log(`User created with ID: ${payload.result.id}`);
});Global Event Listeners for All Models (New in v0.3.5+)
Use onBeforeHookForAll and onAfterHookForAll for events across all models. Events are read-only and perfect for logging, monitoring, and analytics:
import { onBeforeHookForAll, onAfterHookForAll, PrismaOperation, getRequest } from "@tsdiapi/prisma";
import { useSession } from "@tsdiapi/jwt-auth";
interface AuthSession {
userId: string;
email: string;
}
// Example 1: Simple logging for all Create operations
onBeforeHookForAll(PrismaOperation.Create, async (payload) => {
console.log(`Creating ${payload.model}:`, payload.args);
});
// Example 2: Performance monitoring for all operations
const operationTimings = new Map<string, number>();
onBeforeHookForAll('*', (payload) => {
const key = `${payload.model}_${payload.operation}_${Date.now()}`;
operationTimings.set(key, Date.now());
});
onAfterHookForAll('*', (payload) => {
const keys = Array.from(operationTimings.keys());
const matchingKey = keys.find(k =>
k.startsWith(`${payload.model}_${payload.operation}_`)
);
if (matchingKey) {
const duration = Date.now() - operationTimings.get(matchingKey)!;
if (duration > 1000) {
console.warn(`Slow query: ${payload.model}.${payload.operation} took ${duration}ms`);
}
operationTimings.delete(matchingKey);
}
});
// Example 3: Audit logging with user context
const mutationOps = [PrismaOperation.Create, PrismaOperation.Update, PrismaOperation.Delete];
mutationOps.forEach(op => {
onAfterHookForAll(op, async (payload) => {
const request = getRequest();
const session = request ? useSession<AuthSession>(request) : null;
// Log to audit table or external service
await auditService.log({
model: payload.model,
operation: payload.operation,
userId: session?.userId || 'system',
userEmail: session?.email,
ipAddress: request?.ip,
data: JSON.stringify(payload.args),
result: payload.result ? JSON.stringify(payload.result) : null,
timestamp: new Date()
});
});
});
// Example 4: Conditional logging based on model
const sensitiveModels = ['User', 'Payment', 'Order'];
onAfterHookForAll('*', async (payload) => {
if (sensitiveModels.includes(payload.model)) {
const request = getRequest();
const session = request ? useSession<AuthSession>(request) : null;
console.log(`[SECURITY] ${session?.email || 'anonymous'} performed ${payload.operation} on ${payload.model}`);
}
});
// Example 5: Error tracking and alerting
onAfterHookForAll('*', async (payload) => {
// Check if operation failed (no result for mutations)
const mutationOps = [PrismaOperation.Create, PrismaOperation.Update, PrismaOperation.Delete];
if (mutationOps.includes(payload.operation) && !payload.result) {
await errorTracking.capture({
type: 'database_operation_failed',
model: payload.model,
operation: payload.operation,
args: payload.args
});
}
});Available functions:
onBeforeHook(model, operation, handler): Triggered before a Prisma query for specific model.onAfterHook(model, operation, handler): Triggered after a query is executed for specific model.onBeforeHookForAll(operation, handler): Triggered before operations across all models. Use'*'for all operations.onAfterHookForAll(operation, handler): Triggered after operations across all models. Use'*'for all operations.
Hook System
Modify queries dynamically using hooks:
import { usePrismaHook, PrismaOperation } from "@tsdiapi/prisma";
import { Prisma } from "@generated/prisma/client.js";
// Register a hook to modify query arguments before execution
usePrismaHook(Prisma.ModelName.User, PrismaOperation.Create, async (args) => {
// Modify the arguments before query execution
args.data.name = `Modified-${args.data.name}`;
args.data.createdAt = new Date();
return args; // Return the modified query arguments
});
// Hooks can also be synchronous
usePrismaHook(Prisma.ModelName.User, PrismaOperation.Update, (args) => {
args.data.updatedAt = new Date();
return args;
});The usePrismaHook function allows you to intercept and modify query arguments before they are executed by Prisma.
Global Hooks for All Models (New in v0.3.5+)
Use usePrismaHookForAll to register hooks that apply to all models, perfect for cross-cutting concerns:
import { usePrismaHookForAll, PrismaOperation } from "@tsdiapi/prisma";
import { useSession } from "@tsdiapi/jwt-auth";
interface AuthSession {
userId: string;
tenantId: string;
}
// Hook for all Create operations across all models
usePrismaHookForAll(PrismaOperation.Create, async (args, model, operation, request) => {
console.log(`Creating ${model} with operation ${operation}`);
if (request) {
const session = useSession<AuthSession>(request);
// Add metadata to all created records
if (args.data && typeof args.data === 'object') {
args.data.createdBy = session?.userId;
args.data.createdAt = new Date();
}
}
return args;
});
// Hook for ALL operations across ALL models
usePrismaHookForAll('*', async (args, model, operation, request) => {
console.log(`Prisma operation: ${model}.${operation}`);
// Example: Block certain models for non-admin users
const session = request ? useSession<AuthSession>(request) : null;
const restrictedModels = ['AdminSettings', 'SystemConfig'];
if (restrictedModels.includes(model) && !session?.roles?.includes('admin')) {
throw new Error(`Access denied to ${model}`);
}
return args;
});
// Example: Add tenant isolation to all models
const tenantEnabledModels = ['User', 'Post', 'Order'];
usePrismaHookForAll(PrismaOperation.FindMany, async (args, model, operation, request) => {
if (tenantEnabledModels.includes(model)) {
const session = request ? useSession<AuthSession>(request) : null;
if (session?.tenantId) {
args.where = {
...args.where,
tenantId: session.tenantId
};
}
}
return args;
});Benefits of Global Hooks:
- Apply common logic across all models (auditing, soft delete, multi-tenancy)
- Conditional logic based on model name
- Reduce code duplication
- Centralized security and authorization
Request Context in Hooks (New in v0.3.5+)
Note: This feature requires no changes to existing code. All existing hooks continue to work as before. The request parameter is optional.
Integration with @tsdiapi/jwt-auth: The examples below demonstrate integration with the
@tsdiapi/jwt-authplugin for authentication. You can use any authentication method - the request object provides full access to Fastify's request context.
Hooks can now access the current Fastify request context, enabling advanced features like user-based filtering, authorization, and request-specific logging:
import { usePrismaHook, PrismaOperation } from "@tsdiapi/prisma";
import { Prisma } from "@generated/prisma/client.js";
import { useSession } from "@tsdiapi/jwt-auth";
// Access request context in hooks
usePrismaHook(Prisma.ModelName.User, PrismaOperation.Create, async (args, request) => {
// request is the current Fastify request (if available)
if (request) {
// Get authenticated session
const session = useSession<AuthSession>(request);
// Add user ID from authenticated session
args.data.createdBy = session?.userId;
// Add request metadata
args.data.ipAddress = request.ip;
args.data.userAgent = request.headers['user-agent'];
}
return args;
});
// Use request for authorization
usePrismaHook(Prisma.ModelName.Post, PrismaOperation.Update, async (args, request) => {
if (request) {
const session = useSession<AuthSession>(request);
if (session?.userId) {
// Ensure users can only update their own posts
args.where = {
...args.where,
authorId: session.userId
};
}
}
return args;
});
// Request-aware logging
usePrismaHook(Prisma.ModelName.Order, PrismaOperation.Delete, async (args, request) => {
if (request) {
const session = useSession<AuthSession>(request);
console.log(`User ${session?.userId} deleting order from ${request.ip}`);
}
return args;
});Note: The request parameter is optional and will be undefined when Prisma operations are executed outside of an HTTP request context (e.g., in background jobs, during initialization).
Getting Request Context Manually
You can also manually get the current request context anywhere in your code:
import { getRequest } from "@tsdiapi/prisma";
import { useSession } from "@tsdiapi/jwt-auth";
interface AuthSession {
userId: string;
email: string;
roles: string[];
}
// Inside any async function during request processing
async function someFunction() {
const request = getRequest();
if (request) {
const session = useSession<AuthSession>(request);
console.log('Current user:', session?.userId);
console.log('User email:', session?.email);
console.log('Request ID:', request.id);
console.log('IP Address:', request.ip);
}
}Configuration Options
| Option | Type | Default Value | Description |
| ------------------ | -------- | --------------------- | ------------------------------------------- |
| client | any | Required | Prisma Client instance with adapter (Prisma 7.0+) |
| prismaOptions | object | { transactionOptions: { timeout: 10000 } } | Options for Prisma Client configuration. |
Example Configuration
import prismaPlugin from "@tsdiapi/prisma";
import { PrismaClient } from "@generated/prisma/client.js";
import { PrismaPg } from "@prisma/adapter-pg";
import { Pool } from "pg";
// Create client with adapter (Prisma 7.0+)
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const adapter = new PrismaPg(pool);
const prismaClient = new PrismaClient({
adapter,
errorFormat: 'pretty',
log: ['query', 'info', 'warn', 'error']
});
const plugin = prismaPlugin({
client: prismaClient,
prismaOptions: {
transactionOptions: {
timeout: 15000, // 15 seconds
isolationLevel: 'ReadCommitted'
}
}
});Lifecycle Integration
onInit: Initializes the Prisma client and sets up hooks/events.- Cleanup: Ensures the Prisma client disconnects on shutdown (
SIGINT,SIGTERM,exit).
Example Use Cases
Basic Setup
Create a simple setup to modify and log Prisma queries:
import { createApp } from "@tsdiapi/server";
import prismaPlugin from "@tsdiapi/prisma";
import { PrismaClient } from "@generated/prisma/client.js";
createApp({
plugins: [
prismaPlugin({
client: PrismaClient,
prismaOptions: {
transactionOptions: { timeout: 10000 }
}
})
],
});Define hooks and listeners to customize database interactions:
import { usePrismaHook, onBeforeHook, onAfterHook, PrismaOperation } from "@tsdiapi/prisma";
import { Prisma } from "@generated/prisma/client.js";
// Hook to modify data before creation
usePrismaHook(Prisma.ModelName.User, PrismaOperation.Create, (args) => {
console.log("Creating user:", args.data);
args.data.createdAt = new Date();
return args;
});
// Event listener after creation
onAfterHook(Prisma.ModelName.User, PrismaOperation.Create, (payload) => {
console.log(`User created successfully: ${payload.result.id}`);
});Advanced: Soft Delete with Global Hooks
Implement soft delete across all models using global hooks:
import { usePrismaHookForAll, PrismaOperation } from "@tsdiapi/prisma";
// Models that support soft delete
const softDeleteModels = ['User', 'Post', 'Comment', 'Order'];
// Override delete operations to perform soft delete
usePrismaHookForAll(PrismaOperation.Delete, async (args, model, operation, request) => {
if (softDeleteModels.includes(model)) {
// Transform delete into update
args = {
...args,
data: {
deletedAt: new Date(),
deletedBy: request ? useSession(request)?.userId : 'system'
}
};
// Change operation to update (handled internally by changing args)
console.log(`Soft deleting ${model} instead of hard delete`);
}
return args;
});
// Filter out soft-deleted records from all find operations
usePrismaHookForAll(PrismaOperation.FindMany, async (args, model, operation) => {
if (softDeleteModels.includes(model)) {
args.where = {
...args.where,
deletedAt: null // Only show non-deleted records
};
}
return args;
});
usePrismaHookForAll(PrismaOperation.FindFirst, async (args, model, operation) => {
if (softDeleteModels.includes(model)) {
args.where = {
...args.where,
deletedAt: null
};
}
return args;
});Advanced: Multi-Tenancy with Request Context
Implement automatic tenant isolation using request context:
import { usePrismaHook, PrismaOperation } from "@tsdiapi/prisma";
import { Prisma } from "@generated/prisma/client.js";
import { useSession } from "@tsdiapi/jwt-auth";
// Define your session type
interface AuthSession {
userId: string;
tenantId: string;
roles: string[];
}
// Automatically filter all queries by tenant
const tenantModels = ['User', 'Post', 'Order'] as const;
tenantModels.forEach(model => {
// Add tenant filter to all find operations
usePrismaHook(Prisma.ModelName[model], PrismaOperation.FindMany, async (args, request) => {
if (request) {
const session = useSession<AuthSession>(request);
if (session?.tenantId) {
args.where = {
...args.where,
tenantId: session.tenantId
};
}
}
return args;
});
// Add tenant ID to all create operations
usePrismaHook(Prisma.ModelName[model], PrismaOperation.Create, async (args, request) => {
if (request) {
const session = useSession<AuthSession>(request);
if (session?.tenantId) {
args.data.tenantId = session.tenantId;
}
}
return args;
});
// Ensure updates only affect tenant's data
usePrismaHook(Prisma.ModelName[model], PrismaOperation.Update, async (args, request) => {
if (request) {
const session = useSession<AuthSession>(request);
if (session?.tenantId) {
args.where = {
...args.where,
tenantId: session.tenantId
};
}
}
return args;
});
});Audit Logging with Request Context
Track all database changes with user information using global events:
import { onAfterHookForAll, PrismaOperation, getRequest, usePrisma } from "@tsdiapi/prisma";
import { PrismaClient } from "@generated/prisma/client.js";
import { useSession } from "@tsdiapi/jwt-auth";
interface AuthSession {
userId: string;
email: string;
roles: string[];
}
// Get Prisma client
const prisma = usePrisma<PrismaClient>();
// Audit log for all mutations using global events
const auditOperations = [
PrismaOperation.Create,
PrismaOperation.Update,
PrismaOperation.Delete
] as const;
auditOperations.forEach(operation => {
onAfterHookForAll(operation, async (payload) => {
const request = getRequest();
const session = request ? useSession<AuthSession>(request) : null;
// Log to audit table
await prisma.auditLog.create({
data: {
model: payload.model,
operation: payload.operation,
userId: session?.userId || 'system',
userEmail: session?.email,
ipAddress: request?.ip,
userAgent: request?.headers['user-agent'],
data: JSON.stringify(payload.args),
result: payload.result ? JSON.stringify(payload.result) : null,
timestamp: new Date()
}
});
});
});Benefits of using global events:
- Single registration instead of looping through all models
- Automatically covers new models without code changes
- Cleaner and more maintainable code
API Reference
Exported Functions
Events (Read-only, for logging/monitoring)
onBeforeHook(model, operation, handler): Register a listener for events before Prisma operations on specific model.onAfterHook(model, operation, handler): Register a listener for events after Prisma operations on specific model.onBeforeHookForAll(operation, handler): Register a global listener for events before operations on all models. Use'*'for all operations.onAfterHookForAll(operation, handler): Register a global listener for events after operations on all models. Use'*'for all operations.
Hooks (Can modify query arguments)
usePrismaHook(model, operation, handler): Register a hook to modify query arguments before execution. Handler receives(args, request?)parameters.usePrismaHookForAll(operation, handler): Register a global hook for all models. Handler receives(args, model, operation, request?). Use'*'for operation to catch all operations.
Utilities
usePrisma<T>(): Get the current Prisma client instance with type safety.getRequest(): Get the current Fastify request from async context (returnsundefinedif not in request context).
Exported Types
PrismaOperation: Enum containing all supported Prisma operations.PrismaEventOperation: Enum for distinguishing before/after events.PrismaEventPayload<M, O, Args, Result>: Type for event payload data.GlobalPrismaHookHandler: Type for global hook handlers.GlobalPrismaEventHandler: Type for global event handlers.PluginOptions: Configuration options for the plugin.
Available Operations
The PrismaOperation enum includes:
FindUnique,FindUniqueOrThrow,FindFirst,FindFirstOrThrow,FindManyCreate,CreateMany,Update,UpdateManyDelete,DeleteMany,UpsertAggregate,GroupBy,Count
Summary
The @tsdiapi/prisma plugin streamlines Prisma integration by providing hooks, events, and lifecycle management. With this plugin, you can easily customize and extend database operations in a clean and modular way.
For more details, refer to the documentation.
