npm package: @jsonbored/prismocker
Description: A type-safe, in-memory Prisma Client mock for testing. Works perfectly with pnpm and supports all Prisma operations.
Author: JSONbored
Latest version: 0.1.0

<div align="center">

# 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](https://img.shields.io/npm/v/@jsonbored/prismocker?style=flat-square)](https://www.npmjs.com/package/@jsonbored/prismocker)
[![npm downloads](https://img.shields.io/npm/dm/@jsonbored/prismocker?style=flat-square)](https://www.npmjs.com/package/@jsonbored/prismocker)
[![License](https://img.shields.io/npm/l/@jsonbored/prismocker?style=flat-square)](https://github.com/JSONbored/prismocker/blob/main/LICENSE)

**Status**
[![CI](https://github.com/JSONbored/prismocker/workflows/CI/badge.svg?style=flat-square)](https://github.com/JSONbored/prismocker/actions)
[![TypeScript](https://img.shields.io/badge/TypeScript-007ACC?style=flat-square&logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
[![Node](https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen?style=flat-square&logo=node.js)](https://nodejs.org/)

</div>

## 📑 Table of Contents

* [✨ Features](#-features)
* [🆚 Why Prismocker?](#-why-prismocker)
* [🚀 Quick Start](#-quick-start)
* [📦 Installation](#-installation)
* [⚡ Auto-Setup (Recommended)](#-auto-setup-recommended)
* [📚 Quick Start Guide](#-quick-start-guide)
  * [Basic Usage](#basic-usage)
  * [Jest Integration](#jest-integration)
  * [Vitest Integration](#vitest-integration)
* [📖 API Reference](#-api-reference)
  * [Factory Functions](#factory-functions)
  * [Type-Safe Helpers](#type-safe-helpers)
  * [Configuration Options](#configuration-options)
* [💡 Usage Examples](#-usage-examples)
  * [Service Layer Testing](#service-layer-testing)
  * [API Route Testing](#api-route-testing)
  * [Complex Query Testing](#complex-query-testing)
  * [Relation Testing](#relation-testing)
  * [Transaction Testing](#transaction-testing)
  * [Zod Validation Testing](#zod-validation-testing)
* [🚀 Advanced Features](#-advanced-features)
  * [Prisma Ecosystem Compatibility](#prisma-ecosystem-compatibility)
  * [Type Safety](#type-safety)
  * [Test Utilities](#test-utilities)
  * [Query Logging](#query-logging)
* [⚙️ How It Works](#️-how-it-works)
  * [In-Memory Storage](#in-memory-storage)
  * [Query Engine](#query-engine)
  * [Type System](#type-system)
* [📁 Example Files](#-example-files)
* [⚠️ Caveats & Considerations](#️-caveats--considerations)
* [🔧 Troubleshooting](#-troubleshooting)
* [🔄 Migration Guide](#-migration-guide)
* [🛠️ CLI Commands](#️-cli-commands)
* [🤝 Contributing](#-contributing)
* [🔗 Related Projects](#-related-projects)

## ✨ 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<T>) | 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

```typescript
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' }
});
```

<details>
<summary>📖 View full installation and setup guide</summary>

## 📦 Installation

```bash
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:

```bash
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:**

```bash
# 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.

</details>

## 📚 Quick Start Guide

<details>
<summary><strong>Basic Usage</strong></summary>

```typescript
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

</details>

<details>
<summary><strong>Jest Integration</strong></summary>

> **💡 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:

```typescript
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

```typescript
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!

</details>

<details>
<summary><strong>Vitest Integration</strong></summary>

> **💡 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):

```typescript
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`:

```typescript
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

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

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

</details>

## 📖 API Reference

### Factory Functions

<details>
<summary><strong>createPrismocker&lt;T&gt;(options?)</strong></summary>

Creates a new PrismockerClient instance that implements the PrismaClient interface.

```typescript
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:**

```typescript
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', []);
```

</details>

<details>
<summary><strong>createTestPrisma()</strong></summary>

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

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

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

</details>

### Type-Safe Helpers

<details>
<summary><strong>Jest Helpers</strong></summary>

Type-safe utilities for Jest testing:

```typescript
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`

</details>

<details>
<summary><strong>Test Utilities</strong></summary>

Convenient helpers for test setup and data management:

```typescript
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

</details>

<details>
<summary><strong>Prisma Type Helpers</strong></summary>

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

### ExtractModels<T> - Type Preservation

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

```typescript
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

```typescript
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:

```typescript
// ✅ 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');
```

</details>

### Configuration Options

<details>
<summary><strong>PrismockerOptions</strong></summary>

```typescript
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;
}
```

</details>

## 💡 Usage Examples

<details>
<summary><strong>Service Layer Testing</strong></summary>

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

```typescript
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);
  });
});
```

</details>

<details>
<summary><strong>API Route Testing</strong></summary>

Test Next.js API routes with Prismocker:

```typescript
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);
  });
});
```

</details>

<details>
<summary><strong>Complex Query Testing</strong></summary>

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

```typescript
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');
  });
});
```

</details>

<details>
<summary><strong>Aggregation Testing</strong></summary>

Prismocker supports comprehensive aggregation operations including statistical functions:

```typescript
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

</details>

<details>
<summary><strong>findUniqueOrThrow and findFirstOrThrow</strong></summary>

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:

```typescript
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:

```typescript
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

</details>

<details>
<summary><strong>Relation Testing</strong></summary>

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

### Basic Relation Loading

```typescript
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:

```typescript
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

```typescript
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

</details>

<details>
<summary><strong>Transaction Testing</strong></summary>

Prismocker supports full transaction functionality with automatic rollback on errors.

### Successful Transactions

```typescript
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:

```typescript
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

</details>

<details>
<summary><strong>Zod Validation Testing</strong></summary>

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

```typescript
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');
  });
});
```

</details>

## 🚀 Advanced Features

<details>
<summary><strong>Prisma Ecosystem Compatibility</strong></summary>

Prismocker is fully compatible with the Prisma ecosystem:

### Generated Zod Schemas

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

```typescript
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`:

```typescript
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()`:

```typescript
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

</details>

<details>
<summary><strong>Prisma Lifecycle & Middleware Support</strong></summary>

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:

```typescript
// 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()`:

```typescript
// 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**

```typescript
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**

```typescript
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**

```typescript
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()`:

```typescript
// 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:**

```typescript
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+):

```typescript
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:**

```typescript
{
  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:

```typescript
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

</details>

<details>
<summary><strong>Type Safety</strong></summary>

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`:

```typescript
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<T>

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:**

```typescript
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):**

```typescript
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):**

```typescript
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:

```typescript
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`:

```typescript
// 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:

```typescript
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`:

```typescript
// 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.

</details>

<details>
<summary><strong>Test Utilities</strong></summary>

Prismocker provides convenient test utilities:

```typescript
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' })],
  });
});
```

</details>

<details>
<summary><strong>Performance Optimization (Index Manager)</strong></summary>

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:

```typescript
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:

```typescript
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.

</details>

<details>
<summary><strong>Query Result Caching</strong></summary>

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

```typescript
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)

</details>

<details>
<summary><strong>Lazy Relation Loading</strong></summary>

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

```typescript
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**