@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
Maintainers
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.
📑 Table of Contents
- ✨ Features
- 🆚 Why Prismocker?
- 🚀 Quick Start
- 📦 Installation
- ⚡ Auto-Setup (Recommended)
- 📚 Quick Start Guide
- 📖 API Reference
- 💡 Usage Examples
- 🚀 Advanced Features
- ⚙️ How It Works
- 📁 Example Files
- ⚠️ Caveats & Considerations
- 🔧 Troubleshooting
- 🔄 Migration Guide
- 🛠️ CLI Commands
- 🤝 Contributing
- 🔗 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 anyassertions - ✅ Full Prisma API - Supports all Prisma operations (findMany, create, update, delete, count, aggregate, groupBy, etc.)
- ✅ Full Relation Support - Complete
include/selectsupport 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/clientas 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/prismockerPeer 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 setupThis command will:
- ✅ Detect your testing framework (Jest or Vitest)
- ✅ Create the
__mocks__/@prisma/client.tsfile - ✅ Update your test setup files (
jest.setup.tsorvitest.setup.ts) - ✅ Generate enum stubs from your Prisma schema
- ✅ 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-examplesAfter 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 anyoras unknownassertions - ✅ 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 setupto 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 setupto 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 extendPrismaClient, defaults toPrismaClient)
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.companiesis fully typed) - ✅ Preserves all Prisma methods (
$queryRaw,$transaction, etc.) - ✅ Adds Prismocker-specific methods (
reset,setData,getData, etc.) - ✅ Eliminates the need for
as anyassertions 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 PrismockerClientcreateMockQueryRawUnsafe(prisma: PrismaClient): MockQueryRawUnsafe- Type-safe mock for$queryRawUnsafecreateMockQueryRaw(prisma: PrismaClient): MockQueryRaw- Type-safe mock for$queryRawcreateMockTransaction(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 instanceresetAndSeed(prisma: PrismaClient, data: Record<string, any[]>): void- Reset and seed datacreateTestDataFactory<T>(defaults: Partial<T>): (overrides?: Partial<T>) => T- Create data factorysnapshotPrismocker(prisma: PrismaClient, modelNames?: string[]): Record<string, any[]>- Snapshot current staterestorePrismocker(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 fromPrismaClientModelName<T>- Extract model name type fromPrisma.ModelNameModelType<TClient, TModel>- Extract model delegate type fromPrismaClientsetDataTyped<TClient>(prisma: TClient, model: string, data: any[]): void- Type-safe data seedinggetDataTyped<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:
_stddevand_variancereturn0 - No values:
_stddevand_variancereturnnull,_countDistinctreturns0 - All operations support
whereclause 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
includeandselect(same asfindUnique/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
includesupport (loads all fields + relations) - ✅ Full
selectsupport (selective field loading) - ✅ Nested
include/selectin 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 aconnectevent$disconnect()emits adisconnectevent
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: truewhen 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 fireSupported 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 histogramsMetrics 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 availableUse 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:
- Preserves Model Types - Maps all
Prisma.ModelNamevalues to their corresponding model delegate types - Preserves Prisma Methods - Maintains types for
$queryRaw,$transaction,$connect, etc. - 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 typedTransaction 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.ModelNameNote: 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.companiesModule 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 (
idfields) - For fastfindUniquelookups - 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 dataBenefits:
- Memory Efficiency: Relations are only loaded when accessed
- Performance: Faster initial queries (no eager loading overhead)
- Flexibility
