Problem
There is no way to intercept or transform queries globally before they execute. Common cross-cutting concerns — soft deletes, multi-tenancy row filtering, audit logging, query timing — must be manually wired into every repository method, leading to duplication and fragile code.
Desired API
// Register middleware on the PrismaClient
prisma.use((event, next) async {
final stopwatch = Stopwatch()..start();
// Soft-delete filter: auto-add WHERE deletedAt IS NULL
if (event.action == QueryAction.findMany ||
event.action == QueryAction.findFirst) {
event.args.where = event.args.where?.merge(
{'deletedAt': null},
) ?? {'deletedAt': null};
}
// Multi-tenancy: auto-inject tenantId
if (event.model != null) {
event.args.where = event.args.where?.merge(
{'tenantId': currentTenantId},
) ?? {'tenantId': currentTenantId};
}
final result = await next(event);
// Audit logging / query timing
logger.info('${event.model}.${event.action} took ${stopwatch.elapsedMilliseconds}ms');
return result;
});
Prisma JS reference
// Prisma JS middleware API for reference
prisma.$use(async (params, next) => {
if (params.model === 'Post' && params.action === 'delete') {
params.action = 'update';
params.args['data'] = { deleted: true };
}
return next(params);
});
Implementation approach
- Define a
QueryEvent class containing: model (String), action (QueryAction enum: findFirst, findMany, create, update, delete, etc.), args (the current query arguments), and rawSql (the compiled SQL, for read-only inspection).
- Define a
QueryMiddleware typedef: Future<dynamic> Function(QueryEvent event, Future<dynamic> Function(QueryEvent) next).
- Add
void use(QueryMiddleware middleware) to PrismaClient. Middleware is stacked (first registered = outermost).
- In the delegate execution path, wrap the actual SQL execution in the middleware chain. Each middleware calls
next(event) to proceed.
- Design a Dart-native interface:
- Use
Zone values or AsyncLocal for tenant context propagation (avoid global mutable state).
- Middleware should be composable:
prisma.use(softDeleteMiddleware).use(tenantMiddleware).
- Add lifecycle hooks as sugar on top:
prisma.onBeforeCreate<User>((data) => ...), prisma.onAfterUpdate<User>((result) => ...).
- Add tests for middleware ordering, short-circuiting, and error propagation.
Impact
- Enables soft deletes, multi-tenancy, audit logging, and query timing without per-repository boilerplate
- Foundational for plugin ecosystem (third-party middleware packages)
- Matches Prisma JS middleware API — familiar mental model
- Eliminates entire categories of copy-paste bugs in repository layers
Problem
There is no way to intercept or transform queries globally before they execute. Common cross-cutting concerns — soft deletes, multi-tenancy row filtering, audit logging, query timing — must be manually wired into every repository method, leading to duplication and fragile code.
Desired API
Prisma JS reference
Implementation approach
QueryEventclass containing:model(String),action(QueryAction enum: findFirst, findMany, create, update, delete, etc.),args(the current query arguments), andrawSql(the compiled SQL, for read-only inspection).QueryMiddlewaretypedef:Future<dynamic> Function(QueryEvent event, Future<dynamic> Function(QueryEvent) next).void use(QueryMiddleware middleware)toPrismaClient. Middleware is stacked (first registered = outermost).next(event)to proceed.Zonevalues orAsyncLocalfor tenant context propagation (avoid global mutable state).prisma.use(softDeleteMiddleware).use(tenantMiddleware).prisma.onBeforeCreate<User>((data) => ...),prisma.onAfterUpdate<User>((result) => ...).Impact