prisma-flare 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/cli/db-create.cjs +240 -0
  2. package/dist/cli/db-create.d.cts +1 -0
  3. package/dist/cli/db-create.d.ts +1 -0
  4. package/dist/cli/db-create.js +217 -0
  5. package/dist/cli/db-drop.cjs +263 -0
  6. package/dist/cli/db-drop.d.cts +1 -0
  7. package/dist/cli/db-drop.d.ts +1 -0
  8. package/dist/cli/db-drop.js +240 -0
  9. package/dist/cli/db-migrate.cjs +318 -0
  10. package/dist/cli/db-migrate.d.cts +1 -0
  11. package/dist/cli/db-migrate.d.ts +1 -0
  12. package/dist/cli/db-migrate.js +295 -0
  13. package/dist/cli/db-reset.cjs +110 -0
  14. package/dist/cli/db-reset.d.cts +1 -0
  15. package/dist/cli/db-reset.d.ts +1 -0
  16. package/dist/cli/db-reset.js +87 -0
  17. package/dist/cli/db-seed.cjs +87 -0
  18. package/dist/cli/db-seed.d.cts +1 -0
  19. package/dist/cli/db-seed.d.ts +1 -0
  20. package/dist/cli/db-seed.js +64 -0
  21. package/dist/cli/index.cjs +352 -0
  22. package/dist/cli/index.d.cts +1 -0
  23. package/dist/cli/index.d.ts +1 -0
  24. package/dist/cli/index.js +328 -0
  25. package/dist/core/flareBuilder.cjs +681 -0
  26. package/dist/core/flareBuilder.d.cts +402 -0
  27. package/dist/core/flareBuilder.d.ts +402 -0
  28. package/dist/core/flareBuilder.js +658 -0
  29. package/dist/core/hooks.cjs +243 -0
  30. package/dist/core/hooks.d.cts +13 -0
  31. package/dist/core/hooks.d.ts +13 -0
  32. package/dist/core/hooks.js +209 -0
  33. package/dist/generated.cjs +31 -0
  34. package/dist/generated.d.cts +4 -0
  35. package/dist/generated.d.ts +4 -0
  36. package/dist/generated.js +6 -0
  37. package/dist/index.cjs +1315 -0
  38. package/dist/index.d.cts +237 -0
  39. package/dist/index.d.ts +237 -0
  40. package/dist/index.js +1261 -0
  41. package/dist/prisma.types-nGNe1CG8.d.cts +201 -0
  42. package/dist/prisma.types-nGNe1CG8.d.ts +201 -0
  43. package/license.md +21 -0
  44. package/package.json +115 -0
  45. package/readme.md +957 -0
package/readme.md ADDED
@@ -0,0 +1,957 @@
1
+ # Prisma Flare
2
+
3
+ A powerful TypeScript utilities package for Prisma ORM that provides a callback system and a query builder for chained operations.
4
+
5
+ ## Performance
6
+
7
+ Prisma Flare adds **virtually zero overhead** to your queries. Our rigorous benchmarks show:
8
+
9
+ | Query Type | Prisma | Flare | Overhead |
10
+ |------------|--------|-------|----------|
11
+ | findFirst by ID | 0.083ms | 0.083ms | +0.25% |
12
+ | findFirst + include | 0.202ms | 0.202ms | +0.23% |
13
+ | COUNT with WHERE | 0.091ms | 0.091ms | +0.34% |
14
+ | Complex query (WHERE + ORDER + LIMIT + INCLUDE) | 0.331ms | 0.332ms | +0.38% |
15
+ | Custom model methods in include | 0.940ms | 0.942ms | +0.14% |
16
+
17
+ **Median overhead: 0.1% - 0.4%** (~0.001ms per query)
18
+
19
+ <details>
20
+ <summary><b>Benchmark Methodology</b></summary>
21
+
22
+ - **500 iterations** per test with **50 warmup iterations** for connection pool
23
+ - **Random alternating execution** between Prisma and Flare to eliminate ordering bias
24
+ - **Statistical measures**: median, p95, standard deviation (median used for comparison)
25
+ - **Test data**: 10 users, 200 posts with realistic field values
26
+ - **Database**: SQLite (results are consistent across PostgreSQL/MySQL)
27
+
28
+ What Flare adds:
29
+ - Object instantiation: ~0.001ms (FlareBuilder class)
30
+ - Method chaining: ~0.001ms per method call
31
+ - Model registry lookup: ~0.001ms (Map.get for includes with custom methods)
32
+
33
+ Run benchmarks yourself:
34
+ ```bash
35
+ npm test -- --grep "Benchmark"
36
+ ```
37
+ </details>
38
+
39
+ ## Features
40
+
41
+ - **Plug & Play**: Works with any existing Prisma project
42
+ - **Flare Builder**: Elegant chainable query API for Prisma models
43
+ - **Auto-Generated Queries**: Automatically generates query classes based on your schema
44
+ - **Callback System**: Hooks for before/after operations (create, update, delete) and after upsert
45
+ - **Column-Level Hooks**: Track changes to specific columns with `afterChange` callbacks
46
+ - **Extended Prisma Client**: Enhanced PrismaClient with additional utility methods
47
+ - **Type-Safe**: Full IntelliSense and compile-time type checking
48
+
49
+ ## Installation
50
+
51
+ ```bash
52
+ npm install prisma-flare
53
+ ```
54
+
55
+ Ensure you have `@prisma/client` installed as a peer dependency.
56
+
57
+ ### Prisma Version Compatibility
58
+
59
+ | Prisma Version | prisma-flare Support |
60
+ |----------------|----------------------|
61
+ | 5.x | ✅ Full support |
62
+ | 6.x | ✅ Full support |
63
+ | 7.x+ | ✅ Full support |
64
+
65
+ prisma-flare automatically detects your Prisma version at runtime and uses the appropriate API:
66
+ - **Prisma ≤6**: Uses the legacy `$use()` middleware API
67
+ - **Prisma 7+**: Uses the new client extensions API
68
+
69
+ ## Setup
70
+
71
+ ### 1. Initialize your Client
72
+
73
+ Replace your standard `PrismaClient` with `FlareClient` in your database setup file (e.g., `src/db.ts` or `src/lib/prisma.ts`).
74
+
75
+ ```typescript
76
+ // src/db.ts
77
+ import { FlareClient, registerHooks } from 'prisma-flare';
78
+
79
+ // Initialize hooks middleware and auto-load callbacks
80
+ export const db = await registerHooks(new FlareClient());
81
+ ```
82
+
83
+ `registerHooks()` is async and:
84
+ - Registers the hooks middleware (using the appropriate API for your Prisma version)
85
+ - Automatically loads all callback files from `prisma/callbacks` (or your configured path)
86
+ - Returns the extended client instance
87
+
88
+ ### 2. Generate Query Classes
89
+
90
+ Run the generator to create type-safe query classes for your specific schema.
91
+
92
+ ```bash
93
+ npx prisma-flare generate
94
+ ```
95
+
96
+ By default, this will look for your `db` instance in `src/db` and output queries to `src/models`.
97
+
98
+ ### 3. Configuration (Optional)
99
+
100
+ If your project structure is different, create a `prisma-flare.config.json` in your project root:
101
+
102
+ ```json
103
+ {
104
+ "modelsPath": "src/models",
105
+ "dbPath": "src/lib/db",
106
+ "callbacksPath": "src/callbacks",
107
+ "envPath": ".env.local"
108
+ }
109
+ ```
110
+
111
+ - `modelsPath`: Where to generate the query classes (defaults to `prisma/models`).
112
+ - `dbPath`: Path to the file exporting your `db` instance (relative to project root, defaults to `prisma/db`).
113
+ - `callbacksPath`: Directory containing your callback/hook files (defaults to `prisma/callbacks`). All `.ts`/`.js` files in this directory are automatically loaded when `registerHooks()` is called.
114
+ - `envPath`: Path to your environment file (optional, defaults to `.env`).
115
+ - `plurals`: Custom pluralization for model names (optional).
116
+
117
+ Example with custom plurals:
118
+
119
+ ```json
120
+ {
121
+ "plurals": {
122
+ "Person": "people",
123
+ "Equipment": "equipment"
124
+ }
125
+ }
126
+ ```
127
+
128
+ ## Usage
129
+
130
+ ### Flare Builder
131
+
132
+ Once generated, you can import the `DB` class to access chainable methods for your models.
133
+
134
+ ```typescript
135
+ import { DB } from 'prisma-flare/generated';
136
+
137
+ // Chainable query builder with full type safety
138
+ const posts = await DB.posts
139
+ .where({ published: true })
140
+ .order({ createdAt: 'desc' })
141
+ .limit(10)
142
+ .include({ author: true })
143
+ .findMany();
144
+
145
+ // Complex filtering made easy
146
+ const activeUsers = await DB.users
147
+ .where({ isActive: true })
148
+ .where({ role: 'ADMIN' })
149
+ .count();
150
+
151
+ // Pagination
152
+ const { data, meta } = await DB.users.paginate(1, 15);
153
+
154
+ // Conditional queries
155
+ const search = 'John';
156
+ const users = await DB.users
157
+ .when(!!search, (q) => q.where({ name: { contains: search } }))
158
+ .findMany();
159
+
160
+ // Access raw Prisma Client instance
161
+ const rawDb = DB.instance;
162
+ ```
163
+
164
+ ### Transactions
165
+
166
+ Prisma Flare provides a powerful wrapper around Prisma's interactive transactions, allowing you to use the fluent `from()` API within a transaction scope.
167
+
168
+ ```typescript
169
+ // Simple transaction
170
+ const result = await DB.instance.transaction(async (tx) => {
171
+ // Create a user
172
+ const user = await tx.from('user').create({
173
+ email: 'tx-user@example.com',
174
+ name: 'Transaction User',
175
+ });
176
+
177
+ // Create a related post using the user's ID
178
+ const post = await tx.from('post').create({
179
+ title: 'Transaction Post',
180
+ content: 'Content',
181
+ authorId: user.id,
182
+ });
183
+
184
+ return { user, post };
185
+ });
186
+
187
+ // Complex logic with conditional operations
188
+ await DB.instance.transaction(async (tx) => {
189
+ const existing = await tx.from('user').where({ email: 'check@example.com' }).findFirst();
190
+
191
+ if (!existing) {
192
+ await tx.from('user').create({
193
+ email: 'check@example.com',
194
+ name: 'New User'
195
+ });
196
+ } else {
197
+ await tx.from('user').withId(existing.id).update({
198
+ lastLogin: new Date()
199
+ });
200
+ }
201
+ });
202
+ ```
203
+
204
+ ### Callhooks & Middleware
205
+
206
+ Define hooks to run logic before or after database operations. Create callback files in your callbacks directory (default: `prisma/callbacks`) and they'll be automatically loaded.
207
+
208
+ ```typescript
209
+ // prisma/callbacks/user.ts
210
+ import { beforeCreate, afterCreate } from 'prisma-flare';
211
+
212
+ // Validation: Prevent creating users with invalid emails
213
+ beforeCreate('user', async (args) => {
214
+ if (!args.data.email.includes('@')) {
215
+ throw new Error('Invalid email address');
216
+ }
217
+ });
218
+
219
+ // Run after a user is created
220
+ afterCreate('user', async (args, result) => {
221
+ console.log('New user created:', result.email);
222
+ await sendWelcomeEmail(result.email);
223
+ });
224
+ ```
225
+
226
+ ```typescript
227
+ // prisma/callbacks/post.ts
228
+ import { afterChange } from 'prisma-flare';
229
+
230
+ // Run when the 'published' field on post changes
231
+ afterChange('post', 'published', async (oldValue, newValue, record) => {
232
+ if (!oldValue && newValue) {
233
+ console.log(`Post "${record.title}" was published!`);
234
+ }
235
+ });
236
+ ```
237
+
238
+ All files in the callbacks directory are automatically imported when `registerHooks()` is called. No manual loading required.
239
+
240
+ #### Hook Configuration
241
+
242
+ Configure hook behavior globally, especially useful for performance tuning:
243
+
244
+ ```typescript
245
+ import { hookRegistry } from 'prisma-flare';
246
+
247
+ // Disable column hooks globally (for performance-critical paths)
248
+ hookRegistry.configure({ enableColumnHooks: false });
249
+
250
+ // Limit re-fetching on large updateMany operations
251
+ // Column hooks will be skipped if more than 1000 records are affected
252
+ hookRegistry.configure({ maxRefetch: 1000 });
253
+
254
+ // Disable the warning when hooks are skipped
255
+ hookRegistry.configure({ warnOnSkip: false });
256
+
257
+ // Check current configuration
258
+ const config = hookRegistry.getConfig();
259
+ ```
260
+
261
+ **Configuration options:**
262
+
263
+ | Option | Default | Description |
264
+ |--------|---------|-------------|
265
+ | `enableColumnHooks` | `true` | Enable/disable all column-level hooks |
266
+ | `maxRefetch` | `1000` | Max records to re-fetch for column hooks. Prevents expensive operations on large `updateMany`. Set to `Infinity` to disable limit. |
267
+ | `warnOnSkip` | `true` | Log warning when hooks are skipped due to limits |
268
+
269
+ #### Per-Call Hook Skip
270
+
271
+ For fine-grained control, you can skip column hooks on a per-call basis without changing global configuration:
272
+
273
+ ```typescript
274
+ // Skip column hooks for this specific update only
275
+ await DB.users.withId(userId).update({
276
+ status: 'active',
277
+ // This meta key is stripped before reaching Prisma
278
+ __flare: { skipColumnHooks: true }
279
+ } as any);
280
+
281
+ // Regular hooks (beforeUpdate, afterUpdate) still fire
282
+ // Only column-level hooks (afterChange) are skipped
283
+ ```
284
+
285
+ This is useful for:
286
+ - Batch migrations where you don't want to trigger side effects
287
+ - Performance-critical paths where you know the column change doesn't matter
288
+ - Avoiding recursive hook triggers
289
+
290
+ #### Smart Value Comparison
291
+
292
+ Column hooks use intelligent comparison to detect real changes:
293
+
294
+ | Type | Comparison Method |
295
+ |------|-------------------|
296
+ | `Date` | Compares by `.getTime()` (milliseconds) |
297
+ | `Decimal` (Prisma) | Compares by `.toString()` |
298
+ | `null` / `undefined` | Strict equality |
299
+ | Objects/JSON | Deep comparison via `JSON.stringify` |
300
+ | Primitives | Strict equality (`===`) |
301
+
302
+ This prevents false positives when:
303
+ - Dates are re-assigned but represent the same moment
304
+ - Decimal values are equivalent but different instances
305
+ - JSON fields are structurally identical
306
+
307
+ #### Advanced Hook Registration
308
+
309
+ For more control over hook registration, prisma-flare exports additional utilities:
310
+
311
+ ```typescript
312
+ import {
313
+ registerHooks, // Auto-detects Prisma version + auto-loads callbacks (recommended)
314
+ registerHooksLegacy, // Force legacy $use API (Prisma ≤6 only, no auto-load)
315
+ createHooksExtension, // Get raw extension for manual use
316
+ loadCallbacks // Manually load callbacks from a custom path
317
+ } from 'prisma-flare';
318
+
319
+ // Option 1: Auto-detect with auto-loading (recommended)
320
+ const db = await registerHooks(new FlareClient());
321
+
322
+ // Option 2: Manual callback loading from custom path
323
+ import { PrismaClient } from '@prisma/client';
324
+ const prisma = new PrismaClient().$extends(createHooksExtension(new PrismaClient()));
325
+ await loadCallbacks('/custom/path/to/callbacks');
326
+ ```
327
+
328
+ ## CLI Utilities
329
+
330
+ Prisma Flare comes with a suite of CLI tools to manage your database workflow. It supports **PostgreSQL** and **SQLite** out of the box, and is extensible for other databases.
331
+
332
+ ```bash
333
+ npx prisma-flare generate # Generate query classes from schema
334
+ npx prisma-flare create # Create database
335
+ npx prisma-flare drop # Drop database
336
+ npx prisma-flare migrate # Run migrations
337
+ npx prisma-flare reset # Reset database
338
+ npx prisma-flare seed # Seed database
339
+ ```
340
+
341
+ ### Custom Database Adapters
342
+
343
+ You can add support for other databases by registering a custom adapter.
344
+
345
+ ```typescript
346
+ import { dbAdapterRegistry, DatabaseAdapter } from 'prisma-flare';
347
+
348
+ const myAdapter: DatabaseAdapter = {
349
+ name: 'my-db',
350
+ matches: (url) => url.startsWith('mydb://'),
351
+ create: async (url) => { /* custom create logic */ },
352
+ drop: async (url) => { /* custom drop logic */ }
353
+ };
354
+
355
+ dbAdapterRegistry.register(myAdapter);
356
+ ```
357
+
358
+ ## Custom Query Methods
359
+
360
+ You can extend the generated query classes with custom methods for your domain-specific needs. Simply add methods to your query class that use the built-in `where()` method to build conditions.
361
+
362
+ ```typescript
363
+ // src/models/Post.ts
364
+ import { db } from './db';
365
+ import { FlareBuilder } from 'prisma-flare';
366
+
367
+ export default class Post extends FlareBuilder<'post'> {
368
+ constructor() {
369
+ super(db.post);
370
+ }
371
+
372
+ // Filter published posts
373
+ published(): this {
374
+ this.where({ published: true });
375
+ return this;
376
+ }
377
+
378
+ // Filter draft posts
379
+ drafts(): this {
380
+ this.where({ published: false });
381
+ return this;
382
+ }
383
+
384
+ // Search by title (contains)
385
+ withTitle(title: string): this {
386
+ this.where({ title: { contains: title } });
387
+ return this;
388
+ }
389
+
390
+ // Filter by author ID
391
+ withAuthorId(authorId: number): this {
392
+ this.where({ authorId });
393
+ return this;
394
+ }
395
+
396
+ // Get recent posts
397
+ recent(days: number): this {
398
+ const date = new Date();
399
+ date.setDate(date.getDate() - days);
400
+ this.where({ createdAt: { gte: date } });
401
+ return this;
402
+ }
403
+ }
404
+ ```
405
+
406
+ Then use your custom methods in queries:
407
+
408
+ ```typescript
409
+ import { DB } from 'prisma-flare/generated';
410
+
411
+
412
+ // Use custom methods with full chainability
413
+ const recentPublished = await DB.post
414
+ .published()
415
+ .recent(7)
416
+ .order({ createdAt: 'desc' })
417
+ .findMany();
418
+
419
+ const authorPosts = await DB.post
420
+ .withAuthorId(123)
421
+ .withTitle('TypeScript')
422
+ .include({ author: true })
423
+ .findMany();
424
+ ```
425
+
426
+ **Tips for Custom Methods:**
427
+ - Always return `this` to maintain chainability
428
+ - Use descriptive names with prefixes like `with*` for filters
429
+ - Leverage Prisma's query operators (`contains`, `gte`, `lte`, etc.)
430
+ - Keep methods focused on a single responsibility
431
+
432
+ ## Flare Builder API Reference
433
+
434
+ ### Query Building Methods
435
+
436
+ These methods build and customize your query before execution.
437
+
438
+ #### `where(condition)`
439
+ Adds a WHERE condition to the query with full type safety from Prisma.
440
+
441
+ ```typescript
442
+ // Single condition
443
+ const users = await DB.users.where({ isActive: true }).findMany();
444
+
445
+ // Multiple conditions (merged together)
446
+ const users = await DB.users
447
+ .where({ isActive: true })
448
+ .where({ role: 'ADMIN' })
449
+ .findMany();
450
+
451
+ // Complex conditions with operators
452
+ const users = await DB.users.where({
453
+ email: { contains: 'example.com' },
454
+ age: { gte: 18 }
455
+ }).findMany();
456
+ ```
457
+
458
+ ### Boolean Logic (AND/OR/NOT)
459
+
460
+ Prisma Flare provides explicit control over boolean logic. Understanding how conditions compose is critical for correct queries.
461
+
462
+ #### How `where()` chaining works
463
+
464
+ Multiple `where()` calls are composed using **AND** logic:
465
+
466
+ ```typescript
467
+ // These are equivalent:
468
+ DB.users.where({ status: 'active' }).where({ role: 'admin' })
469
+ // → { AND: [{ status: 'active' }, { role: 'admin' }] }
470
+ ```
471
+
472
+ #### `orWhere(condition)` - ⚠️ Advanced
473
+
474
+ `orWhere()` wraps the **entire accumulated where** in an OR:
475
+
476
+ ```typescript
477
+ DB.users.where({ status: 'active' }).orWhere({ role: 'admin' })
478
+ // → { OR: [{ status: 'active' }, { role: 'admin' }] }
479
+ ```
480
+
481
+ **⚠️ Common Mistake:** Adding more conditions after `orWhere` can produce unexpected results:
482
+
483
+ ```typescript
484
+ // ❌ WRONG: User thinks "active users named Alice or Bob"
485
+ const wrong = await DB.users
486
+ .where({ status: 'active' })
487
+ .where({ name: 'Alice' })
488
+ .orWhere({ name: 'Bob' }) // This OR-wraps EVERYTHING before it!
489
+ .findMany();
490
+ // Actual: (status='active' AND name='Alice') OR (name='Bob')
491
+ // Bob is included even if inactive!
492
+
493
+ // ✅ CORRECT: Use whereGroup for explicit grouping
494
+ const correct = await DB.users
495
+ .where({ status: 'active' })
496
+ .whereGroup(qb => qb
497
+ .where({ name: 'Alice' })
498
+ .orWhere({ name: 'Bob' })
499
+ )
500
+ .findMany();
501
+ // Result: status='active' AND (name='Alice' OR name='Bob')
502
+ ```
503
+
504
+ For complex logic, **always prefer `whereGroup()`** for explicit control.
505
+
506
+ #### `whereGroup(callback)` - Recommended for complex logic
507
+
508
+ Creates an explicit group that's AND-ed with the existing where:
509
+
510
+ ```typescript
511
+ // (status = 'active') AND (role = 'admin' OR role = 'moderator')
512
+ const users = await DB.users
513
+ .where({ status: 'active' })
514
+ .whereGroup(qb => qb
515
+ .where({ role: 'admin' })
516
+ .orWhere({ role: 'moderator' })
517
+ )
518
+ .findMany();
519
+ ```
520
+
521
+ #### `orWhereGroup(callback)`
522
+
523
+ Creates an explicit group that's OR-ed with the existing where:
524
+
525
+ ```typescript
526
+ // (status = 'active') OR (role = 'admin' AND verified = true)
527
+ const users = await DB.users
528
+ .where({ status: 'active' })
529
+ .orWhereGroup(qb => qb
530
+ .where({ role: 'admin' })
531
+ .where({ verified: true })
532
+ )
533
+ .findMany();
534
+ ```
535
+
536
+ #### NOT conditions
537
+
538
+ Use Prisma's `NOT` operator inside `where()`:
539
+
540
+ ```typescript
541
+ // Active users who are NOT banned
542
+ const users = await DB.users
543
+ .where({ status: 'active' })
544
+ .where({ NOT: { role: 'banned' } })
545
+ .findMany();
546
+ ```
547
+
548
+ #### Quick Reference
549
+
550
+ | Pattern | Result |
551
+ |---------|--------|
552
+ | `.where(A).where(B)` | `A AND B` |
553
+ | `.where(A).orWhere(B)` | `A OR B` |
554
+ | `.where(A).orWhere(B).where(C)` | `(A OR B) AND C` |
555
+ | `.where(A).whereGroup(q => q.where(B).orWhere(C))` | `A AND (B OR C)` |
556
+ | `.where(A).orWhereGroup(q => q.where(B).where(C))` | `A OR (B AND C)` |
557
+
558
+ **Rule of thumb:** For anything beyond simple AND chains or single OR, use `whereGroup()`/`orWhereGroup()`.
559
+
560
+ #### `withId(id)`
561
+ Filters records by ID. Throws an error if no ID is provided.
562
+
563
+ ```typescript
564
+ const user = await DB.users.withId(123).findFirst();
565
+ const post = await DB.posts.withId('uuid-string').findUnique();
566
+ ```
567
+
568
+ #### `select(fields)`
569
+ Selects specific fields to retrieve. Reduces data transfer and improves query performance.
570
+
571
+ ```typescript
572
+ // Select only name and email
573
+ const users = await DB.users
574
+ .select({ id: true, name: true, email: true })
575
+ .findMany();
576
+
577
+ // Combine with other conditions
578
+ const admin = await DB.users
579
+ .where({ role: 'ADMIN' })
580
+ .select({ id: true, email: true })
581
+ .findFirst();
582
+ ```
583
+
584
+ #### `include(relation)` or `include(relation, callback)`
585
+ Includes related records in the query. Can be called multiple times for nested relations.
586
+
587
+ ```typescript
588
+ // Include all default fields from the relation
589
+ const posts = await DB.posts
590
+ .include('author')
591
+ .findMany();
592
+
593
+ // Include with custom query on the relation
594
+ const posts = await DB.posts
595
+ .include('author', (q) =>
596
+ q.select({ id: true, name: true, email: true })
597
+ )
598
+ .findMany();
599
+
600
+ // Multiple includes
601
+ const posts = await DB.posts
602
+ .include('author')
603
+ .include('comments', (q) => q.limit(5))
604
+ .findMany();
605
+
606
+ // Nested includes
607
+ const posts = await DB.posts
608
+ .include('author', (q) =>
609
+ q.include('profile')
610
+ )
611
+ .findMany();
612
+ ```
613
+
614
+ #### `order(orderBy)`
615
+ Adds ordering to the query results.
616
+
617
+ ```typescript
618
+ // Single field ascending
619
+ const users = await DB.users.order({ createdAt: 'asc' }).findMany();
620
+
621
+ // Single field descending
622
+ const posts = await DB.posts.order({ published: 'desc' }).findMany();
623
+
624
+ // Multiple fields
625
+ const comments = await DB.comments
626
+ .order({ likes: 'desc', createdAt: 'asc' })
627
+ .findMany();
628
+ ```
629
+
630
+ #### `first(key?)` and `last(key?)`
631
+ Convenience methods to get the first or last record. Automatically sets limit to 1.
632
+
633
+ ```typescript
634
+ // Get the first user (by createdAt)
635
+ const first = await DB.users.first().findFirst();
636
+
637
+ // Get the last post (by date)
638
+ const latest = await DB.posts.last('publishedAt').findFirst();
639
+
640
+ // Chain with where conditions
641
+ const first = await DB.posts
642
+ .where({ published: true })
643
+ .first()
644
+ .findFirst();
645
+ ```
646
+
647
+ #### `limit(n)`
648
+ Limits the number of records returned.
649
+
650
+ ```typescript
651
+ const topTen = await DB.posts.limit(10).findMany();
652
+ ```
653
+
654
+ #### `skip(offset)`
655
+ Skips a number of records (useful for custom pagination).
656
+
657
+ ```typescript
658
+ const page = await DB.users.skip(20).limit(10).findMany();
659
+ ```
660
+
661
+ #### `distinct(fields)`
662
+ Returns only distinct records based on the specified fields.
663
+
664
+ ```typescript
665
+ // Get distinct user emails
666
+ const distinctEmails = await DB.users
667
+ .distinct({ email: true })
668
+ .select({ email: true })
669
+ .findMany();
670
+ ```
671
+
672
+ #### `groupBy(fields)`
673
+ Groups results by the specified fields (aggregation).
674
+
675
+ ```typescript
676
+ const grouped = await DB.posts
677
+ .groupBy({ authorId: true })
678
+ .findMany();
679
+ ```
680
+
681
+ #### `having(condition)`
682
+ Adds a HAVING clause for aggregate queries.
683
+
684
+ ```typescript
685
+ const authors = await DB.posts
686
+ .groupBy({ authorId: true })
687
+ .having({ id: { _count: { gt: 5 } } })
688
+ .findMany();
689
+ ```
690
+
691
+ #### `getQuery()`
692
+ Returns the current internal query object. Useful for debugging or passing to raw operations.
693
+
694
+ ```typescript
695
+ const query = DB.users.where({ active: true }).getQuery();
696
+ console.log(query);
697
+ ```
698
+
699
+ ### Execution Methods
700
+
701
+ These methods execute the query and return results.
702
+
703
+ #### `findMany()`
704
+ Returns all records matching the query conditions.
705
+
706
+ ```typescript
707
+ const allUsers = await DB.users.findMany();
708
+ const activeUsers = await DB.users.where({ isActive: true }).findMany();
709
+ const limited = await DB.users.limit(10).findMany();
710
+ ```
711
+
712
+ #### `findFirst()`
713
+ Returns the first record matching the query, or `null` if none found.
714
+
715
+ ```typescript
716
+ const user = await DB.users.where({ email: 'user@example.com' }).findFirst();
717
+ if (user) {
718
+ console.log('User found:', user.name);
719
+ }
720
+ ```
721
+
722
+ #### `findFirstOrThrow()`
723
+ Like `findFirst()`, but throws a `Prisma.NotFoundError` if no record is found.
724
+
725
+ ```typescript
726
+ try {
727
+ const user = await DB.users
728
+ .where({ email: 'admin@example.com' })
729
+ .findFirstOrThrow();
730
+ } catch (error) {
731
+ console.error('User not found');
732
+ }
733
+ ```
734
+
735
+ #### `findUnique()`
736
+ Finds a record by a unique constraint (typically the ID). Returns `null` if not found.
737
+
738
+ ```typescript
739
+ const user = await DB.users.withId(123).findUnique();
740
+ ```
741
+
742
+ #### `findUniqueOrThrow()`
743
+ Like `findUnique()`, but throws an error if the record is not found.
744
+
745
+ ```typescript
746
+ const user = await DB.users.withId(123).findUniqueOrThrow();
747
+ ```
748
+
749
+ #### `create(data)`
750
+ Creates a new record with the provided data. Triggers any registered hooks.
751
+
752
+ ```typescript
753
+ const newUser = await DB.users.create({
754
+ email: 'new@example.com',
755
+ name: 'New User'
756
+ });
757
+ ```
758
+
759
+ #### `createMany(data)`
760
+ Creates multiple records in a single operation. More efficient than individual creates.
761
+
762
+ ```typescript
763
+ const result = await DB.posts.createMany({
764
+ data: [
765
+ { title: 'Post 1', content: 'Content 1', authorId: 1 },
766
+ { title: 'Post 2', content: 'Content 2', authorId: 1 }
767
+ ]
768
+ });
769
+ console.log(`Created ${result.count} posts`);
770
+ ```
771
+
772
+ #### `update(data)`
773
+ Updates a single record. Requires a unique constraint (typically id) in the where condition.
774
+
775
+ ```typescript
776
+ const updated = await DB.users
777
+ .withId(123)
778
+ .update({ name: 'Updated Name' });
779
+ ```
780
+
781
+ #### `updateMany(data)`
782
+ Updates multiple records matching the current conditions.
783
+
784
+ ```typescript
785
+ const result = await DB.users
786
+ .where({ status: 'inactive' })
787
+ .updateMany({ lastLogin: new Date() });
788
+ console.log(`Updated ${result.count} users`);
789
+ ```
790
+
791
+ #### `delete()`
792
+ Deletes a single record. Requires a unique constraint in the where condition.
793
+
794
+ ```typescript
795
+ const deleted = await DB.posts.withId(123).delete();
796
+ ```
797
+
798
+ #### `deleteMany()`
799
+ Deletes multiple records matching the current conditions.
800
+
801
+ ```typescript
802
+ const result = await DB.posts
803
+ .where({ published: false })
804
+ .deleteMany();
805
+ console.log(`Deleted ${result.count} drafts`);
806
+ ```
807
+
808
+ #### `upsert(args)`
809
+ Updates a record if it exists, otherwise creates a new one.
810
+
811
+ ```typescript
812
+ const result = await DB.users
813
+ .where({ email: 'user@example.com' })
814
+ .upsert({
815
+ create: { email: 'user@example.com', name: 'New User' },
816
+ update: { lastLogin: new Date() }
817
+ });
818
+ ```
819
+
820
+ ### Aggregation Methods
821
+
822
+ These methods perform calculations on your data.
823
+
824
+ #### `count()`
825
+ Counts records matching the current query.
826
+
827
+ ```typescript
828
+ const totalUsers = await DB.users.count();
829
+ const activeCount = await DB.users.where({ isActive: true }).count();
830
+ ```
831
+
832
+ #### `sum(field)`
833
+ Sums a numeric field across matching records.
834
+
835
+ ```typescript
836
+ const totalSales = await DB.orders.where({ status: 'completed' }).sum('amount');
837
+ ```
838
+
839
+ #### `avg(field)`
840
+ Calculates the average of a numeric field.
841
+
842
+ ```typescript
843
+ const avgPrice = await DB.products.avg('price');
844
+ ```
845
+
846
+ #### `min(field)`
847
+ Finds the minimum value of a field.
848
+
849
+ ```typescript
850
+ const oldest = await DB.users.min('createdAt');
851
+ ```
852
+
853
+ #### `max(field)`
854
+ Finds the maximum value of a field.
855
+
856
+ ```typescript
857
+ const latest = await DB.posts.max('publishedAt');
858
+ ```
859
+
860
+ ### Utility Methods
861
+
862
+ These methods provide additional functionality for querying and data processing.
863
+
864
+ #### `only(field)`
865
+ Selects and returns only a specific field value from the first matching record.
866
+
867
+ ```typescript
868
+ const email = await DB.users.withId(123).only('email');
869
+ // Returns: 'user@example.com' or null
870
+ ```
871
+
872
+ #### `pluck(field)`
873
+ Extracts a specific field from all matching records as an array.
874
+
875
+ ```typescript
876
+ const emails = await DB.users.where({ isActive: true }).pluck('email');
877
+ // Returns: ['user1@example.com', 'user2@example.com', ...]
878
+ ```
879
+
880
+ #### `exists(key?)`
881
+ Checks if any record exists matching the current query.
882
+
883
+ ```typescript
884
+ const hasAdmins = await DB.users.where({ role: 'ADMIN' }).exists();
885
+
886
+ // Check for existence of a specific field
887
+ const hasEmail = await DB.users.where({ id: 123 }).exists('email');
888
+ ```
889
+
890
+ #### `paginate(page, perPage)`
891
+ Returns paginated results with metadata for easy navigation.
892
+
893
+ ```typescript
894
+ const result = await DB.users.where({ isActive: true }).paginate(1, 15);
895
+
896
+ console.log(result.data); // Array of users
897
+ console.log(result.meta); // Pagination metadata
898
+
899
+ // Meta structure
900
+ {
901
+ total: 150, // Total records matching query
902
+ lastPage: 10, // Total number of pages
903
+ currentPage: 1, // Current page number
904
+ perPage: 15, // Records per page
905
+ prev: null, // Previous page number or null
906
+ next: 2 // Next page number or null
907
+ }
908
+
909
+ // Fetch next page
910
+ const nextPage = await DB.users
911
+ .where({ isActive: true })
912
+ .paginate(result.meta.next, 15);
913
+ ```
914
+
915
+ #### `when(condition, callback)`
916
+ Conditionally applies query operations based on a boolean or function.
917
+
918
+ ```typescript
919
+ const search = req.query.search;
920
+ const role = req.query.role;
921
+
922
+ const users = await DB.users
923
+ .when(!!search, (q) => q.where({ name: { contains: search } }))
924
+ .when(!!role, (q) => q.where({ role }))
925
+ .findMany();
926
+
927
+ // With function condition
928
+ const users = await DB.users
929
+ .when(() => isAdmin(user), (q) => q.select({ id: true, email: true, role: true }))
930
+ .findMany();
931
+ ```
932
+
933
+ #### `chunk(size, callback)`
934
+ Processes large datasets in chunks to avoid memory issues.
935
+
936
+ ```typescript
937
+ await DB.posts.chunk(100, async (posts) => {
938
+ // Process each chunk of 100 posts
939
+ for (const post of posts) {
940
+ await sendNotification(post.authorId);
941
+ }
942
+ });
943
+ ```
944
+
945
+ #### `clone()`
946
+ Creates an independent copy of the current query builder.
947
+
948
+ ```typescript
949
+ const baseQuery = DB.posts.where({ published: true });
950
+
951
+ const recent = baseQuery.clone().order({ createdAt: 'desc' }).findMany();
952
+ const popular = baseQuery.clone().order({ likes: 'desc' }).findMany();
953
+ ```
954
+
955
+ ## License
956
+
957
+ ISC