npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@jsonbored/prismocker

v0.1.0

Published

A type-safe, in-memory Prisma Client mock for testing. Works perfectly with pnpm and supports all Prisma operations.

Downloads

85

Readme

Prismocker

A type-safe, in-memory Prisma Client mock for testing

Works perfectly with pnpm, Jest, and Vitest. Fully compatible with the Prisma ecosystem including generated Zod schemas, PrismaJson types, and Prisma extensions.

Why Prismocker? Prismocker solves the critical problem of testing Prisma-based applications without a real database. It provides a complete, type-safe mock that works seamlessly with all Prisma generators and extensions, making it the perfect testing companion for modern Prisma applications.

Package Info npm version npm downloads License

Status CI TypeScript Node

📑 Table of Contents

✨ Features

Prismocker provides a complete, type-safe mock for Prisma Client that:

  • Works with pnpm - Solves module resolution issues that plague other Prisma mocks
  • Type-safe - Uses Prisma's generated types, eliminates as any assertions
  • Full Prisma API - Supports all Prisma operations (findMany, create, update, delete, count, aggregate, groupBy, etc.)
  • Full Relation Support - Complete include/select support with relation filters (some, every, none)
  • Transaction Rollback - Automatic rollback on errors with state snapshotting
  • Middleware Support - Full $use() middleware support for intercepting and modifying operations
  • Event Listeners - $on() event listener support for query events and lifecycle hooks
  • Lifecycle Methods - $connect(), $disconnect(), and $metrics() API compatibility
  • Enhanced Error Messages - Comprehensive, actionable errors with debugging hints
  • Prisma Ecosystem Compatible - Works with generated Zod schemas, PrismaJson types, and Prisma extensions
  • Fast & Isolated - In-memory storage with automatic indexing for performance, perfect for unit tests
  • Performance Optimized - Automatic index management for primary keys, foreign keys, and custom fields
  • Minimal Dependencies - Only requires @prisma/client as a peer dependency (no runtime dependencies)
  • Environment Agnostic - Works with any Prisma generator setup, not tied to specific environments
  • Standalone Package - Can be extracted to separate repo for OSS distribution

🆚 Why Prismocker?

Prismocker was created to solve specific challenges when testing Prisma-based applications. Here's what makes it unique:

Key Differentiators

| Feature | Prismocker | Notes | |:---|:---:|:---| | Type Safety | ✅ Full (ExtractModels) | Complete type preservation from your Prisma schema - no as any assertions needed | | pnpm Support | ✅ Perfect | Designed from the ground up to work seamlessly with pnpm's module resolution | | Prisma API Coverage | ✅ Complete | Supports all Prisma operations including advanced features like aggregations, transactions, and extensions | | Setup Complexity | ✅ Auto-setup CLI | One command setup with automatic framework detection and enum generation | | Relations | ✅ Full (include/select/filters) | Complete relation support with some, every, none filters and nested relations | | Transactions | ✅ Full rollback support | Automatic state snapshotting and rollback on errors for realistic transaction behavior | | Ecosystem Compatible | ✅ Zod/Extensions/PrismaJson | Works seamlessly with generated Zod schemas, Prisma extensions, and PrismaJson types | | Dependencies | ✅ Minimal | Only requires @prisma/client as a peer dependency (no runtime dependencies) |

How Prismocker Differs from Alternatives

Compared to other Prisma mocking solutions:

  • No schema parsing overhead - Works directly with Prisma's generated types
  • Type-preserving Proxy system - Maintains full TypeScript type safety without assertions
  • Built for pnpm - Solves module resolution issues that can occur with other solutions
  • Auto-setup tooling - CLI commands for setup, enum generation, and verification
  • Prisma ecosystem first - Designed to work with the entire Prisma toolchain

Compared to manual mocks:

  • Real Prisma API behavior - Matches Prisma's actual behavior, not simplified mocks
  • Less boilerplate - No need to manually mock every operation
  • Type-safe by default - Full TypeScript support out of the box
  • Maintainable - Automatically stays in sync with Prisma API changes

🚀 Quick Start

import { createPrismocker } from '@jsonbored/prismocker';
import type { PrismaClient } from '@prisma/client';

// ✅ Fully type-safe! Returns ExtractModels<PrismaClient>
const prisma = createPrismocker<PrismaClient>();

// Seed test data
prisma.setData('companies', [
  { id: '1', name: 'Acme Corp', owner_id: 'user-1' }
]);

// Use like real Prisma - fully typed!
const companies = await prisma.companies.findMany();
const company = await prisma.companies.findUnique({
  where: { id: '1' }
});

// All operations are type-safe
await prisma.companies.create({
  data: { name: 'New Corp', owner_id: 'user-2', slug: 'new-corp' }
});

📦 Installation

npm install @jsonbored/prismocker --save-dev
# or
pnpm add -D @jsonbored/prismocker
# or
yarn add -D @jsonbored/prismocker

Peer Dependencies:

  • @prisma/client (^7.0.0 or higher)
  • zod (optional, for Zod validation support)

⚡ Auto-Setup (Recommended)

The easiest way to get started with Prismocker is using the auto-setup command:

npx @jsonbored/prismocker setup

This command will:

  1. ✅ Detect your testing framework (Jest or Vitest)
  2. ✅ Create the __mocks__/@prisma/client.ts file
  3. ✅ Update your test setup files (jest.setup.ts or vitest.setup.ts)
  4. ✅ Generate enum stubs from your Prisma schema
  5. ✅ Create example test files (optional)

Options:

# Specify framework manually
npx @jsonbored/prismocker setup --framework jest

# Custom schema/mock paths
npx @jsonbored/prismocker setup --schema ./prisma/schema.prisma --mock ./__mocks__/@prisma/client.ts

# Skip example files
npx @jsonbored/prismocker setup --skip-examples

After setup, run npx @jsonbored/prismocker generate-enums whenever you add or modify enums in your Prisma schema.

📚 Quick Start Guide

import { createPrismocker } from '@jsonbored/prismocker';
import type { PrismaClient } from '@prisma/client';

// ✅ Fully type-safe! Returns ExtractModels<PrismaClient>
const prisma = createPrismocker<PrismaClient>();

// ✅ All model access is fully typed - no `as any` needed!
const companies = await prisma.companies.findMany();
// companies is typed as Company[] (from your Prisma schema)

const company = await prisma.companies.findUnique({
  where: { id: 'company-1' },
});
// company is typed as Company | null

await prisma.companies.create({
  data: {
    name: 'Company 1',
    owner_id: 'user-1',
    slug: 'company-1',
  },
});
// ✅ Full type checking - TypeScript will error if fields don't match schema

// ✅ Prismocker methods are also fully typed
prisma.reset();
prisma.setData('companies', []);
const data = prisma.getData('companies');

Key Benefits:

  • Full Type Safety - All model access is typed using Prisma's generated types
  • No Type Assertions - No need for as any or as unknown assertions
  • IntelliSense Support - Full autocomplete and type checking in your IDE
  • Type Preservation - ExtractModels<T> preserves all model types through Proxy

💡 Tip: Use npx @jsonbored/prismocker setup to automatically set up Jest integration!

Manual Setup

Step 1: Create Mock File

Create __mocks__/@prisma/client.ts in your project root:

import { createPrismocker } from '@jsonbored/prismocker';
import type { PrismaClient } from '@prisma/client';

// Create PrismockerClient instance
const PrismockerClientClass = createPrismocker<PrismaClient>();

// Export as PrismaClient for Jest auto-mocking
export { PrismockerClientClass as PrismaClient };

// Export Prisma namespace (for Prisma.Decimal, etc.)
export const Prisma = {
  Decimal: class Decimal {
    value: any;
    constructor(value: any) {
      this.value = value;
    }
    toString() {
      return String(this.value);
    }
    toNumber() {
      return Number(this.value);
    }
    toFixed(decimalPlaces?: number) {
      return Number(this.value).toFixed(decimalPlaces);
    }
    toJSON() {
      return this.value;
    }
  },
};

// Export Prisma enum stubs (auto-generated - see Enum Support section)
// Run: npx @jsonbored/prismocker generate-enums
export { job_status, job_type /* ... other enums */ } from './enums';

Step 2: Use in Tests

import { prisma } from '@heyclaude/data-layer/prisma/client';
import type { PrismaClient } from '@prisma/client';

describe('MyService', () => {
  let prisma: PrismaClient;

  beforeEach(() => {
    // PrismaClient is automatically PrismockerClient in tests
    prisma = prisma;

    // Reset data before each test
    if ('reset' in prisma && typeof (prisma as any).reset === 'function') {
      (prisma as any).reset();
    }

    // Seed test data
    if ('setData' in prisma && typeof (prisma as any).setData === 'function') {
      (prisma as any).setData('companies', [
        { id: 'company-1', name: 'Company 1', owner_id: 'user-1' },
      ]);
    }
  });

  it('should query companies', async () => {
    const companies = await prisma.companies.findMany();
    expect(companies).toHaveLength(1);
  });
});

Step 3: Verify Jest Auto-Mock

Jest will automatically use __mocks__/@prisma/client.ts when you import @prisma/client in your code. No additional configuration needed!

💡 Tip: Use npx @jsonbored/prismocker setup to automatically set up Vitest integration!

Manual Setup

Step 1: Create Mock File

Create __mocks__/@prisma/client.ts (same as Jest):

import { createPrismocker } from '@jsonbored/prismocker';
import type { PrismaClient } from '@prisma/client';

const PrismockerClientClass = createPrismocker<PrismaClient>();
export { PrismockerClientClass as PrismaClient };

// ... Prisma namespace and enum exports ...

Step 2: Register Mock

Add to vitest.setup.ts:

import { vi } from 'vitest';

// Explicitly mock @prisma/client to use Prismocker
vi.mock('@prisma/client', async () => {
  const mockModule = await import('./__mocks__/@prisma/client.ts');
  return mockModule;
});

Step 3: Use in Tests

import { PrismaClient } from '@prisma/client';
import { job_status } from '@prisma/client'; // ✅ Enum stubs work!

// PrismaClient is automatically PrismockerClient in tests
const prisma = new PrismaClient();

📖 API Reference

Factory Functions

Creates a new PrismockerClient instance that implements the PrismaClient interface.

import { createPrismocker } from '@jsonbored/prismocker';
import type { PrismaClient } from '@prisma/client';

const prisma = createPrismocker<PrismaClient>({
  logQueries: true,
  validateWithZod: true,
  zodSchemasPath: '@prisma/zod',
});

Type Parameters:

  • T - PrismaClient type (must extend PrismaClient, defaults to PrismaClient)

Options:

  • logQueries?: boolean - Enable query logging (default: false)
  • logger?: (message: string, data?: any) => void - Custom logger (default: console.log)
  • validateWithZod?: boolean - Enable Zod validation for create/update (default: false)
  • zodSchemasPath?: string - Path to generated Zod schemas (default: '@prisma/zod')
  • zodSchemaLoader?: (modelName: string, operation: string) => Promise<any> | any | undefined - Custom schema loader

Returns: ExtractModels<T> - PrismockerClient instance with full type preservation

Type Safety:

The returned instance is typed as ExtractModels<T>, which:

  • ✅ Preserves all model types from PrismaClient (e.g., prisma.companies is fully typed)
  • ✅ Preserves all Prisma methods ($queryRaw, $transaction, etc.)
  • ✅ Adds Prismocker-specific methods (reset, setData, getData, etc.)
  • ✅ Eliminates the need for as any assertions for models in your schema

Example:

import { createPrismocker } from '@jsonbored/prismocker';
import type { PrismaClient } from '@prisma/client';
import type { ExtractModels } from 'prisma/prisma-types';

const prisma = createPrismocker<PrismaClient>();

// ✅ prisma is typed as ExtractModels<PrismaClient>
const _typeCheck: ExtractModels<PrismaClient> = prisma;

// ✅ prisma.companies is fully typed as PrismaClient['companies']
const companies = await prisma.companies.findMany();
// companies is typed as Company[] (from your Prisma schema)

// ✅ No type assertions needed!
prisma.reset();
prisma.setData('companies', []);

Convenience function for creating a test PrismaClient instance with sensible defaults.

import { createTestPrisma } from 'prisma/test-utils';
import type { PrismaClient } from '@prisma/client';

const prisma = createTestPrisma();
// Equivalent to: createPrismocker<PrismaClient>()

Type-Safe Helpers

Type-safe utilities for Jest testing:

import { isPrismockerClient, createMockQueryRawUnsafe } from 'prisma/jest-helpers';
import type { PrismaClient } from '@prisma/client';

let prisma: PrismaClient;

beforeEach(() => {
  prisma = createPrismocker<PrismaClient>();

  // ✅ Type-safe check
  if (isPrismockerClient(prisma)) {
    prisma.reset(); // ✅ No type assertion needed
    prisma.setData('companies', []); // ✅ Type-safe
  }

  // ✅ Type-safe mock
  const mockQuery = createMockQueryRawUnsafe(prisma);
  prisma.$queryRawUnsafe = mockQuery;
});

Available Helpers:

  • isPrismockerClient(prisma: PrismaClient): boolean - Type guard for PrismockerClient
  • createMockQueryRawUnsafe(prisma: PrismaClient): MockQueryRawUnsafe - Type-safe mock for $queryRawUnsafe
  • createMockQueryRaw(prisma: PrismaClient): MockQueryRaw - Type-safe mock for $queryRaw
  • createMockTransaction(prisma: PrismaClient): MockTransaction - Type-safe mock for $transaction

Convenient helpers for test setup and data management:

import {
  createTestPrisma,
  resetAndSeed,
  createTestDataFactory,
  snapshotPrismocker,
  restorePrismocker,
} from 'prisma/test-utils';
import type { PrismaClient } from '@prisma/client';

const prisma = createTestPrisma();

// Create data factory for consistent test data
const companyFactory = createTestDataFactory({
  name: 'Test Company',
  owner_id: 'test-user',
  slug: 'test-company',
});

beforeEach(() => {
  // Reset and seed in one call
  resetAndSeed(prisma, {
    companies: [companyFactory({ name: 'Company 1' }), companyFactory({ name: 'Company 2' })],
    jobs: [{ id: 'job-1', company_id: 'company-1', title: 'Job 1' }],
  });
});

// Snapshot and restore for complex test scenarios
it('should handle complex state', async () => {
  const snapshot = snapshotPrismocker(prisma);

  // Make changes
  await prisma.companies.create({ data: { name: 'New Company' } });

  // Restore original state
  restorePrismocker(prisma, snapshot);
});

Available Utilities:

  • createTestPrisma(): PrismaClient - Create test PrismaClient instance
  • resetAndSeed(prisma: PrismaClient, data: Record<string, any[]>): void - Reset and seed data
  • createTestDataFactory<T>(defaults: Partial<T>): (overrides?: Partial<T>) => T - Create data factory
  • snapshotPrismocker(prisma: PrismaClient, modelNames?: string[]): Record<string, any[]> - Snapshot current state
  • restorePrismocker(prisma: PrismaClient, snapshot: Record<string, any[]>): void - Restore from snapshot

Type-safe utilities for working with Prisma models and types:

ExtractModels - Type Preservation

The core type utility that preserves all model types from PrismaClient:

import type { ExtractModels } from 'prisma/prisma-types';
import type { PrismaClient } from '@prisma/client';

// ExtractModels<T> preserves all model types
type PrismockerClient = ExtractModels<PrismaClient>;

// This means:
// - prisma.companies → PrismaClient['companies'] (fully typed)
// - prisma.jobs → PrismaClient['jobs'] (fully typed)
// - All Prisma methods preserved
// - Prismocker methods added

const prisma = createPrismocker<PrismaClient>();
// prisma is typed as ExtractModels<PrismaClient>

// ✅ Full type safety - no assertions needed!
const companies = await prisma.companies.findMany();
// companies is typed as Company[] (from your Prisma schema)

Type Helpers

import {
  ExtractModels,
  ModelName,
  ModelType,
  setDataTyped,
  getDataTyped,
} from 'prisma/prisma-types';
import type { PrismaClient } from '@prisma/client';

const prisma = createPrismocker<PrismaClient>();

// ExtractModels<T> - Preserves all model types
type PrismockerClient = ExtractModels<PrismaClient>;

// ModelName<T> - Extract model name type
type CompanyModelName = ModelName<'companies'>; // 'companies'

// ModelType<TClient, TModel> - Extract model delegate type
type CompanyModel = ModelType<PrismaClient, 'companies'>;
// CompanyModel is the type of prisma.companies

// ✅ Type-safe setData with model type inference
setDataTyped(prisma, 'companies', [
  { id: '1', name: 'Company 1', owner_id: 'user-1', slug: 'company-1' },
]);

// ✅ Type-safe getData
const companies = getDataTyped(prisma, 'companies');
// companies is typed as any[] (can be explicitly typed if needed)

Available Type Helpers:

  • ExtractModels<T> - Core type utility that preserves all model types from PrismaClient
  • ModelName<T> - Extract model name type from Prisma.ModelName
  • ModelType<TClient, TModel> - Extract model delegate type from PrismaClient
  • setDataTyped<TClient>(prisma: TClient, model: string, data: any[]): void - Type-safe data seeding
  • getDataTyped<TClient>(prisma: TClient, model: string): any[] - Type-safe data retrieval

Note: setDataTyped and getDataTyped accept string for model names to support dynamic models. For models in your schema, you can use direct model access without these helpers:

// ✅ Direct model access (fully typed for models in schema)
const companies = await prisma.companies.findMany();

// ✅ Helper functions (useful for dynamic models or test utilities)
setDataTyped(prisma, 'companies', []);
const data = getDataTyped(prisma, 'companies');

Configuration Options

interface PrismockerOptions {
  /**
   * Whether to log queries (useful for debugging)
   * @default false
   */
  logQueries?: boolean;

  /**
   * Custom logger function
   * @default console.log
   */
  logger?: (message: string, data?: any) => void;

  /**
   * Enable Zod schema validation for create/update operations
   * Requires prisma-zod-generator to be configured
   * @default false
   */
  validateWithZod?: boolean;

  /**
   * Path to generated Zod schemas
   * Defaults to '@prisma/zod' or '@heyclaude/database-types/prisma/zod'
   * @default '@prisma/zod'
   */
  zodSchemasPath?: string;

  /**
   * Custom Zod schema loader function
   * Allows custom loading logic for Zod schemas
   */
  zodSchemaLoader?: (
    modelName: string,
    operation: 'create' | 'update' | 'where' | 'select' | 'include'
  ) => Promise<any> | any | undefined;
}

💡 Usage Examples

Test your service layer with Prismocker - fully type-safe:

import { describe, it, expect, beforeEach } from '@jest/globals';
import { createPrismocker } from '@jsonbored/prismocker';
import type { PrismaClient } from '@prisma/client';
import { isPrismockerClient } from 'prisma/jest-helpers';
import { CompaniesService } from './companies-service';

describe('CompaniesService', () => {
  let prisma: PrismaClient;
  let service: CompaniesService;

  beforeEach(() => {
    // ✅ Create Prismocker instance - fully typed!
    prisma = createPrismocker<PrismaClient>();

    // ✅ Type-safe reset and seeding
    if (isPrismockerClient(prisma)) {
      prisma.reset(); // ✅ No type assertion needed
      prisma.setData('companies', [
        { id: 'company-1', name: 'Company 1', owner_id: 'user-1', slug: 'company-1' },
        { id: 'company-2', name: 'Company 2', owner_id: 'user-2', slug: 'company-2' },
      ]); // ✅ Fully typed
    }

    service = new CompaniesService(prisma);
  });

  it('should get company by slug', async () => {
    const company = await service.getCompanyBySlug('company-1');

    expect(company).toMatchObject({
      id: 'company-1',
      name: 'Company 1',
      slug: 'company-1',
    });
  });

  it('should create company', async () => {
    const company = await service.createCompany({
      name: 'New Company',
      owner_id: 'user-1',
      slug: 'new-company',
    });

    expect(company.name).toBe('New Company');

    // Verify it was created
    const allCompanies = await prisma.companies.findMany();
    expect(allCompanies).toHaveLength(3);
  });
});

Test Next.js API routes with Prismocker:

import { describe, it, expect, beforeEach } from '@jest/globals';
import { NextRequest } from 'next/server';
import { prisma } from '@heyclaude/data-layer/prisma/client';
import type { PrismaClient } from '@prisma/client';
import { GET } from './route';

describe('GET /api/company', () => {
  let prisma: PrismaClient;

  beforeEach(() => {
    prisma = prisma;

    if ('reset' in prisma && typeof (prisma as any).reset === 'function') {
      (prisma as any).reset();
    }

    // Seed test data
    if ('setData' in prisma && typeof (prisma as any).setData === 'function') {
      (prisma as any).setData('companies', [
        { id: 'company-1', name: 'Company 1', slug: 'company-1', owner_id: 'user-1' },
      ]);
    }
  });

  it('should return company by slug', async () => {
    const request = new NextRequest('http://localhost/api/company?slug=company-1');
    const response = await GET(request);
    const data = await response.json();

    expect(response.status).toBe(200);
    expect(data).toMatchObject({
      id: 'company-1',
      name: 'Company 1',
      slug: 'company-1',
    });
  });

  it('should return 404 for non-existent company', async () => {
    const request = new NextRequest('http://localhost/api/company?slug=non-existent');
    const response = await GET(request);

    expect(response.status).toBe(404);
  });
});

Test complex Prisma queries with filters, sorting, and pagination - fully type-safe:

import { describe, it, expect, beforeEach } from '@jest/globals';
import { createPrismocker } from '@jsonbored/prismocker';
import type { PrismaClient } from '@prisma/client';
import { isPrismockerClient } from 'prisma/jest-helpers';

describe('Complex Queries', () => {
  let prisma: PrismaClient;

  beforeEach(() => {
    // ✅ Create Prismocker instance - fully typed!
    prisma = createPrismocker<PrismaClient>();

    // ✅ Type-safe reset and seeding
    if (isPrismockerClient(prisma)) {
      prisma.reset(); // ✅ No type assertion needed
      prisma.setData('jobs', [
        { id: 'job-1', title: 'Senior Engineer', status: 'published', view_count: 100 },
        { id: 'job-2', title: 'Junior Engineer', status: 'published', view_count: 50 },
        { id: 'job-3', title: 'Product Manager', status: 'draft', view_count: 200 },
      ]); // ✅ Fully typed
    }
  });

  it('should filter by status and sort by view_count', async () => {
    // ✅ Model access and queries are fully typed!
    const jobs = await prisma.jobs.findMany({
      where: {
        status: 'published',
      },
      orderBy: {
        view_count: 'desc',
      },
    });

    expect(jobs).toHaveLength(2);
    expect(jobs[0].view_count).toBe(100);
    expect(jobs[1].view_count).toBe(50);
  });

  it('should paginate results', async () => {
    // ✅ Pagination is fully typed!
    const page1 = await prisma.jobs.findMany({
      skip: 0,
      take: 2,
      orderBy: { view_count: 'desc' },
    });
    // page1 is typed as Job[]

    expect(page1).toHaveLength(2);

    const page2 = await prisma.jobs.findMany({
      skip: 2,
      take: 2,
      orderBy: { view_count: 'desc' },
    });
    // page2 is typed as Job[]

    expect(page2).toHaveLength(1);
  });

  it('should use complex where clauses', async () => {
    const jobs = await prisma.jobs.findMany({
      where: {
        AND: [{ status: 'published' }, { view_count: { gte: 75 } }],
        OR: [{ title: { contains: 'Engineer' } }, { title: { contains: 'Manager' } }],
      },
    });

    expect(jobs).toHaveLength(1);
    expect(jobs[0].title).toBe('Senior Engineer');
  });
});

Prismocker supports comprehensive aggregation operations including statistical functions:

import { describe, it, expect, beforeEach } from '@jest/globals';
import { prisma } from '@heyclaude/data-layer/prisma/client';
import type { PrismaClient } from '@prisma/client';

describe('Aggregations', () => {
  let prisma: PrismaClient;

  beforeEach(() => {
    prisma = prisma;

    if ('reset' in prisma && typeof (prisma as any).reset === 'function') {
      (prisma as any).reset();
    }

    // Seed test data with numeric values
    if ('setData' in prisma && typeof (prisma as any).setData === 'function') {
      (prisma as any).setData('jobs', [
        { id: 'job-1', company_id: 'company-1', title: 'Job 1', view_count: 100 },
        { id: 'job-2', company_id: 'company-1', title: 'Job 2', view_count: 200 },
        { id: 'job-3', company_id: 'company-2', title: 'Job 3', view_count: 150 },
        { id: 'job-4', company_id: 'company-2', title: 'Job 4', view_count: 75 },
        { id: 'job-5', company_id: 'company-3', title: 'Job 5', view_count: 50 },
      ]);
    }
  });

  it('should aggregate with _count, _avg, _sum, _min, _max', async () => {
    const stats = await prisma.jobs.aggregate({
      _count: { id: true },
      _avg: { view_count: true },
      _sum: { view_count: true },
      _min: { view_count: true },
      _max: { view_count: true },
    });

    expect(stats._count?.id).toBe(5);
    expect(stats._avg?.view_count).toBe(115); // (100 + 200 + 150 + 75 + 50) / 5
    expect(stats._sum?.view_count).toBe(575);
    expect(stats._min?.view_count).toBe(50);
    expect(stats._max?.view_count).toBe(200);
  });

  it('should aggregate with _stddev (standard deviation)', async () => {
    const stats = await prisma.jobs.aggregate({
      _stddev: { view_count: true },
    });

    // Mean = (100 + 200 + 150 + 75 + 50) / 5 = 115
    // Variance = ((100-115)^2 + (200-115)^2 + (150-115)^2 + (75-115)^2 + (50-115)^2) / 5
    //          = (225 + 7225 + 1225 + 1600 + 4225) / 5 = 14500 / 5 = 2900
    // Stddev = sqrt(2900) ≈ 53.85
    expect(stats._stddev?.view_count).toBeCloseTo(53.85, 1);
  });

  it('should aggregate with _variance', async () => {
    const stats = await prisma.jobs.aggregate({
      _variance: { view_count: true },
    });

    // Mean = 115, Variance = 2900
    expect(stats._variance?.view_count).toBeCloseTo(2900, 0);
  });

  it('should aggregate with _countDistinct', async () => {
    const stats = await prisma.jobs.aggregate({
      _countDistinct: { company_id: true },
    });

    // Should have 3 distinct company_id values: company-1, company-2, company-3
    expect(stats._countDistinct?.company_id).toBe(3);
  });

  it('should handle _stddev with single value', async () => {
    // Reset and create single record
    if ('reset' in prisma && typeof (prisma as any).reset === 'function') {
      (prisma as any).reset();
    }
    await prisma.jobs.create({
      data: { id: 'job-1', company_id: 'company-1', title: 'Job 1', view_count: 100 },
    });

    const stats = await prisma.jobs.aggregate({
      _stddev: { view_count: true },
    });

    // Single value: stddev should be 0
    expect(stats._stddev?.view_count).toBe(0);
  });

  it('should handle _variance with single value', async () => {
    // Reset and create single record
    if ('reset' in prisma && typeof (prisma as any).reset === 'function') {
      (prisma as any).reset();
    }
    await prisma.jobs.create({
      data: { id: 'job-1', company_id: 'company-1', title: 'Job 1', view_count: 100 },
    });

    const stats = await prisma.jobs.aggregate({
      _variance: { view_count: true },
    });

    // Single value: variance should be 0
    expect(stats._variance?.view_count).toBe(0);
  });

  it('should handle aggregations with where clause', async () => {
    const stats = await prisma.jobs.aggregate({
      where: { company_id: 'company-1' },
      _count: { id: true },
      _avg: { view_count: true },
      _sum: { view_count: true },
    });

    // Only company-1 jobs: job-1 (100) and job-2 (200)
    expect(stats._count?.id).toBe(2);
    expect(stats._avg?.view_count).toBe(150); // (100 + 200) / 2
    expect(stats._sum?.view_count).toBe(300);
  });
});

Supported Aggregation Operations:

  • _count - Count records
  • _sum - Sum numeric fields
  • _avg - Average numeric fields
  • _min - Minimum value (numeric or date)
  • _max - Maximum value (numeric or date)
  • _stddev - Standard deviation (statistical)
  • _variance - Variance (statistical)
  • _countDistinct - Count distinct values

Edge Cases:

  • Single value: _stddev and _variance return 0
  • No values: _stddev and _variance return null, _countDistinct returns 0
  • All operations support where clause filtering
  • Non-numeric values are automatically filtered out for numeric operations

Prismocker supports findUniqueOrThrow and findFirstOrThrow methods that throw errors when records are not found:

findUniqueOrThrow

Similar to findUnique, but throws an error if no record is found:

import { describe, it, expect, beforeEach } from '@jest/globals';
import { createPrismocker } from '@jsonbored/prismocker';
import type { PrismaClient } from '@prisma/client';

describe('findUniqueOrThrow', () => {
  let prisma: PrismaClient;

  beforeEach(() => {
    prisma = createPrismocker<PrismaClient>();
    if ('reset' in prisma && typeof (prisma as any).reset === 'function') {
      (prisma as any).reset();
    }
  });

  it('should return record when found', async () => {
    await prisma.companies.create({
      data: { id: 'company-1', name: 'Company 1', owner_id: 'user-1', slug: 'company-1' },
    });

    const company = await prisma.companies.findUniqueOrThrow({ where: { id: 'company-1' } });
    expect(company.name).toBe('Company 1');
  });

  it('should throw error when not found', async () => {
    await expect(
      prisma.companies.findUniqueOrThrow({ where: { id: 'non-existent' } })
    ).rejects.toThrow('Record not found');
  });

  it('should include helpful error message with context', async () => {
    await prisma.companies.create({
      data: { id: 'company-1', name: 'Company 1', owner_id: 'user-1', slug: 'company-1' },
    });

    try {
      await prisma.companies.findUniqueOrThrow({ where: { id: 'non-existent' } });
      expect(true).toBe(false); // Should not reach here
    } catch (error: any) {
      expect(error.message).toContain('Record not found');
      expect(error.message).toContain('Where clause');
      expect(error.message).toContain('Total records');
      expect(error.message).toContain('Sample records');
    }
  });
});

findFirstOrThrow

Similar to findFirst, but throws an error if no record is found:

it('should return record when found', async () => {
  await prisma.companies.create({
    data: { id: 'company-1', name: 'Company 1', owner_id: 'user-1', slug: 'company-1' },
  });

  const company = await prisma.companies.findFirstOrThrow({ where: { name: 'Company 1' } });
  expect(company.name).toBe('Company 1');
});

it('should throw error when not found', async () => {
  await expect(
    prisma.companies.findFirstOrThrow({ where: { name: 'Non-existent Company' } })
  ).rejects.toThrow('Record not found');
});

Key Features:

  • ✅ Throws descriptive error when record not found
  • ✅ Enhanced error messages with context and suggestions
  • ✅ Supports include and select (same as findUnique/findFirst)
  • ✅ Works with all where clause operators
  • ✅ Perfect for testing error scenarios

When to Use:

  • Testing error handling when records don't exist
  • Ensuring code fails fast when required records are missing
  • Matching Prisma's real behavior in production

Prismocker supports full relation functionality including include, select, and relation filters (some, every, none).

Basic Relation Loading

import { describe, it, expect, beforeEach } from '@jest/globals';
import { createPrismocker } from '@jsonbored/prismocker';
import type { PrismaClient } from '@prisma/client';
import { isPrismockerClient } from 'prisma/jest-helpers';

describe('Relations', () => {
  let prisma: PrismaClient;

  beforeEach(() => {
    // ✅ Create Prismocker instance - fully typed!
    prisma = createPrismocker<PrismaClient>();

    // ✅ Type-safe reset and seeding
    if (isPrismockerClient(prisma)) {
      prisma.reset(); // ✅ No type assertion needed
      prisma.setData('companies', [{ id: 'company-1', name: 'Company 1', owner_id: 'user-1' }]); // ✅ Fully typed
      prisma.setData('jobs', [
        { id: 'job-1', company_id: 'company-1', title: 'Job 1', status: 'published' },
        { id: 'job-2', company_id: 'company-1', title: 'Job 2', status: 'draft' },
      ]); // ✅ Fully typed
    }
  });

  it('should load one-to-many relations with include', async () => {
    // ✅ Model access and relations are fully typed!
    const company = await prisma.companies.findUnique({
      where: { id: 'company-1' },
      include: {
        jobs: true, // Include all job fields
      },
    });

    expect(company?.jobs).toHaveLength(2);
    expect(company?.jobs[0].title).toBe('Job 1');
  });

  it('should load relations with select', async () => {
    const company = await prisma.companies.findUnique({
      where: { id: 'company-1' },
      select: {
        id: true,
        name: true,
        jobs: {
          select: {
            id: true,
            title: true,
          },
        },
      },
    });

    expect(company?.jobs).toHaveLength(2);
    expect(company?.jobs[0]).toHaveProperty('id');
    expect(company?.jobs[0]).toHaveProperty('title');
    expect(company?.jobs[0]).not.toHaveProperty('status'); // Not selected
  });

  it('should load nested relations', async () => {
    const company = await prisma.companies.findUnique({
      where: { id: 'company-1' },
      include: {
        jobs: {
          include: {
            // Nested relation (if jobs has relations)
            // Example: applications: true
          },
        },
      },
    });

    expect(company?.jobs).toHaveLength(2);
  });
});

Relation Filters (some, every, none)

Prismocker supports filtering records based on related records:

it('should filter by relation with some', async () => {
  // ✅ Find companies that have at least one published job - fully typed!
  const companies = await prisma.companies.findMany({
    where: {
      jobs: {
        some: {
          status: 'published',
        },
      },
    },
  });
  // companies is typed as Company[]

  expect(companies).toHaveLength(1);
  expect(companies[0].id).toBe('company-1');
});

it('should filter by relation with every', async () => {
  // ✅ Find companies where ALL jobs are published - fully typed!
  const companies = await prisma.companies.findMany({
    where: {
      jobs: {
        every: {
          status: 'published',
        },
      },
    },
  });
  // companies is typed as Company[]

  // This company has both published and draft jobs, so it won't match
  expect(companies).toHaveLength(0);
});

it('should filter by relation with none', async () => {
  // ✅ Find companies that have NO draft jobs - fully typed!
  const companies = await prisma.companies.findMany({
    where: {
      jobs: {
        none: {
          status: 'draft',
        },
      },
    },
  });
  // companies is typed as Company[]

  // This company has a draft job, so it won't match
  expect(companies).toHaveLength(0);
});

One-to-One Relations

it('should load one-to-one relations', async () => {
  // Seed one-to-one relation data
  (prisma as any).setData('profiles', [
    { id: 'profile-1', company_id: 'company-1', bio: 'Company bio' },
  ]);

  const company = await prisma.companies.findUnique({
    where: { id: 'company-1' },
    include: {
      profile: true,
    },
  });

  expect(company?.profile).toBeDefined();
  expect(company?.profile.bio).toBe('Company bio');
});

Key Features:

  • ✅ Full include support (loads all fields + relations)
  • ✅ Full select support (selective field loading)
  • ✅ Nested include/select in relations
  • ✅ Relation filters: some, every, none
  • ✅ One-to-many and one-to-one relations
  • ✅ Automatic foreign key inference

Prismocker supports full transaction functionality with automatic rollback on errors.

Successful Transactions

import { describe, it, expect, beforeEach } from '@jest/globals';
import { createPrismocker } from '@jsonbored/prismocker';
import type { PrismaClient } from '@prisma/client';
import { isPrismockerClient } from 'prisma/jest-helpers';

describe('Transactions', () => {
  let prisma: PrismaClient;

  beforeEach(() => {
    // ✅ Create Prismocker instance - fully typed!
    prisma = createPrismocker<PrismaClient>();

    // ✅ Type-safe reset
    if (isPrismockerClient(prisma)) {
      prisma.reset(); // ✅ No type assertion needed
    }
  });

  it('should commit transaction on success', async () => {
    // ✅ Model access is fully typed!
    await prisma.companies.create({
      data: { name: 'Company 1', owner_id: 'user-1', slug: 'company-1' },
    });

    // ✅ Transaction callback receives fully typed tx!
    await prisma.$transaction(async (tx) => {
      // tx is typed as ExtractModels<PrismaClient> - full type safety!
      await tx.companies.create({
        data: { name: 'Company 2', owner_id: 'user-2', slug: 'company-2' },
      });
      await tx.companies.create({
        data: { name: 'Company 3', owner_id: 'user-3', slug: 'company-3' },
      });
    });

    // ✅ Verify all changes were committed - fully typed!
    const companies = await prisma.companies.findMany();
    // companies is typed as Company[]
    expect(companies).toHaveLength(3);
  });

  it('should return transaction result', async () => {
    // ✅ Transaction result is fully typed!
    const result = await prisma.$transaction(async (tx) => {
      const company = await tx.companies.create({
        data: { name: 'Company 1', owner_id: 'user-1', slug: 'company-1' },
      });

      const job = await tx.jobs.create({
        data: { company_id: company.id, title: 'Job 1' },
      });

      return { company, job };
    });

    expect(result.company.name).toBe('Company 1');
    expect(result.job.title).toBe('Job 1');
  });
});

Transaction Rollback

Prismocker automatically rolls back all changes if an error occurs:

it('should rollback transaction on error', async () => {
  // ✅ Create initial data - fully typed!
  await prisma.companies.create({
    data: { name: 'Company 1', owner_id: 'user-1', slug: 'company-1' },
  });

  // ✅ Execute transaction that fails - tx is fully typed!
  try {
    await prisma.$transaction(async (tx) => {
      // tx is typed as ExtractModels<PrismaClient> - full type safety!
      await tx.companies.create({
        data: { name: 'Company 2', owner_id: 'user-2', slug: 'company-2' },
      });
      // Force an error
      throw new Error('Transaction failed');
    });
    // Should not reach here
    expect(true).toBe(false);
  } catch (error: any) {
    expect(error.message).toBe('Transaction failed');
  }

  // Verify rollback - only original company should exist
  const companies = await prisma.companies.findMany();
  expect(companies).toHaveLength(1);
  expect(companies[0].name).toBe('Company 1');
});

it('should rollback all changes in transaction on error', async () => {
  // Create initial data
  await prisma.companies.create({
    data: { name: 'Company 1', owner_id: 'user-1', slug: 'company-1' },
  });

  // Execute transaction with multiple operations that fails
  try {
    await prisma.$transaction(async (tx) => {
      await tx.companies.create({
        data: { name: 'Company 2', owner_id: 'user-2', slug: 'company-2' },
      });
      await tx.companies.create({
        data: { name: 'Company 3', owner_id: 'user-3', slug: 'company-3' },
      });
      // Force an error after multiple operations
      throw new Error('Transaction failed');
    });
    // Should not reach here
    expect(true).toBe(false);
  } catch (error: any) {
    expect(error.message).toBe('Transaction failed');
  }

  // Verify rollback - none of the transaction changes should be committed
  const companies = await prisma.companies.findMany();
  expect(companies).toHaveLength(1);
  expect(companies[0].name).toBe('Company 1');
});

it('should rollback updates and deletes in transaction', async () => {
  // Create initial data
  const company1 = await prisma.companies.create({
    data: { name: 'Company 1', owner_id: 'user-1', slug: 'company-1' },
  });
  const company2 = await prisma.companies.create({
    data: { name: 'Company 2', owner_id: 'user-2', slug: 'company-2' },
  });

  // Execute transaction with updates and delete that fails
  try {
    await prisma.$transaction(async (tx) => {
      await tx.companies.update({
        where: { id: company1.id },
        data: { name: 'Updated Company 1' },
      });
      await tx.companies.delete({
        where: { id: company2.id },
      });
      // Force an error
      throw new Error('Transaction failed');
    });
    // Should not reach here
    expect(true).toBe(false);
  } catch (error: any) {
    expect(error.message).toBe('Transaction failed');
  }

  // Verify rollback - original state should be restored
  const companies = await prisma.companies.findMany();
  expect(companies).toHaveLength(2);
  expect(companies.find((c) => c.id === company1.id)?.name).toBe('Company 1');
  expect(companies.find((c) => c.id === company2.id)?.name).toBe('Company 2');
});

Key Features:

  • ✅ Automatic state snapshotting before transaction
  • ✅ Automatic rollback on any error
  • ✅ All-or-nothing atomicity
  • ✅ Supports all operations (create, update, delete)
  • ✅ State restoration preserves data integrity
  • ✅ Works with nested operations and complex scenarios

Test with optional Zod validation from prisma-zod-generator:

import { describe, it, expect, beforeEach } from '@jest/globals';
import { createPrismocker } from '@jsonbored/prismocker';
import type { PrismaClient } from '@prisma/client';

describe('Zod Validation', () => {
  let prisma: PrismaClient;

  beforeEach(() => {
    // Enable Zod validation
    prisma = createPrismocker<PrismaClient>({
      validateWithZod: true,
      zodSchemasPath: '@prisma/zod', // Path to your generated schemas
    });
  });

  it('should validate data against Zod schemas', async () => {
    // This will validate against CompaniesCreateInputSchema if available
    const company = await prisma.companies.create({
      data: {
        name: 'Valid Company',
        owner_id: 'user-1',
        slug: 'valid-company',
      },
    });

    expect(company.name).toBe('Valid Company');
  });

  it('should reject invalid data', async () => {
    await expect(
      prisma.companies.create({
        data: {
          name: '', // Invalid: empty string
          owner_id: 'user-1',
          slug: 'valid-company',
        },
      })
    ).rejects.toThrow('Zod validation failed');
  });
});

🚀 Advanced Features

Prismocker is fully compatible with the Prisma ecosystem:

Generated Zod Schemas

If you use prisma-zod-generator, you can enable optional validation:

import { createPrismocker } from '@jsonbored/prismocker';
import type { PrismaClient } from '@prisma/client';

const prisma = createPrismocker<PrismaClient>({
  validateWithZod: true,
  zodSchemasPath: '@prisma/zod', // Path to your generated schemas
});

// Data will be validated against generated Zod schemas
await prisma.companies.create({
  data: { name: 'Test Company', slug: 'test-company' },
});

PrismaJson Types

Prismocker automatically supports PrismaJson types from prisma-json-types-generator:

import type { PrismaClient } from '@prisma/client';
import type { PrismaJson } from '@prisma/client';

const prisma = createPrismocker<PrismaClient>();

// PrismaJson types work seamlessly
const metadata: PrismaJson.ContentMetadata = {
  dependencies: ['react', 'next'],
};

await prisma.content.create({
  data: {
    title: 'Test',
    metadata, // Fully typed!
  },
});

Prisma Extensions

Prismocker fully supports Prisma Client extensions via $extends():

import { createPrismocker } from '@jsonbored/prismocker';
import type { PrismaClient } from '@prisma/client';

const basePrisma = createPrismocker<PrismaClient>();

// Client extensions (add methods to client)
const extended = basePrisma.$extends({
  client: {
    customMethod: () => 'custom-value',
  },
});

// Model extensions (add methods to models)
const extendedWithModels = basePrisma.$extends({
  model: {
    companies: {
      async findActive() {
        return basePrisma.companies.findMany({ where: { featured: true } });
      },
    },
  },
});

// Query extensions (modify query behavior)
const extendedWithQuery = basePrisma.$extends({
  model: {
    companies: {
      query: {
        findMany: async (args, originalMethod) => {
          // Modify args or call originalMethod
          return originalMethod({ ...args, where: { ...args.where, active: true } });
        },
      },
    },
  },
});

// Result extensions (modify result behavior)
const extendedWithResult = basePrisma.$extends({
  model: {
    companies: {
      result: {
        findMany: (result) => {
          // Transform result
          return result.map((company) => ({ ...company, transformed: true }));
        },
      },
    },
  },
});

// Chaining extensions
const chained = basePrisma
  .$extends({ client: { method1: () => 'value1' } })
  .$extends({ client: { method2: () => 'value2' } });

Extension Features:

  • ✅ Client extensions (add methods to client)
  • ✅ Model extensions (add methods to models)
  • ✅ Query extensions (modify query behavior)
  • ✅ Result extensions (modify result behavior)
  • ✅ Extension chaining (multiple $extends() calls)
  • ✅ Full compatibility with Prisma's extension API

Prismocker supports Prisma's lifecycle methods and middleware for complete API compatibility:

Connection Management ($connect / $disconnect)

Prismocker provides no-op implementations of $connect() and $disconnect() for API compatibility:

// Connect (no-op for in-memory mocking)
await prisma.$connect();

// Disconnect (no-op for in-memory mocking)
await prisma.$disconnect();

Event Emission:

  • $connect() emits a connect event
  • $disconnect() emits a disconnect event

Middleware Support ($use)

Prismocker supports Prisma middleware via $use():

// Register middleware
prisma.$use(async (params, next) => {
  console.log(`Executing ${params.model}.${params.action}`);
  return next(params);
});

// Middleware executes before all operations
await prisma.companies.findMany(); // Logs: "Executing companies.findMany"
await prisma.companies.create({ data: { name: 'Test' } }); // Logs: "Executing companies.create"

Middleware Features:

  • ✅ Executes before all operations (findMany, create, update, delete, etc.)
  • ✅ Can modify operation parameters
  • ✅ Can intercept and return custom results
  • ✅ Supports multiple middleware (executed in registration order)
  • ✅ Works with all Prisma operations
  • ✅ Correctly sets runInTransaction: true when operations run inside transactions

Example: Logging Middleware

prisma.$use(async (params, next) => {
  const start = Date.now();
  const result = await next(params);
  const duration = Date.now() - start;
  console.log(`${params.model}.${params.action} took ${duration}ms`);
  return result;
});

Example: Parameter Modification

prisma.$use(async (params, next) => {
  // Add default filter to all findMany operations
  if (params.action === 'findMany' && !params.args?.where) {
    params.args = { ...params.args, where: { active: true } };
  }
  return next(params);
});

Example: Result Interception

prisma.$use(async (params, next) => {
  // Intercept and return custom result for specific operations
  if (params.model === 'companies' && params.action === 'findMany') {
    return [{ id: 'custom-1', name: 'Custom Company' }];
  }
  return next(params);
});

Event Listeners ($on)

Prismocker supports Prisma event listeners via $on():

// Register query event listener
prisma.$on('query', (event) => {
  console.log('Query executed:', event.model, event.action);
});

// Register multiple listeners
prisma.$on('query', (event) => console.log('Listener 1:', event));
prisma.$on('query', (event) => console.log('Listener 2:', event));

// Operations emit query events
await prisma.companies.findMany(); // Both listeners fire

Supported Event Types:

  • query - Emitted for all database operations
  • connect - Emitted when $connect() is called
  • disconnect - Emitted when $disconnect() is called
  • info - For informational messages (not emitted by default)
  • warn - For warnings (not emitted by default)
  • error - For errors (not emitted by default)

Event Data Structure:

prisma.$on('query', (event) => {
  // event.model - Model name (e.g., 'companies')
  // event.action - Operation name (e.g., 'findMany', 'create')
  // event.args - Operation arguments
});

Metrics API ($metrics)

Prismocker provides a stub implementation of Prisma's metrics API (Prisma 7.1.0+):

const metrics = await prisma.$metrics();

// Returns metrics structure matching Prisma's API
console.log(metrics.counters); // Query count metrics
console.log(metrics.gauges); // Active query metrics
console.log(metrics.histograms); // Query duration histograms

Metrics Structure:

{
  counters: [
    {
      key: 'prisma_client_queries_total',
      value: 10, // Total queries executed
      labels: {},
    },
  ],
  gauges: [
    {
      key: 'prisma_client_queries_active',
      value: 0, // Active queries (always 0 for in-memory)
      labels: {},
    },
  ],
  histograms: [
    {
      key: 'prisma_client_queries_duration_histogram_ms',
      value: [1, 2, 5, 10, 50], // Query durations in milliseconds
      labels: {},
      buckets: [1, 5, 10, 50, 100, 500, 1000, 5000],
    },
  ],
  // In debug mode, includes detailed query statistics
  queryStats?: {
    totalQueries: 10,
    queriesByModel: { companies: 5, jobs: 5 },
    queriesByOperation: { findMany: 3, create: 2 },
    averageDuration: 2.5,
  },
}

Integration with Debug Mode: When debug mode is enabled (prisma.enableDebugMode()), metrics include detailed query statistics:

prisma.enableDebugMode();
await prisma.companies.findMany();
await prisma.companies.create({ data: { name: 'Test' } });

const metrics = await prisma.$metrics();
console.log(metrics.queryStats); // Detailed statistics available

Use Cases:

  • ✅ Testing metrics collection in your application
  • ✅ Verifying query performance in tests
  • ✅ Monitoring query patterns during testing
  • ✅ Integration with monitoring/observability tools

Prismocker provides full type safety through a type-preserving Proxy system that eliminates the need for as any type assertions.

How It Works

Prismocker uses ExtractModels<T> to preserve all model types from your PrismaClient:

import { createPrismocker } from '@jsonbored/prismocker';
import type { PrismaClient } from '@prisma/client';
import type { ExtractModels } from 'prisma/prisma-types';

// ✅ Returns ExtractModels<PrismaClient> - full type preservation!
const prisma = createPrismocker<PrismaClient>();

// ✅ prisma.companies is fully typed as PrismaClient['companies']
const companies = await prisma.companies.findMany();
// companies is typed as Company[] (from your Prisma schema)

// ✅ All Prisma operations are fully typed
const company = await prisma.companies.findUnique({
  where: { id: 'company-1' },
});
// company is typed as Company | null

await prisma.companies.create({
  data: {
    name: 'Company 1',
    owner_id: 'user-1',
    slug: 'company-1',
  },
});
// ✅ TypeScript will error if fields don't match your schema

// ✅ Prismocker methods are also fully typed
prisma.reset();
prisma.setData('companies', []);
const data = prisma.getData('companies');

Type Preservation with ExtractModels

The ExtractModels<T> type utility:

  1. Preserves Model Types - Maps all Prisma.ModelName values to their corresponding model delegate types
  2. Preserves Prisma Methods - Maintains types for $queryRaw, $transaction, $connect, etc.
  3. Adds Prismocker Methods - Includes reset, setData, getData, enableDebugMode, etc.

Example:

import type { ExtractModels } from 'prisma/prisma-types';

// ExtractModels<PrismaClient> preserves:
// - prisma.companies → PrismaClient['companies'] (fully typed)
// - prisma.jobs → PrismaClient['jobs'] (fully typed)
// - prisma.$transaction → PrismaClient['$transaction'] (fully typed)
// - prisma.reset() → void (Prismocker method)
// - prisma.setData() → void (Prismocker method)

Before vs After

Before (with type assertions):

const prisma = createPrismocker<PrismaClient>();

// ❌ Requires type assertions for model access
const companies = await (prisma as any).companies.findMany();
(prisma as any).reset();
(prisma as any).setData('companies', []);

After (fully type-safe):

const prisma = createPrismocker<PrismaClient>();

// ✅ Fully typed - no assertions needed!
const companies = await prisma.companies.findMany();
// companies is typed as Company[]

prisma.reset(); // ✅ Fully typed
prisma.setData('companies', []); // ✅ Fully typed

Transaction Type Safety

Transaction callbacks also receive fully typed transaction clients:

await prisma.$transaction(async (tx) => {
  // ✅ tx is typed as ExtractModels<PrismaClient>
  // ✅ All model access is fully typed
  const companies = await tx.companies.findMany();
  await tx.companies.create({
    data: { name: 'New Company', owner_id: 'user-1', slug: 'new-company' },
  });
  // ✅ Full type checking - TypeScript will error if fields don't match
});

Dynamic Models (Not in Schema)

For dynamic models that don't exist in your Prisma schema (e.g., test-only models), you may still need as any:

// If 'users' doesn't exist in your Prisma schema:
const users = await (prisma as any).users.findMany();
// TypeScript can't infer types for models not in Prisma.ModelName

Note: This is expected behavior - TypeScript can only provide type safety for models that exist in your Prisma schema. For models in your schema, full type safety is guaranteed without any assertions.

Type Helpers

Prismocker provides additional type helpers for advanced use cases:

import type { ExtractModels, ModelName, ModelType } from 'prisma/prisma-types';

// ExtractModels<T> - Preserves all model types
type PrismockerClient = ExtractModels<PrismaClient>;

// ModelName<T> - Extract model name type
type CompanyModelName = ModelName<'companies'>; // 'companies'

// ModelType<TClient, TModel> - Extract model delegate type
type CompanyModel = ModelType<PrismaClient, 'companies'>;
// CompanyModel is the type of prisma.companies

Module Augmentation

Prismocker uses TypeScript module augmentation to add Prismocker-specific methods to PrismaClient:

// types-augmentation.d.ts automatically extends PrismaClient
declare module '@prisma/client' {
  interface PrismaClient {
    reset(): void;
    setData<T = any>(modelName: string, data: T[]): void;
    getData<T = any>(modelName: string): T[];
    enableDebugMode(enabled?: boolean): void;
    getQueryStats(): QueryStats;
    visualizeState(options?: VisualizationOptions): string;
  }
}

This means all Prismocker methods are available on any PrismaClient instance when using Prismocker, with full type safety.

Prismocker provides convenient test utilities:

import { createTestPrisma, resetAndSeed, createTestDataFactory } from 'prisma/test-utils';

const prisma = createTestPrisma();

// Create data factory for consistent test data
const companyFactory = createTestDataFactory({
  name: 'Test Company',
  owner_id: 'test-user',
  slug: 'test-company',
});

beforeEach(() => {
  // Reset and seed in one call
  resetAndSeed(prisma, {
    companies: [companyFactory({ name: 'Company 1' }), companyFactory({ name: 'Company 2' })],
  });
});

Prismocker includes an automatic index manager that optimizes query performance by maintaining indexes for:

  • Primary keys (id fields) - For fast findUnique lookups
  • Foreign keys (fields ending in _id) - For fast relation loading
  • All fields - Automatically indexed for fast filtering

Automatic Indexing:

Indexes are enabled by default and automatically maintained:

import { createPrismocker } from '@jsonbored/prismocker';
import type { PrismaClient } from '@prisma/client';

// Indexes are enabled by default
const prisma = createPrismocker<PrismaClient>();

// findUnique with id uses index (O(1) lookup instead of O(n) scan)
const company = await prisma.companies.findUnique({
  where: { id: 'company-1' },
});

Disabling Indexes:

If you don't need performance optimization, you can disable indexes:

const prisma = createPrismocker<PrismaClient>({
  enableIndexes: false, // Disable indexes
});

Performance Benefits:

  • findUnique with indexed fields: O(1) lookup instead of O(n) scan
  • Relation loading: Fast foreign key lookups
  • Large datasets: Significant performance improvement with 100+ records

Note: Indexes are automatically maintained when data changes (create, update, delete, setData). No manual index management required.

Prismocker can cache query results to improve performance for repeated queries:

import { createPrismocker } from '@jsonbored/prismocker';
import type { PrismaClient } from '@prisma/client';

// Enable query caching
const prisma = createPrismocker<PrismaClient>({
  enableQueryCache: true,
  queryCacheMaxSize: 100, // Maximum cache entries (default: 100)
  queryCacheTTL: 0, // Time to live in ms (0 = no expiration, default: 0)
});

// First call - executes query and caches result
const companies1 = await prisma.companies.findMany();

// Second call - uses cached result (same query args)
const companies2 = await prisma.companies.findMany();
// companies2 === companies1 (same reference, instant return)

// Cache is automatically invalidated when data changes
await prisma.companies.create({ data: { name: 'New Company', ... } });
// Next findMany() will execute fresh query (cache invalidated)

Cache Invalidation:

The cache is automatically invalidated when:

  • Records are created (create, createMany)
  • Records are updated (update, updateMany)
  • Records are deleted (delete, deleteMany)
  • Data is set via setData()
  • Client is reset via reset()

Configuration:

  • enableQueryCache: Enable/disable query caching (default: false)
  • queryCacheMaxSize: Maximum number of cache entries (default: 100)
  • queryCacheTTL: Time to live in milliseconds (default: 0 = no expiration)

Prismocker can load relations lazily (on-demand) instead of eagerly:

import { createPrismocker } from '@jsonbored/prismocker';
import type { PrismaClient } from '@prisma/client';

// Enable lazy relation loading
const prisma = createPrismocker<PrismaClient>({
  enableLazyRelations: true,
});

// Query with include - relation is a Proxy that loads on first access
const company = await prisma.companies.findUnique({
  where: { id: 'company-1' },
  include: { jobs: true },
});

// Relation is not loaded yet (Proxy object)
console.log(company.jobs); // Proxy object

// Access relation - loads on first access
const jobs = company.jobs; // Now loads the actual data
console.log(jobs.length); // 5
console.log(jobs[0].title); // "Job 1"

// Works with both include and select
const company2 = await prisma.companies.findUnique({
  where: { id: 'company-2' },
  select: { id: true, name: true, jobs: true },
});

// Access relation - loads on first access
const jobs2 = company2.jobs; // Loads data

Benefits:

  • Memory Efficiency: Relations are only loaded when accessed
  • Performance: Faster initial queries (no eager loading overhead)
  • Flexibility