Skip to content

Installation

Preview: choose the consistency explicitly

Use consistency: 'atomic-required' with withAuditTransaction() for authoritative automatic records. Explicit best-effort is legacy non-atomic behavior and can leave orphan rows or stale diffs after caller rollback. See Automatic CUD Tracking for the complete boundary.

1. Install

bash
npm install @nestarc/audit-log @prisma/client @prisma/adapter-pg pg
npm install --save-dev prisma dotenv

audit-log 0.4 uses Prisma 7 as its primary target while retaining Prisma 5/6 peer compatibility. It requires Node.js 20.19+, 22.12+, or 24.x.

Upgrading from 0.3

consistency is now required. Choose atomic-required and move tracked writes into withAuditTransaction() for authoritative evidence, or explicitly select best-effort to preserve the old non-atomic behavior. Atomic mode rejects tracked writes outside the helper before mutation.

2. Configure Prisma 7

Use an explicit generated-client output and move the CLI datasource URL into prisma.config.ts:

prisma
// prisma/schema.prisma
generator client {
  provider = "prisma-client"
  output   = "../src/generated/prisma"
}

datasource db {
  provider = "postgresql"
}
typescript
// prisma.config.ts
import 'dotenv/config';
import { defineConfig, env } from 'prisma/config';

export default defineConfig({
  schema: 'prisma/schema.prisma',
  datasource: { url: env('DATABASE_URL') },
});

3. Create the audit_logs table

typescript
import { applyAuditTableSchema } from '@nestarc/audit-log';

// In a migration or setup script:
await applyAuditTableSchema(prisma);

Or use getAuditTableSQL() to get the raw SQL string for your migration tool.

4. Complete NestJS Integration

The library requires two Prisma clients with distinct roles:

  • Base client — used by AuditService for writing/querying audit logs
  • Extended client — used by your application code for business writes (CUD tracking fires here)
typescript
// prisma.service.ts
import { Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaPg } from '@prisma/adapter-pg';
import { Prisma, PrismaClient } from './generated/prisma/client';
import { createAuditedClient } from '@nestarc/audit-log';

export const prismaModule = { Prisma };

const auditExtensionOptions = {
  consistency: 'atomic-required' as const,
  trackedModels: ['User', 'Invoice', 'Document'],
  sensitiveFields: ['password', 'ssn'],
  ignoreTimestampOnlyUpdates: true,
  prismaModule,
  // primaryKey: { Order: 'orderNumber' }, // for non-id PKs
};

@Injectable()
export class PrismaService implements OnModuleInit {
  /** Base client — for audit storage (log/query) */
  readonly base = new PrismaClient({
    adapter: new PrismaPg({
      connectionString: process.env.DATABASE_URL!,
    }),
  });

  /** Extended client — use this for all application queries */
  readonly client = createAuditedClient(this.base, auditExtensionOptions);

  async onModuleInit() {
    await this.base.$connect();
  }
}
typescript
// prisma.module.ts
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';

@Global()
@Module({
  providers: [PrismaService],
  exports: [PrismaService],
})
export class PrismaModule {}
typescript
// app.module.ts
import { Module } from '@nestjs/common';
import { AuditLogModule } from '@nestarc/audit-log';
import { PrismaModule } from './prisma.module';
import { PrismaService, prismaModule } from './prisma.service';

@Module({
  imports: [
    PrismaModule,
    AuditLogModule.forRootAsync({
      inject: [PrismaService],
      useFactory: (prisma: PrismaService) => ({
        prisma: prisma.base,
        prismaModule,
        actorExtractor: (req) => ({
          id: req.user?.id ?? null,
          type: req.user ? 'user' : 'system',
          ip: req.ip,
        }),
        // tenantRequired: true, // fail-closed for multi-tenant deployments
        // correlationIdHeader: 'x-request-id',
      }),
    }),
  ],
})
export class AppModule {}
typescript
// user.service.ts — use prisma.client (extended) for all business writes
@Injectable()
export class UserService {
  constructor(private readonly prisma: PrismaService) {}

  async createUser(data: CreateUserDto) {
    // Automatic audit tracking fires because we use the extended client
    return this.prisma.client.withAuditTransaction((tx) =>
      tx.user.create({ data }),
    );
  }
}

With the Prisma 7 prisma-client generator, passing { Prisma } as prismaModule is required for both the extension and AuditLogModule. Prisma 5/6 applications using the legacy @prisma/client output can keep their existing imports. See Prisma 7 Setup.

createAuditExtension Options

OptionTypeDefaultDescription
consistency'atomic-required' | 'best-effort'requiredSelect the atomic helper contract or explicit legacy behavior
databaseMappingRecord<string, { tableName; schema?; primaryKeyColumn? }>{}PostgreSQL identifiers used for atomic row locks when public Prisma mapping metadata is unavailable
maxBatchRecordsnumber1000Per-record atomic deleteMany cap
batchOverflow'reject' | 'summary''reject'Summary overflow is best-effort-only
trackedModelsstring[]all models when omittedAllowlist of Prisma model names to track. trackedModels: [] means no models are audited
ignoredModelsstring[][]Denylist used only when trackedModels is not set
sensitiveFieldsstring[][]Fields to mask as [REDACTED] in diffs
sensitiveFieldsByModelRecord<string, string[]>{}Per-model fields unioned with sensitiveFields
primaryKeyRecord<string, string>{ *: 'id' }Map of model name to primary key field name
tableNamestringaudit_logsAudit table used by automatic inserts
tenantRequiredbooleanfalseMissing tenant rolls back atomic mutations; best-effort skips the audit row and reports it
tenantResolver() => string | nullCustom tenant lookup before the optional @nestarc/tenancy fallback
onAuditError(error, ctx) => voidStructured callback for automatic audit failures
loggerAuditLoggerconsoleLogger used for audit warnings and errors
logFailuresbooleanfalseRecord best-effort result='failure' rows when business writes throw
ignoreTimestampOnlyUpdatesbooleanfalseSuppress @updatedAt-only update entries
prismaModulegenerated Prisma modulelegacy @prisma/client fallbackRequired with the Prisma 7 prisma-client generator; pass { Prisma } from the generated output
experimentalTxAuditbooleanfalseDeprecated compatibility path available only with best-effort; prefer atomic-required

When neither trackedModels nor ignoredModels is configured, createAuditExtension() audits all Prisma models and emits a one-time warning. Set trackedModels as an allowlist or ignoredModels as a denylist to narrow scope.

AuditLogModule Options

OptionTypeDefaultDescription
prismaPrismaClientrequiredBase Prisma client for audit storage
actorExtractor(req) => AuditActor | Promise<AuditActor>requiredExtracts actor from HTTP request
tenantRequiredbooleanfalseWhen true, module-side log() and ambient query()/getById() require tenant context unless tenantId or allTenants is explicit
excludeRoutesRouteInfo[][]Routes excluded from AuditActorMiddleware
registerGlobalInterceptorbooleantrueSet false to bind AuditInterceptor manually
correlationIdHeaderstringx-request-idHeader copied into metadata.correlationId
correlationIdGetter(req) => string | undefinedCustom correlation ID source
tableNamestringaudit_logsAudit table name used by module-side log/query/scan/export/prune APIs
tenantResolver() => string | nullCustom tenant lookup before the optional @nestarc/tenancy fallback
sensitiveFieldsstring[][]Metadata redaction keys for manual logs
sensitiveFieldsByModelRecord<string, string[]>{}Model-specific metadata redaction keys
onAuditError(error, ctx) => voidStructured callback for module-side audit failures
loggerAuditLoggerconsoleLogger used for audit warnings and errors
prismaModulegenerated Prisma modulelegacy @prisma/client fallbackRequired with the Prisma 7 prisma-client generator; pass { Prisma } from the generated output

Schema Utilities

FunctionDescription
getAuditTableSQL(options?)Returns raw SQL string for creating audit tables, trigger enforcement, optional partitions, and indexes
getAuditTableStatements(options?)Returns SQL split into individual executable statements
applyAuditTableSchema(prisma, options?)Executes the schema SQL statement by statement via Prisma
ensurePartitions(prisma, options?)Creates missing monthly partitions for partitioned audit tables

Released under the MIT License.