Transform Prisma into a powerful Active Record ORM with advanced querying, batch operations, and graph utilities
A complete TypeScript framework that extends Prisma Client with Active Record pattern, declarative query builder, relation graph traversal, and batch operations.
Prisma documentation states: "Prisma is not a traditional ORM". It's a query builder focused on type safety and developer experience.
We believe both approaches have value. This framework brings traditional ORM ergonomics to Prisma while maintaining its type safety and performance benefits.
This isn't just an ORM wrapper. It's a complete framework with:
- ποΈ Entity System - Active Record with lifecycle management
- π Query DSL - Declarative search with LIKE, ranges, lists, OR/AND operators
- πΈοΈ Graph Utilities - Dependency trees, path finding, automatic include builders
- π Smart Relations - Automatic connect/create processing
- β‘ Batch Engine - SQL-optimized bulk operations (up to 1500 records per batch)
- π Pagination - Built-in formatted responses
npm install prisma-entity-framework
# or
yarn add prisma-entity-framework
# or
pnpm add prisma-entity-frameworkRequirements:
- Node.js >= 16
- Prisma Client >= 4.0.0
Supported Databases:
- β SQLite
- β MySQL
- β PostgreSQL
- β SQL Server
- β MongoDB
import { PrismaClient } from '@prisma/client';
import { configurePrisma } from 'prisma-entity-framework';
const prisma = new PrismaClient();
configurePrisma(prisma); // One-time setupOption A: Using @Property() Decorator (Recommended)
import { BaseEntity, Property } from 'prisma-entity-framework';
import { User as PrismaUser } from '@prisma/client';
import { prisma } from './prisma-client';
export class User extends BaseEntity<PrismaUser> {
static readonly model = prisma.user;
@Property() declare name: string;
@Property() declare email: string;
@Property() declare age?: number;
@Property() declare isActive: boolean;
}Option B: Traditional Getters/Setters (For Validation)
export class User extends BaseEntity<PrismaUser> {
static readonly model = prisma.user;
private _email!: string;
get email() { return this._email; }
set email(value: string) {
if (!value.includes('@')) throw new Error('Invalid email');
this._email = value.toLowerCase(); // Normalize
}
}π‘ Tip: Use
@Property()for simple properties. Use traditional getters/setters when you need validation or transformation logic.
// Create
const user = new User({ name: "John", email: "john@example.com" });
await user.create();
// Update
user.name = "Jane";
await user.update();
// Delete
await user.delete();Build complex queries declaratively without writing raw Prisma syntax:
const results = await User.findByFilter({
isActive: true // Base filter
}, {
search: {
// String search with LIKE, STARTS_WITH, ENDS_WITH, EXACT
stringSearch: [{
keys: ['name', 'email'],
value: 'john',
mode: 'LIKE',
grouping: 'or' // OR across fields
}],
// Range search for numbers and dates
rangeSearch: [{
keys: ['age'],
min: 18,
max: 65
}],
// List search with IN, NOT_IN, HAS_SOME, HAS_EVERY
listSearch: [{
keys: ['status'],
values: ['active', 'pending'],
mode: 'IN'
}],
grouping: 'and' // AND across search types
},
// Pagination
pagination: {
page: 1,
pageSize: 10,
take: 10,
skip: 0
},
// Sorting
orderBy: { createdAt: 'desc' },
// Include relations
relationsToInclude: [
{ posts: [{ comments: '*' }] }
]
});Search Modes:
LIKE- Contains substring (case-insensitive)STARTS_WITH- Begins with valueENDS_WITH- Ends with valueEXACT- Exact match
List Modes:
IN- Value in listNOT_IN- Value not in listHAS_SOME- Array has some valuesHAS_EVERY- Array has all values
Get formatted pagination responses automatically:
const result = await User.findByFilter({}, {
pagination: {
page: 2,
pageSize: 20,
take: 20,
skip: 20
}
});
console.log(result);
// {
// total: 142,
// page: 2,
// pageSize: 20,
// data: [... 20 users ...]
// }Automatically processes relations and detects JSON fields:
// Nested relations with wildcard
const users = await User.findByFilter({}, {
relationsToInclude: [
{
posts: [
{ comments: [{ author: '*' }] }
]
}
]
});
// Automatic connect/create processing
const post = new Post({
title: "My Post",
author: { id: 1 }, // Automatically converts to { connect: { id: 1 } }
metadata: { tags: ['tech'] } // JSON fields preserved as-is
});
await post.create();Optimized bulk operations with database-specific batch sizes:
// Batch create (auto-batched by database type)
const count = await User.createMany([
{ name: "User 1", email: "user1@example.com" },
{ name: "User 2", email: "user2@example.com" },
]);
// Batch update with SQL optimization
const updated = await User.updateManyById([
{ id: 1, name: "Updated 1" },
{ id: 2, name: "Updated 2" },
{ id: 3, name: "Updated 3" }
]);
// Generates optimized CASE WHEN SQL
// Batch delete
const deleted = await User.deleteByFilter({
isActive: false
});Smart create/update with automatic change detection:
// Single upsert
const user = await User.upsert({
email: "john@example.com", // Unique field
name: "John Doe",
age: 30
});
// Creates if not exists, updates only if changed
// Batch upsert with statistics
const result = await User.upsertMany([
{ email: "user1@example.com", name: "User 1" },
{ email: "user2@example.com", name: "User 2" },
{ email: "user3@example.com", name: "User 3" }
]);
console.log(result);
// {
// created: 2, // New records
// updated: 1, // Changed records
// unchanged: 0, // No changes detected
// total: 3
// }Navigate and analyze your data model relationships:
import { ModelUtils } from 'prisma-entity-framework';
// Find path between models
const path = ModelUtils.findPathToParentModel('Comment', 'User');
// β "post.author"
// Build dependency tree
const deps = ModelUtils.getModelDependencyTree(['User', 'Post', 'Comment']);
// β [
// { name: 'User', dependencies: [] },
// { name: 'Post', dependencies: ['User'] },
// { name: 'Comment', dependencies: ['Post', 'User'] }
// ]
// Sort by dependencies (topological sort)
const sorted = ModelUtils.sortModelsByDependencies(deps);
// β ['User', 'Post', 'Comment']
// Auto-generate includes from relation graph
const includes = await ModelUtils.getIncludesTree('User', [
{ posts: [{ comments: [{ author: '*' }] }] }
]);Configure the Prisma client instance. Call once at startup.
import { PrismaClient } from '@prisma/client';
import { configurePrisma } from 'prisma-entity-framework';
const prisma = new PrismaClient();
configurePrisma(prisma);Get the configured Prisma instance.
Check if Prisma has been configured.
Reset configuration (useful for testing).
Advanced query with filters, search, pagination, and relations.
Parameters:
filter- Base Prisma where clauseoptions.search- Search configuration (string, range, list)options.pagination- Pagination settingsoptions.relationsToInclude- Relations to includeoptions.orderBy- Sort configurationoptions.onlyOne- Return single result
Returns: Array of entities or paginated response
const users = await User.findByFilter(
{ isActive: true },
{
search: {
stringSearch: [{ keys: ['name'], value: 'john', mode: 'LIKE' }]
},
pagination: { page: 1, pageSize: 10 },
orderBy: { createdAt: 'desc' }
}
);Count records matching filter.
const count = await User.countByFilter({ isActive: true });Bulk create with automatic batching and retry logic.
const count = await User.createMany([
{ name: "User 1", email: "user1@example.com" },
{ name: "User 2", email: "user2@example.com" }
], true); // skipDuplicatesBulk update by ID with SQL optimization (CASE WHEN) or transactions (MongoDB).
const updated = await User.updateManyById([
{ id: 1, name: "Updated 1" },
{ id: 2, name: "Updated 2" }
]);Create or update based on unique constraints. Only updates if changes detected.
const user = await User.upsert({
email: "john@example.com", // Unique field
name: "John Doe"
});Batch upsert with statistics.
const result = await User.upsertMany([...]);
// { created: 2, updated: 1, unchanged: 0, total: 3 }Delete records matching filter.
const deleted = await User.deleteByFilter({ isActive: false });Delete records by ID array.
const deleted = await User.deleteByIds([1, 2, 3]);Create the entity in database.
const user = new User({ name: "John", email: "john@example.com" });
await user.create();Update the entity in database.
user.name = "Jane";
await user.update();Delete the entity from database.
await user.delete();Convert entity to plain object.
const obj = user.toObject();Convert entity to JSON string.
const json = user.toJson();Utilities for analyzing and traversing your Prisma data model.
Get dependency relationships between models.
Topological sort of models by dependencies.
Find relation path between two models.
Generate Prisma include object from relation graph.
Get unique field combinations for a model.
Utilities for processing relational data.
Transform nested objects into Prisma connect/create structures. Preserves JSON fields.
Convert relation objects to foreign key fields.
Build complex search queries declaratively.
import { SearchBuilder, SearchUtils } from 'prisma-entity-framework';
const builder = new SearchBuilder(modelInfo);
const filters = builder.build(searchOptions);Get current database provider (sqlite, mysql, postgresql, sqlserver, mongodb).
Get database dialect for SQL generation.
Quote identifier for SQL queries.
Format boolean for database.
const products = await Product.findByFilter({
// Base filter
categoryId: { in: [1, 2, 3] },
isActive: true
}, {
search: {
// Text search across multiple fields
stringSearch: [
{
keys: ['name', 'description'],
value: 'laptop',
mode: 'LIKE',
grouping: 'or'
}
],
// Price range
rangeSearch: [
{
keys: ['price'],
min: 500,
max: 2000
}
],
// Stock status
listSearch: [
{
keys: ['status'],
values: ['in_stock', 'low_stock'],
mode: 'IN'
}
],
grouping: 'and' // AND between search types
},
// Pagination
pagination: {
page: 1,
pageSize: 20
},
// Sort by relevance
orderBy: {
price: 'asc',
name: 'asc'
},
// Include relations
relationsToInclude: [
'category',
{ reviews: ['user'] }
]
});import {
createOptimalBatches,
createBatchMetrics,
withRetry
} from 'prisma-entity-framework';
const metrics = createBatchMetrics();
const batches = createOptimalBatches(largeDataset, 'createMany');
for (const batch of batches) {
const startTime = Date.now();
await withRetry(
() => User.createMany(batch),
{ maxRetries: 3, initialDelayMs: 100 }
);
metrics.recordBatch(batch.length, Date.now() - startTime);
}
console.log('Performance:', metrics.getStats());
// {
// totalBatches: 10,
// totalItems: 10000,
// totalTime: 45000,
// avgBatchSize: 1000,
// avgBatchTime: 4500,
// itemsPerSecond: 222
// }| Feature | Prisma Client | Prisma Entity Framework |
|---|---|---|
| Active Record | β No | β
user.create(), user.update() |
| Instance Methods | β No | β Full lifecycle methods |
| Query DSL | Basic where | β LIKE, ranges, lists, OR/AND |
| Batch Optimization | Basic | β Database-specific, SQL-optimized |
| Upsert | Manual | β Automatic with change detection |
| Graph Traversal | Manual | β Automatic path finding |
| Performance Tools | β No | β Metrics, retry, memory estimation |
| JSON Field Detection | Manual | β Automatic |
| Pagination | Manual | β Built-in formatted responses |
| Type Safety | β Full | β Full (maintains Prisma types) |
Execute batch operations in parallel for 2-6x performance improvements. Auto-detects your connection pool and runs operations concurrently.
import { configurePrisma } from 'prisma-entity-framework';
// Setup (auto-detects pool size from DATABASE_URL)
configurePrisma(prisma);
// Operations now run in parallel automatically!
await User.createMany(users); // 2-4x faster
await User.upsertMany(users); // 3-6x faster
await User.updateManyById(updates); // 2-4x faster
await User.deleteByIds(ids); // 2-4x faster// Global configuration
configurePrisma(prisma, {
maxConcurrency: 8, // Max concurrent operations (default: auto-detect)
maxQueriesPerSecond: 100 // Rate limiting (default: 100)
});
// Per-operation override
await User.createMany(users, false, undefined, {
parallel: true,
concurrency: 4
});Benchmarks on 10,000 records:
| Database | Operation | Sequential | Parallel | Speedup |
|---|---|---|---|---|
| PostgreSQL | Create | 947ms | 388ms | 2.4x β‘ |
| PostgreSQL | Delete | 209ms | 66ms | 3.2x π |
| MySQL | Delete | 1,035ms | 218ms | 4.8x π₯ |
| SQLite | Create | 400ms | - | Auto-sequential |
- β Zero Config - Auto-detects pool size from DATABASE_URL
- β Database-Aware - Adapts to PostgreSQL, MySQL, SQLite, MongoDB, SQL Server
- β Rate Limiting - Prevents database overload with token bucket algorithm
- β Backward Compatible - Works with existing code
π Complete Guide - Configuration, benchmarks, best practices, and troubleshooting
# Run all tests (SQLite)
npm test
# Test specific database
npm run test:sqlite
npm run test:mysql
npm run test:postgresql
npm run test:mongodb
# Test all databases
npm run test:all-databases
# Coverage report
npm run test:coverageContributions are welcome! Please feel free to submit a Pull Request.
# Install dependencies
npm install
# Start databases
npm run docker:up
# Run tests
npm test
# Run tests on all databases
npm run test:all-databasesFound a bug or have a feature request? Please open an issue on GitHub Issues.
MIT Β© 2025 Hector Arrechea & Eduardo Estrada
Built on top of the amazing Prisma project.
Made with β€οΈ for the Prisma community
β If you find this framework useful, please star the repository!