Skip to main content

Model Watcher

The Model Watcher tracks ORM model operations (TypeORM and Prisma), monitoring CRUD operations, entity changes, and database interactions at a higher level than raw queries.

What Gets Captured​

  • Operation type (find, create, update, delete, save)
  • Entity/model name
  • ORM source (TypeORM or Prisma)
  • Operation duration
  • Number of records affected
  • Entity data (if enabled, with masking)
  • Where conditions
  • Error messages

Configuration​

NestLensModule.forRoot({
watchers: {
model: {
enabled: true,
ignoreEntities: ['Migration', 'AuditLog'],
captureData: false, // Set to true to capture entity data
},
},
})

Configuration Options​

OptionTypeDefaultDescription
enabledbooleanfalseEnable/disable model tracking
ignoreEntitiesstring[][]Entity names to ignore
captureDatabooleanfalseCapture entity data (with masking)

Payload Structure​

interface ModelEntry {
type: 'model';
payload: {
action: 'find' | 'create' | 'update' | 'delete' | 'save';
entity: string; // Entity/model name
source: 'typeorm' | 'prisma';
duration: number; // Operation time (ms)
recordCount?: number; // Number of records affected
data?: unknown; // Entity data (masked, if captureData: true)
where?: unknown; // Query conditions
error?: string; // Error message
};
}

Usage Example​

TypeORM Setup​

import { NESTLENS_MODEL_SUBSCRIBER } from 'nestlens';
import { EntitySubscriberInterface, EventSubscriber } from 'typeorm';

@EventSubscriber()
export class NestLensEntitySubscriber implements EntitySubscriberInterface {
// Implement TypeORM subscriber methods
}

@Module({
providers: [
{
provide: NESTLENS_MODEL_SUBSCRIBER,
useClass: NestLensEntitySubscriber,
},
],
})
export class AppModule {}

Prisma Setup​

setupPrismaClient() returns the client to use, and on Prisma 6 and later that is a different object from the one passed in — see the note below. Keep the one it hands back:

import { Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import { ModelWatcher } from 'nestlens';

@Injectable()
export class PrismaService implements OnModuleInit {
client: PrismaClient;

constructor(private modelWatcher: ModelWatcher) {
this.client = new PrismaClient();
}

async onModuleInit() {
await this.client.$connect();
// Recorded from here on; queries through the old client are not.
this.client = this.modelWatcher.setupPrismaClient(this.client);
}
}

:::info Which Prisma API is used Prisma 5 and earlier have $use, the middleware API, which is installed into the client itself — there the returned client is the same object, and a service that extends PrismaClient works as before.

Prisma removed $use in 6.0, and NestLens uses a client extension instead. Extensions do not modify the client: $extends returns a new one, and only queries made through that are recorded. This is why the example above holds the client in a field rather than extending PrismaClient — this cannot be replaced from inside its own method. :::

Model Operations​

@Injectable()
export class UserService {
constructor(
@InjectRepository(User)
private userRepository: Repository<User>,
) {}

async findAll() {
// Tracked as: action: 'find', entity: 'User'
return this.userRepository.find();
}

async findOne(id: string) {
// Tracked with where condition
return this.userRepository.findOne({ where: { id } });
}

async create(data: CreateUserDto) {
// Tracked as: action: 'create', entity: 'User'
const user = this.userRepository.create(data);
return this.userRepository.save(user);
}

async update(id: string, data: UpdateUserDto) {
// Tracked as: action: 'update', entity: 'User'
await this.userRepository.update(id, data);
return this.findOne(id);
}

async remove(id: string) {
// Tracked as: action: 'delete', entity: 'User'
return this.userRepository.delete(id);
}
}

Dashboard View​

In the NestLens dashboard, model entries show:

  • Timeline of model operations
  • Most active entities
  • CRUD operation distribution
  • Slow operations
  • Entity access patterns
  • Data modification frequency

Sensitive Data Masking​

When captureData: true, sensitive fields are automatically masked:

  • password
  • passwordHash
  • secret
  • token
  • apiKey
  • accessToken
  • refreshToken
  • creditCard
  • ssn
  • privateKey
// With captureData: true
const user = await userRepository.save({
email: 'user@example.com',
password: 'secret123', // Shown as: '***MASKED***'
name: 'John Doe', // Shown normally
});

Prisma Operations​

// All tracked automatically
await prisma.user.findMany(); // action: 'find'
await prisma.user.findUnique({ where: { id } }); // action: 'find'
await prisma.user.create({ data }); // action: 'create'
await prisma.user.update({ where: { id }, data }); // action: 'update'
await prisma.user.delete({ where: { id } }); // action: 'delete'

Performance Analysis​

Identify N+1 Problems​

// BAD: N+1 queries (visible in dashboard)
const users = await this.userRepository.find();
for (const user of users) {
user.posts = await this.postRepository.find({
where: { userId: user.id }
});
}
// Dashboard shows: 1 User.find + N Post.find operations

// GOOD: Single query with relations
const users = await this.userRepository.find({
relations: ['posts'],
});
// Dashboard shows: 1 User.find operation

Filtering Entities​

NestLensModule.forRoot({
watchers: {
model: {
ignoreEntities: [
'Migration', // Ignore migration entities
'AuditLog', // Ignore audit logs
'SessionStore', // Ignore session storage
],
},
},
})