Skip to content

feat: Query middleware and lifecycle hooks #59

Description

@teetangh

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

  1. 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).
  2. Define a QueryMiddleware typedef: Future<dynamic> Function(QueryEvent event, Future<dynamic> Function(QueryEvent) next).
  3. Add void use(QueryMiddleware middleware) to PrismaClient. Middleware is stacked (first registered = outermost).
  4. In the delegate execution path, wrap the actual SQL execution in the middleware chain. Each middleware calls next(event) to proceed.
  5. 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).
  6. Add lifecycle hooks as sugar on top: prisma.onBeforeCreate<User>((data) => ...), prisma.onAfterUpdate<User>((result) => ...).
  7. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions