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/dist/index.js ADDED
@@ -0,0 +1,1261 @@
1
+ // src/core/extendedPrismaClient.ts
2
+ import { PrismaClient } from "@prisma/client";
3
+
4
+ // src/core/modelRegistry.ts
5
+ var ModelRegistry = class {
6
+ constructor() {
7
+ this.models = /* @__PURE__ */ new Map();
8
+ }
9
+ /**
10
+ * Register a custom model class for a given model name.
11
+ * The model name should match the Prisma model name (e.g., 'user', 'post', 'enrollment')
12
+ * @param modelName - The lowercase model name (matching Prisma delegate name)
13
+ * @param modelClass - The custom class that extends FlareBuilder
14
+ */
15
+ register(modelName, modelClass) {
16
+ this.models.set(modelName.toLowerCase(), modelClass);
17
+ }
18
+ /**
19
+ * Register multiple models at once
20
+ * @param models - Object mapping model names to their classes
21
+ */
22
+ registerMany(models) {
23
+ for (const [name, modelClass] of Object.entries(models)) {
24
+ this.register(name, modelClass);
25
+ }
26
+ }
27
+ /**
28
+ * Get a custom model class by name
29
+ * @param modelName - The model name to look up
30
+ * @returns The model class or undefined if not registered
31
+ */
32
+ get(modelName) {
33
+ return this.models.get(modelName.toLowerCase());
34
+ }
35
+ /**
36
+ * Check if a model is registered
37
+ * @param modelName - The model name to check
38
+ */
39
+ has(modelName) {
40
+ return this.models.has(modelName.toLowerCase());
41
+ }
42
+ /**
43
+ * Create an instance of a registered model
44
+ * @param modelName - The model name to instantiate
45
+ * @returns A new instance of the custom model class, or undefined if not registered
46
+ */
47
+ create(modelName) {
48
+ const ModelClass = this.get(modelName);
49
+ if (ModelClass) {
50
+ return new ModelClass();
51
+ }
52
+ return void 0;
53
+ }
54
+ /**
55
+ * Clear all registered models (useful for testing)
56
+ */
57
+ clear() {
58
+ this.models.clear();
59
+ }
60
+ /**
61
+ * Get all registered model names
62
+ */
63
+ getRegisteredModels() {
64
+ return Array.from(this.models.keys());
65
+ }
66
+ };
67
+ var modelRegistry = new ModelRegistry();
68
+
69
+ // src/core/flareBuilder.ts
70
+ function deepClone(obj) {
71
+ if (obj === null || typeof obj !== "object") {
72
+ return obj;
73
+ }
74
+ if (typeof obj === "bigint") {
75
+ return obj;
76
+ }
77
+ if (typeof structuredClone === "function") {
78
+ try {
79
+ return structuredClone(obj);
80
+ } catch {
81
+ }
82
+ }
83
+ if (obj instanceof Date) {
84
+ return new Date(obj.getTime());
85
+ }
86
+ if (obj instanceof RegExp) {
87
+ return new RegExp(obj.source, obj.flags);
88
+ }
89
+ if (typeof Buffer !== "undefined" && Buffer.isBuffer(obj)) {
90
+ return Buffer.from(obj);
91
+ }
92
+ if (obj instanceof ArrayBuffer) {
93
+ return obj.slice(0);
94
+ }
95
+ if (ArrayBuffer.isView(obj) && !(obj instanceof DataView)) {
96
+ const TypedArrayConstructor = obj.constructor;
97
+ const buffer = obj.buffer instanceof ArrayBuffer ? obj.buffer.slice(0) : obj.buffer;
98
+ return new TypedArrayConstructor(buffer);
99
+ }
100
+ if (obj instanceof Map) {
101
+ const clonedMap = /* @__PURE__ */ new Map();
102
+ obj.forEach((value, key) => {
103
+ clonedMap.set(deepClone(key), deepClone(value));
104
+ });
105
+ return clonedMap;
106
+ }
107
+ if (obj instanceof Set) {
108
+ const clonedSet = /* @__PURE__ */ new Set();
109
+ obj.forEach((value) => {
110
+ clonedSet.add(deepClone(value));
111
+ });
112
+ return clonedSet;
113
+ }
114
+ if (Array.isArray(obj)) {
115
+ return obj.map((item) => deepClone(item));
116
+ }
117
+ if (typeof obj.toDecimalPlaces === "function") {
118
+ return obj;
119
+ }
120
+ const prototype = Object.getPrototypeOf(obj);
121
+ const cloned = Object.create(prototype);
122
+ for (const key in obj) {
123
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
124
+ cloned[key] = deepClone(obj[key]);
125
+ }
126
+ }
127
+ return cloned;
128
+ }
129
+ var FlareBuilder = class _FlareBuilder {
130
+ constructor(model, query = {}) {
131
+ this.model = model;
132
+ this.query = query;
133
+ }
134
+ /**
135
+ * Adds a where condition to the query with type safety from Prisma.
136
+ * Multiple where() calls are composed using AND logic to avoid silent overwrites.
137
+ * @param condition - Where filter matching your Prisma model
138
+ *
139
+ * @example
140
+ * // These conditions are AND-ed together:
141
+ * DB.posts
142
+ * .where({ published: true })
143
+ * .where({ authorId: 1 })
144
+ * .findMany()
145
+ * // Equivalent to: { AND: [{ published: true }, { authorId: 1 }] }
146
+ */
147
+ where(condition) {
148
+ if (!this.query.where || Object.keys(this.query.where).length === 0) {
149
+ this.query.where = condition;
150
+ } else {
151
+ const prevWhere = this.query.where;
152
+ this.query.where = { AND: [prevWhere, condition] };
153
+ }
154
+ return this;
155
+ }
156
+ /**
157
+ * Adds a where condition using AND logic (explicit alias for where())
158
+ * @param condition - Where filter matching your Prisma model
159
+ *
160
+ * @example
161
+ * DB.posts
162
+ * .where({ published: true })
163
+ * .andWhere({ createdAt: { gte: new Date('2024-01-01') } })
164
+ * .findMany()
165
+ */
166
+ andWhere(condition) {
167
+ return this.where(condition);
168
+ }
169
+ /**
170
+ * Adds a where condition using OR logic.
171
+ *
172
+ * ⚠️ **IMPORTANT**: `orWhere()` wraps the *entire* accumulated where clause:
173
+ * `OR([prevWhere, condition])`. This means:
174
+ *
175
+ * ```ts
176
+ * .where(A).orWhere(B).where(C) // becomes: (A OR B) AND C
177
+ * ```
178
+ *
179
+ * For complex boolean logic, prefer `whereGroup()` / `orWhereGroup()` for explicit control.
180
+ *
181
+ * @param condition - Where filter matching your Prisma model
182
+ *
183
+ * @example
184
+ * // Simple case - OK:
185
+ * DB.posts
186
+ * .where({ published: true })
187
+ * .orWhere({ featured: true })
188
+ * .findMany()
189
+ * // Result: published OR featured
190
+ *
191
+ * @example
192
+ * // For complex logic, use whereGroup instead:
193
+ * DB.posts
194
+ * .where({ published: true })
195
+ * .whereGroup(qb => qb
196
+ * .where({ category: 'news' })
197
+ * .orWhere({ category: 'tech' })
198
+ * )
199
+ * .findMany()
200
+ * // Result: published AND (category='news' OR category='tech')
201
+ */
202
+ orWhere(condition) {
203
+ if (!this.query.where || Object.keys(this.query.where).length === 0) {
204
+ this.query.where = condition;
205
+ } else {
206
+ const prevWhere = this.query.where;
207
+ this.query.where = { OR: [prevWhere, condition] };
208
+ }
209
+ return this;
210
+ }
211
+ /**
212
+ * Creates a grouped where condition using a callback.
213
+ * Use this for explicit control over boolean logic grouping.
214
+ * The callback receives a fresh builder - its accumulated where becomes a single group.
215
+ *
216
+ * @param callback - Function that builds the grouped condition
217
+ * @param mode - How to combine with existing where: 'AND' (default) or 'OR'
218
+ *
219
+ * @example
220
+ * // (status = 'active') AND (name LIKE 'A%' OR name LIKE 'B%')
221
+ * DB.users
222
+ * .where({ status: 'active' })
223
+ * .whereGroup(qb => qb
224
+ * .where({ name: { startsWith: 'A' } })
225
+ * .orWhere({ name: { startsWith: 'B' } })
226
+ * )
227
+ * .findMany()
228
+ *
229
+ * @example
230
+ * // (status = 'active') OR (role = 'admin' AND verified = true)
231
+ * DB.users
232
+ * .where({ status: 'active' })
233
+ * .whereGroup(qb => qb
234
+ * .where({ role: 'admin' })
235
+ * .where({ verified: true })
236
+ * , 'OR')
237
+ * .findMany()
238
+ */
239
+ whereGroup(callback, mode = "AND") {
240
+ const groupBuilder = new _FlareBuilder(this.model, {});
241
+ callback(groupBuilder);
242
+ const groupWhere = groupBuilder.getQuery().where;
243
+ if (!groupWhere || Object.keys(groupWhere).length === 0) {
244
+ return this;
245
+ }
246
+ if (!this.query.where || Object.keys(this.query.where).length === 0) {
247
+ this.query.where = groupWhere;
248
+ } else {
249
+ const prevWhere = this.query.where;
250
+ this.query.where = { [mode]: [prevWhere, groupWhere] };
251
+ }
252
+ return this;
253
+ }
254
+ /**
255
+ * Alias for whereGroup with OR mode.
256
+ * Creates a grouped condition that's OR-ed with existing where.
257
+ *
258
+ * @param callback - Function that builds the grouped condition
259
+ *
260
+ * @example
261
+ * // (published = true) OR (authorId = 1 AND draft = true)
262
+ * DB.posts
263
+ * .where({ published: true })
264
+ * .orWhereGroup(qb => qb
265
+ * .where({ authorId: 1 })
266
+ * .where({ draft: true })
267
+ * )
268
+ * .findMany()
269
+ */
270
+ orWhereGroup(callback) {
271
+ return this.whereGroup(callback, "OR");
272
+ }
273
+ /**
274
+ * Adds a where condition to the query for the specified id.
275
+ * Uses the same AND composition as where() for consistency.
276
+ * @param id - The id to search for
277
+ */
278
+ withId(id) {
279
+ if (!id) {
280
+ throw new Error("Id is required");
281
+ }
282
+ if (!this.query.where || Object.keys(this.query.where).length === 0) {
283
+ this.query.where = { id };
284
+ } else {
285
+ const prevWhere = this.query.where;
286
+ this.query.where = { AND: [prevWhere, { id }] };
287
+ }
288
+ return this;
289
+ }
290
+ /**
291
+ * Adds an order by condition to the query
292
+ * @param orderBy - OrderBy object matching your Prisma model
293
+ */
294
+ order(orderBy) {
295
+ this.query.orderBy = orderBy;
296
+ return this;
297
+ }
298
+ /**
299
+ * Gets the last record sorted by the specified field
300
+ * @param key - Field to sort by (defaults to 'createdAt')
301
+ */
302
+ last(key = "createdAt") {
303
+ return this.order({ [key]: "desc" }).limit(1);
304
+ }
305
+ /**
306
+ * Gets the first record sorted by the specified field
307
+ * @param key - Field to sort by (defaults to 'createdAt')
308
+ */
309
+ first(key = "createdAt") {
310
+ return this.order({ [key]: "asc" }).limit(1);
311
+ }
312
+ /**
313
+ * Sets a limit on the number of records to retrieve
314
+ * @param limit - Maximum number of records
315
+ */
316
+ limit(limit) {
317
+ this.query.take = limit;
318
+ return this;
319
+ }
320
+ /**
321
+ * Sets distinct fields for the query
322
+ * @param distinct - Fields to be distinct
323
+ */
324
+ distinct(distinct) {
325
+ this.query.distinct = distinct;
326
+ return this;
327
+ }
328
+ /**
329
+ * Selects specific fields to retrieve
330
+ * @param fields - Select object matching your Prisma model
331
+ */
332
+ select(fields) {
333
+ this.query.select = fields;
334
+ return this;
335
+ }
336
+ /**
337
+ * Selects only the specified field and returns its value
338
+ * @param field - Field name to retrieve
339
+ */
340
+ async only(field) {
341
+ this.query.select = { [field]: true };
342
+ const result = await this.model.findFirst(this.query);
343
+ if (!result) {
344
+ return null;
345
+ }
346
+ return result[field];
347
+ }
348
+ /**
349
+ * Returns the current query object
350
+ */
351
+ getQuery() {
352
+ return this.query;
353
+ }
354
+ include(relation, callback) {
355
+ let relationQuery = true;
356
+ if (callback) {
357
+ const builder = modelRegistry.create(relation) ?? new _FlareBuilder(null);
358
+ callback(builder);
359
+ relationQuery = builder.getQuery();
360
+ if (Object.keys(relationQuery).length === 0) {
361
+ relationQuery = true;
362
+ }
363
+ }
364
+ this.query.include = {
365
+ ...this.query.include,
366
+ [relation]: relationQuery
367
+ };
368
+ return this;
369
+ }
370
+ /**
371
+ * Groups results by specified fields
372
+ * @param groupBy - Fields to group by
373
+ */
374
+ groupBy(groupBy) {
375
+ this.query.by = groupBy;
376
+ return this;
377
+ }
378
+ /**
379
+ * Adds a having condition to the query
380
+ * @param condition - Having condition
381
+ */
382
+ having(condition) {
383
+ this.query.having = condition;
384
+ return this;
385
+ }
386
+ /**
387
+ * Skips the specified number of records
388
+ * @param offset - Number of records to skip
389
+ */
390
+ skip(offset) {
391
+ this.query.skip = offset;
392
+ return this;
393
+ }
394
+ /**
395
+ * Checks if any record exists matching the current query
396
+ * @param existenceKey - Key to check for existence (defaults to 'id')
397
+ */
398
+ async exists(existenceKey = "id") {
399
+ const result = await this.model.findFirst({
400
+ where: this.query.where,
401
+ select: { [existenceKey]: true }
402
+ });
403
+ return Boolean(result);
404
+ }
405
+ /**
406
+ * Paginates the results
407
+ * @param page - Page number (1-based)
408
+ * @param perPage - Number of records per page
409
+ */
410
+ async paginate(page = 1, perPage = 15) {
411
+ const skip = (page - 1) * perPage;
412
+ const take = perPage;
413
+ this.query.skip = skip;
414
+ this.query.take = take;
415
+ const [data, total] = await Promise.all([
416
+ this.model.findMany(this.query),
417
+ this.model.count({ where: this.query.where })
418
+ ]);
419
+ const lastPage = Math.ceil(total / perPage);
420
+ return {
421
+ data,
422
+ meta: {
423
+ total,
424
+ lastPage,
425
+ currentPage: page,
426
+ perPage,
427
+ prev: page > 1 ? page - 1 : null,
428
+ next: page < lastPage ? page + 1 : null
429
+ }
430
+ };
431
+ }
432
+ /**
433
+ * Conditionally executes a callback on the query builder
434
+ * @param condition - Boolean or function returning boolean
435
+ * @param callback - Function to execute if condition is true
436
+ */
437
+ when(condition, callback) {
438
+ const isTrue = typeof condition === "function" ? condition() : condition;
439
+ if (isTrue) {
440
+ callback(this);
441
+ }
442
+ return this;
443
+ }
444
+ /**
445
+ * Processes results in chunks to avoid memory issues
446
+ * @param size - Size of each chunk
447
+ * @param callback - Function to process each chunk
448
+ */
449
+ async chunk(size, callback) {
450
+ let page = 1;
451
+ let hasMore = true;
452
+ const originalSkip = this.query.skip;
453
+ const originalTake = this.query.take;
454
+ while (hasMore) {
455
+ this.query.skip = (page - 1) * size;
456
+ this.query.take = size;
457
+ const results = await this.model.findMany(this.query);
458
+ if (results.length > 0) {
459
+ await callback(results);
460
+ page++;
461
+ if (results.length < size) {
462
+ hasMore = false;
463
+ }
464
+ } else {
465
+ hasMore = false;
466
+ }
467
+ }
468
+ this.query.skip = originalSkip;
469
+ this.query.take = originalTake;
470
+ }
471
+ /**
472
+ * Clones the current query builder instance.
473
+ * Uses structuredClone for proper handling of Date, BigInt, etc.
474
+ */
475
+ clone() {
476
+ const queryCopy = deepClone(this.query);
477
+ return new _FlareBuilder(this.model, queryCopy);
478
+ }
479
+ /**
480
+ * Finds the first record matching the query or throws an error if none found
481
+ * Throws a Prisma NotFoundError if no record matches the query
482
+ * @throws {Prisma.NotFoundError} When no record matches the query
483
+ * @returns Promise resolving to the found record
484
+ */
485
+ async findFirstOrThrow() {
486
+ return this.model.findFirstOrThrow(this.query);
487
+ }
488
+ /**
489
+ * Finds a unique record by primary key or throws an error if not found
490
+ * Requires a unique constraint (typically the id field)
491
+ * Throws a Prisma NotFoundError if no record matches
492
+ * @throws {Prisma.NotFoundError} When no record is found
493
+ * @returns Promise resolving to the found record
494
+ */
495
+ async findUniqueOrThrow() {
496
+ return this.model.findUniqueOrThrow(this.query);
497
+ }
498
+ /**
499
+ * Finds all records matching the query
500
+ * Respects all previously set query conditions (where, orderBy, take, skip, include, select, distinct)
501
+ * @returns Promise resolving to an array of records matching the query
502
+ */
503
+ async findMany() {
504
+ return this.model.findMany(this.query);
505
+ }
506
+ /**
507
+ * Finds the first record matching the query
508
+ * Returns null if no record matches. To throw an error instead, use findFirstOrThrow()
509
+ * @returns Promise resolving to the first matching record or null
510
+ */
511
+ async findFirst() {
512
+ return this.model.findFirst(this.query);
513
+ }
514
+ /**
515
+ * Finds a unique record by primary key
516
+ * Returns null if no record is found. To throw an error instead, use findUniqueOrThrow()
517
+ * Requires a unique constraint in the where condition (typically the id field)
518
+ * @returns Promise resolving to the found record or null
519
+ */
520
+ async findUnique() {
521
+ return this.model.findUnique(this.query);
522
+ }
523
+ /**
524
+ * Creates a new record with the provided data
525
+ * Any hooks registered for 'create' operations will be triggered
526
+ * @param data - Data matching your Prisma model's create input
527
+ * @returns Promise resolving to the newly created record
528
+ */
529
+ async create(data) {
530
+ const query = { ...this.query, data };
531
+ return this.model.create(query);
532
+ }
533
+ /**
534
+ * Creates multiple records in a single operation
535
+ * More efficient than creating records individually
536
+ * Any hooks registered for 'create' operations will be triggered for each record
537
+ * @param data - Array of data objects matching your Prisma model's create input
538
+ * @returns Promise resolving to the count of created records
539
+ */
540
+ async createMany(data) {
541
+ const query = { ...this.query, data };
542
+ return this.model.createMany(query);
543
+ }
544
+ /**
545
+ * Deletes a single record matching the current query conditions
546
+ * Requires at least one unique constraint in the where condition (typically id)
547
+ * Any hooks registered for 'delete' operations will be triggered
548
+ * @param args - Optional additional delete arguments to override query conditions
549
+ * @returns Promise resolving to the deleted record
550
+ */
551
+ async delete(args) {
552
+ const query = args ? { ...this.query, ...args } : this.query;
553
+ return this.model.delete(query);
554
+ }
555
+ /**
556
+ * Deletes multiple records matching the current query conditions
557
+ * More efficient than deleting records individually
558
+ * Any hooks registered for 'delete' operations will be triggered for each record
559
+ * @param args - Optional additional delete arguments to override query conditions
560
+ * @returns Promise resolving to the count of deleted records
561
+ */
562
+ async deleteMany(args) {
563
+ const query = args ? { ...this.query, ...args } : this.query;
564
+ return this.model.deleteMany(query);
565
+ }
566
+ /**
567
+ * Updates a single record matching the current query conditions
568
+ * Requires at least one unique constraint in the where condition (typically id)
569
+ * Any hooks registered for 'update' operations will be triggered
570
+ * @param data - Data to update, matching your Prisma model's update input
571
+ * @returns Promise resolving to the updated record
572
+ */
573
+ async update(data) {
574
+ const query = { ...this.query, data };
575
+ return this.model.update(query);
576
+ }
577
+ /**
578
+ * Updates multiple records matching the current query conditions
579
+ * More efficient than updating records individually
580
+ * Any hooks registered for 'update' operations will be triggered for each record
581
+ * @param data - Data to update, matching your Prisma model's update input
582
+ * @returns Promise resolving to the count of updated records
583
+ */
584
+ async updateMany(data) {
585
+ const query = { ...this.query, data };
586
+ return this.model.updateMany(query);
587
+ }
588
+ /**
589
+ * Updates a record if it exists, otherwise creates a new record
590
+ * The record is uniquely identified by the where condition (typically id)
591
+ * Any hooks registered for 'update' or 'create' operations will be triggered accordingly
592
+ * @param args - Optional upsert arguments including where, update, and create data
593
+ * @returns Promise resolving to the upserted record
594
+ */
595
+ async upsert(args) {
596
+ const query = args ? { ...this.query, ...args } : this.query;
597
+ return this.model.upsert(query);
598
+ }
599
+ /**
600
+ * Counts the number of records matching the query
601
+ */
602
+ async count() {
603
+ return this.model.count(this.query);
604
+ }
605
+ /**
606
+ * Sums the specified numeric field
607
+ * @param field - Field name to sum
608
+ */
609
+ async sum(field) {
610
+ const result = await this.model.aggregate({
611
+ _sum: { [field]: true },
612
+ where: this.query.where
613
+ });
614
+ return result._sum[field];
615
+ }
616
+ /**
617
+ * Calculates the average of the specified numeric field
618
+ * @param field - Field name to average
619
+ */
620
+ async avg(field) {
621
+ const result = await this.model.aggregate({
622
+ _avg: { [field]: true },
623
+ where: this.query.where
624
+ });
625
+ return result._avg[field];
626
+ }
627
+ /**
628
+ * Finds the minimum value of the specified field
629
+ * @param field - Field name to find minimum
630
+ */
631
+ async min(field) {
632
+ const result = await this.model.aggregate({
633
+ _min: { [field]: true },
634
+ where: this.query.where
635
+ });
636
+ return result._min[field];
637
+ }
638
+ /**
639
+ * Finds the maximum value of the specified field
640
+ * @param field - Field name to find maximum
641
+ */
642
+ async max(field) {
643
+ const result = await this.model.aggregate({
644
+ _max: { [field]: true },
645
+ where: this.query.where
646
+ });
647
+ return result._max[field];
648
+ }
649
+ /**
650
+ * Plucks the specified field from all results
651
+ * @param field - Field name to pluck
652
+ */
653
+ async pluck(field) {
654
+ this.query.select = { [field]: true };
655
+ const results = await this.model.findMany(this.query);
656
+ return results.map((result) => result[field]);
657
+ }
658
+ };
659
+
660
+ // src/core/extendedPrismaClient.ts
661
+ var FlareClient = class extends PrismaClient {
662
+ constructor(options = {}) {
663
+ super(options);
664
+ }
665
+ /**
666
+ * Creates a new FlareBuilder instance for the specified model.
667
+ * @param modelName - The name of the model.
668
+ * @returns FlareBuilder instance
669
+ */
670
+ from(modelName) {
671
+ const key = modelName.charAt(0).toLowerCase() + modelName.slice(1);
672
+ const model = this[key];
673
+ if (!model) {
674
+ throw new Error(`Model ${modelName} does not exist on PrismaClient.`);
675
+ }
676
+ return new FlareBuilder(model);
677
+ }
678
+ /**
679
+ * Executes a transaction with the FlareClient capabilities.
680
+ * @param fn - The transaction function.
681
+ * @param options - Transaction options.
682
+ * @returns The result of the transaction.
683
+ */
684
+ async transaction(fn, options) {
685
+ return super.$transaction(async (tx) => {
686
+ const extendedTx = new Proxy(tx, {
687
+ get: (target, prop, receiver) => {
688
+ if (prop === "from") {
689
+ return (modelName) => {
690
+ const key = modelName.charAt(0).toLowerCase() + modelName.slice(1);
691
+ const model = target[key];
692
+ if (!model) {
693
+ throw new Error(`Model ${modelName} does not exist on TransactionClient.`);
694
+ }
695
+ return new FlareBuilder(model);
696
+ };
697
+ }
698
+ return Reflect.get(target, prop, receiver);
699
+ }
700
+ });
701
+ return fn(extendedTx);
702
+ }, options);
703
+ }
704
+ };
705
+ var ExtendedPrismaClient = FlareClient;
706
+
707
+ // src/core/hookRegistry.ts
708
+ function valuesEqual(a, b) {
709
+ if (a == null && b == null) return true;
710
+ if (a == null || b == null) return false;
711
+ if (a === b) return true;
712
+ if (a instanceof Date && b instanceof Date) {
713
+ return a.getTime() === b.getTime();
714
+ }
715
+ if (typeof a.toDecimalPlaces === "function" && typeof b.toDecimalPlaces === "function") {
716
+ return a.toString() === b.toString();
717
+ }
718
+ if (typeof a === "bigint" && typeof b === "bigint") {
719
+ return a === b;
720
+ }
721
+ if (typeof a === "object" && typeof b === "object") {
722
+ try {
723
+ return JSON.stringify(a) === JSON.stringify(b);
724
+ } catch {
725
+ return false;
726
+ }
727
+ }
728
+ return a === b;
729
+ }
730
+ var DEFAULT_CONFIG = {
731
+ enableColumnHooks: true,
732
+ maxRefetch: 1e3,
733
+ warnOnSkip: true
734
+ };
735
+ var HookRegistry = class {
736
+ constructor() {
737
+ this.hooks = {
738
+ before: {},
739
+ after: {}
740
+ };
741
+ this.columnHooks = {
742
+ afterChange: {}
743
+ };
744
+ this.fieldCache = {};
745
+ this.modelsWithColumnHooks = /* @__PURE__ */ new Set();
746
+ this.config = { ...DEFAULT_CONFIG };
747
+ }
748
+ /**
749
+ * Configure the hook system.
750
+ * @param config - Partial configuration to merge with defaults
751
+ *
752
+ * @example
753
+ * // Disable column hooks globally for performance
754
+ * hookRegistry.configure({ enableColumnHooks: false });
755
+ *
756
+ * @example
757
+ * // Increase maxRefetch limit
758
+ * hookRegistry.configure({ maxRefetch: 5000 });
759
+ *
760
+ * @example
761
+ * // Disable limit entirely (use with caution)
762
+ * hookRegistry.configure({ maxRefetch: Infinity });
763
+ */
764
+ configure(config) {
765
+ this.config = { ...this.config, ...config };
766
+ }
767
+ /**
768
+ * Get current configuration.
769
+ */
770
+ getConfig() {
771
+ return this.config;
772
+ }
773
+ addHook(model, action, timing, fn) {
774
+ const key = `${model}:${action}`;
775
+ if (!this.hooks[timing][key]) {
776
+ this.hooks[timing][key] = [];
777
+ }
778
+ this.hooks[timing][key].push(fn);
779
+ }
780
+ addColumnHook(model, column, fn) {
781
+ const key = `${model}:${column}`;
782
+ if (!this.columnHooks.afterChange[key]) {
783
+ this.columnHooks.afterChange[key] = [];
784
+ }
785
+ this.columnHooks.afterChange[key].push(fn);
786
+ this.modelsWithColumnHooks.add(model);
787
+ }
788
+ async runHooks(timing, model, action, args, prisma) {
789
+ const key = `${model}:${action}`;
790
+ const hooks = this.hooks[timing]?.[key] ?? [];
791
+ if (timing === "after") {
792
+ await Promise.all(hooks.map((hook) => hook(...args, prisma)));
793
+ } else {
794
+ for (const hook of hooks) {
795
+ await hook(...args, prisma);
796
+ }
797
+ }
798
+ }
799
+ async runColumnHooks(model, newData, prevData, prisma) {
800
+ const promises = [];
801
+ for (const column in newData) {
802
+ const key = `${model}:${column}`;
803
+ const hooks = this.columnHooks.afterChange[key];
804
+ if (hooks && !valuesEqual(newData[column], prevData[column])) {
805
+ for (const hook of hooks) {
806
+ promises.push(hook(prevData[column], newData[column], newData, prisma));
807
+ }
808
+ }
809
+ }
810
+ await Promise.all(promises);
811
+ }
812
+ hasColumnHooks(model) {
813
+ return this.modelsWithColumnHooks.has(model);
814
+ }
815
+ /**
816
+ * Check if column hooks should run for an operation.
817
+ * Takes into account global config, record count limits, and per-call options.
818
+ *
819
+ * @param model - The model name
820
+ * @param recordCount - Number of records affected (for maxRefetch check)
821
+ * @param args - The operation args (to check for __flare skip option)
822
+ * @returns Whether column hooks should execute
823
+ */
824
+ shouldRunColumnHooks(model, recordCount, args) {
825
+ if (args?.__flare?.skipColumnHooks === true) {
826
+ return false;
827
+ }
828
+ if (!this.config.enableColumnHooks) {
829
+ return false;
830
+ }
831
+ if (!this.modelsWithColumnHooks.has(model)) {
832
+ return false;
833
+ }
834
+ if (this.config.maxRefetch > 0 && recordCount > this.config.maxRefetch) {
835
+ if (this.config.warnOnSkip) {
836
+ console.warn(
837
+ `[prisma-flare] Skipping column hooks for ${model}: ${recordCount} records exceeds maxRefetch limit of ${this.config.maxRefetch}. Configure via hookRegistry.configure({ maxRefetch: ... })`
838
+ );
839
+ }
840
+ return false;
841
+ }
842
+ return true;
843
+ }
844
+ getRelevantFields(model) {
845
+ if (this.fieldCache[model]) {
846
+ return this.fieldCache[model];
847
+ }
848
+ const fields = /* @__PURE__ */ new Set();
849
+ for (const key of Object.keys(this.columnHooks.afterChange)) {
850
+ if (key.startsWith(`${model}:`)) {
851
+ const [, column] = key.split(":");
852
+ fields.add(column);
853
+ }
854
+ }
855
+ fields.add("id");
856
+ const result = Array.from(fields).reduce((acc, field) => {
857
+ acc[field] = true;
858
+ return acc;
859
+ }, {});
860
+ this.fieldCache[model] = result;
861
+ return result;
862
+ }
863
+ /**
864
+ * Clear all registered hooks (useful for testing)
865
+ */
866
+ clearAll() {
867
+ this.hooks.before = {};
868
+ this.hooks.after = {};
869
+ this.columnHooks.afterChange = {};
870
+ this.fieldCache = {};
871
+ this.modelsWithColumnHooks.clear();
872
+ this.config = { ...DEFAULT_CONFIG };
873
+ }
874
+ };
875
+ var hookRegistry = new HookRegistry();
876
+ var hookRegistry_default = hookRegistry;
877
+
878
+ // src/core/hooks.ts
879
+ function normalizeModelName(model) {
880
+ return model.toLowerCase();
881
+ }
882
+ function beforeCreate(model, callback) {
883
+ hookRegistry_default.addHook(normalizeModelName(model), "create", "before", callback);
884
+ }
885
+ function beforeDelete(model, callback) {
886
+ hookRegistry_default.addHook(normalizeModelName(model), "delete", "before", callback);
887
+ }
888
+ function afterCreate(model, callback) {
889
+ hookRegistry_default.addHook(normalizeModelName(model), "create", "after", callback);
890
+ }
891
+ function afterDelete(model, callback) {
892
+ hookRegistry_default.addHook(normalizeModelName(model), "delete", "after", callback);
893
+ }
894
+ function beforeUpdate(model, callback) {
895
+ hookRegistry_default.addHook(normalizeModelName(model), "update", "before", callback);
896
+ }
897
+ function afterUpdate(model, callback) {
898
+ hookRegistry_default.addHook(normalizeModelName(model), "update", "after", callback);
899
+ }
900
+ function afterChange(model, column, callback) {
901
+ hookRegistry_default.addColumnHook(normalizeModelName(model), column, callback);
902
+ }
903
+ function afterUpsert(model, callback) {
904
+ hookRegistry_default.addHook(normalizeModelName(model), "upsert", "after", callback);
905
+ }
906
+
907
+ // src/core/hookMiddleware.ts
908
+ import { Prisma } from "@prisma/client";
909
+
910
+ // src/cli/config.ts
911
+ import * as fs from "fs";
912
+ import * as path from "path";
913
+ function findProjectRoot(currentDir) {
914
+ if (fs.existsSync(path.join(currentDir, "package.json"))) {
915
+ return currentDir;
916
+ }
917
+ const parentDir = path.dirname(currentDir);
918
+ if (parentDir === currentDir) {
919
+ throw new Error("Could not find package.json");
920
+ }
921
+ return findProjectRoot(parentDir);
922
+ }
923
+ function loadConfig(rootDir) {
924
+ const projectRoot = rootDir || findProjectRoot(process.cwd());
925
+ const configPath = path.join(projectRoot, "prisma-flare.config.json");
926
+ let config = {
927
+ modelsPath: "prisma/models",
928
+ dbPath: "prisma/db",
929
+ callbacksPath: "prisma/callbacks"
930
+ };
931
+ if (fs.existsSync(configPath)) {
932
+ try {
933
+ const configFile = fs.readFileSync(configPath, "utf-8");
934
+ const userConfig = JSON.parse(configFile);
935
+ config = { ...config, ...userConfig };
936
+ } catch {
937
+ console.warn("\u26A0\uFE0F Could not read prisma-flare.config.json, using defaults.");
938
+ }
939
+ }
940
+ return {
941
+ ...config
942
+ };
943
+ }
944
+
945
+ // src/core/hookMiddleware.ts
946
+ import fs2 from "fs";
947
+ import path2 from "path";
948
+ function supportsTypeScriptImports() {
949
+ if (process.env.TS_NODE || /* @__PURE__ */ Symbol.for("ts-node.register.instance") in process) {
950
+ return true;
951
+ }
952
+ if (process.env.TSX) {
953
+ return true;
954
+ }
955
+ if (typeof globalThis.Bun !== "undefined") {
956
+ return true;
957
+ }
958
+ if (process.env.VITEST) {
959
+ return true;
960
+ }
961
+ return false;
962
+ }
963
+ async function loadCallbacks(callbacksDir) {
964
+ if (!callbacksDir) {
965
+ callbacksDir = path2.join(process.cwd(), "prisma", "callbacks");
966
+ }
967
+ if (!fs2.existsSync(callbacksDir)) {
968
+ console.warn(`Callbacks directory not found: ${callbacksDir}`);
969
+ return;
970
+ }
971
+ const canImportTs = supportsTypeScriptImports();
972
+ const files = fs2.readdirSync(callbacksDir);
973
+ for (const file of files) {
974
+ const filePath = path2.join(callbacksDir, file);
975
+ if (file.endsWith(".js")) {
976
+ await import(filePath);
977
+ } else if (file.endsWith(".ts") && canImportTs) {
978
+ await import(filePath);
979
+ } else if (file.endsWith(".ts") && !canImportTs) {
980
+ console.warn(
981
+ `Skipping TypeScript callback file: ${file}. TypeScript imports require ts-node, tsx, or Bun. Compile to JavaScript for production use.`
982
+ );
983
+ }
984
+ }
985
+ }
986
+ async function fetchAffectedRecords(db, model, where, fields) {
987
+ const key = model.charAt(0).toLowerCase() + model.slice(1);
988
+ const delegate = db[key];
989
+ const select = fields ? { ...fields, id: true } : void 0;
990
+ const records = await delegate.findMany({
991
+ where,
992
+ ...select && { select }
993
+ });
994
+ return records;
995
+ }
996
+ async function executeHookLogic(prisma, model, action, args, next) {
997
+ if (!model) {
998
+ return next();
999
+ }
1000
+ let flareOptions = args?.__flare;
1001
+ if (args?.__flare) {
1002
+ delete args.__flare;
1003
+ }
1004
+ if (args?.data?.__flare) {
1005
+ flareOptions = args.data.__flare;
1006
+ delete args.data.__flare;
1007
+ }
1008
+ const modelName = model.toLowerCase();
1009
+ const hasColumnHooks = hookRegistry_default.hasColumnHooks(modelName);
1010
+ let prevData = [];
1011
+ let fields;
1012
+ let shouldRunColumnHooks = false;
1013
+ const isUpdateAction = action === "update" || action === "updateMany";
1014
+ if (hasColumnHooks && isUpdateAction) {
1015
+ fields = hookRegistry_default.getRelevantFields(modelName);
1016
+ prevData = await fetchAffectedRecords(prisma, modelName, args.where, fields);
1017
+ shouldRunColumnHooks = hookRegistry_default.shouldRunColumnHooks(modelName, prevData.length, { __flare: flareOptions });
1018
+ }
1019
+ await hookRegistry_default.runHooks("before", modelName, action, [args], prisma);
1020
+ const result = await next();
1021
+ if (shouldRunColumnHooks && prevData.length > 0) {
1022
+ let newData = [];
1023
+ const ids = prevData.map((r) => r.id);
1024
+ newData = await fetchAffectedRecords(prisma, modelName, { id: { in: ids } }, fields);
1025
+ for (let i = 0; i < prevData.length; i++) {
1026
+ const prevRecord = prevData[i];
1027
+ const newRecord = newData.find((record) => record.id === prevRecord.id);
1028
+ if (newRecord) {
1029
+ hookRegistry_default.runColumnHooks(modelName, newRecord, prevRecord, prisma).catch((error) => {
1030
+ console.error("Column hook error:", error);
1031
+ });
1032
+ }
1033
+ }
1034
+ }
1035
+ hookRegistry_default.runHooks("after", modelName, action, [args, result], prisma).catch((error) => {
1036
+ console.error("After hook error:", error);
1037
+ });
1038
+ return result;
1039
+ }
1040
+ function supportsPrisma6Middleware(prisma) {
1041
+ return typeof prisma.$use === "function";
1042
+ }
1043
+ function createHooksExtension(basePrisma) {
1044
+ return Prisma.defineExtension({
1045
+ name: "prisma-flare-hooks",
1046
+ query: {
1047
+ $allModels: {
1048
+ async $allOperations({ model, operation, args, query }) {
1049
+ return executeHookLogic(
1050
+ basePrisma,
1051
+ model,
1052
+ operation,
1053
+ args,
1054
+ () => query(args)
1055
+ );
1056
+ }
1057
+ }
1058
+ }
1059
+ });
1060
+ }
1061
+ function registerHooksLegacy(prisma) {
1062
+ prisma.$use(async (params, next) => {
1063
+ const { model, action, args } = params;
1064
+ return executeHookLogic(prisma, model, action, args, () => next(params));
1065
+ });
1066
+ }
1067
+ async function registerHooks(prisma) {
1068
+ let client;
1069
+ if (supportsPrisma6Middleware(prisma)) {
1070
+ registerHooksLegacy(prisma);
1071
+ client = prisma;
1072
+ } else {
1073
+ const extension = createHooksExtension(prisma);
1074
+ client = prisma.$extends(extension);
1075
+ }
1076
+ try {
1077
+ const config = loadConfig();
1078
+ const projectRoot = findProjectRoot(process.cwd());
1079
+ const callbacksPath = path2.join(projectRoot, config.callbacksPath);
1080
+ await loadCallbacks(callbacksPath);
1081
+ } catch {
1082
+ }
1083
+ return client;
1084
+ }
1085
+
1086
+ // src/core/adapters/postgres.ts
1087
+ var PostgresAdapter = {
1088
+ name: "postgres",
1089
+ matches(url) {
1090
+ return url.startsWith("postgresql://") || url.startsWith("postgres://");
1091
+ },
1092
+ async create(url) {
1093
+ const config = parseDatabaseUrl(url);
1094
+ const { Client } = await import("pg");
1095
+ const client = new Client({
1096
+ user: config.user,
1097
+ password: config.password,
1098
+ host: config.host,
1099
+ port: config.port,
1100
+ database: "postgres"
1101
+ });
1102
+ try {
1103
+ await client.connect();
1104
+ const checkRes = await client.query(
1105
+ `SELECT 1 FROM pg_database WHERE datname = $1`,
1106
+ [config.database]
1107
+ );
1108
+ if (checkRes.rowCount === 0) {
1109
+ await client.query(`CREATE DATABASE "${config.database}"`);
1110
+ console.log(`\u2705 Database "${config.database}" created successfully.`);
1111
+ } else {
1112
+ console.log(`\u26A0\uFE0F Database "${config.database}" already exists.`);
1113
+ }
1114
+ } catch (error) {
1115
+ console.error("\u274C Error creating database:", error);
1116
+ throw error;
1117
+ } finally {
1118
+ await client.end();
1119
+ }
1120
+ },
1121
+ async drop(url) {
1122
+ const config = parseDatabaseUrl(url);
1123
+ const { Client } = await import("pg");
1124
+ const client = new Client({
1125
+ user: config.user,
1126
+ password: config.password,
1127
+ host: config.host,
1128
+ port: config.port,
1129
+ database: "postgres"
1130
+ });
1131
+ try {
1132
+ await client.connect();
1133
+ await client.query(
1134
+ `SELECT pg_terminate_backend(pg_stat_activity.pid)
1135
+ FROM pg_stat_activity
1136
+ WHERE pg_stat_activity.datname = $1
1137
+ AND pid <> pg_backend_pid()`,
1138
+ [config.database]
1139
+ );
1140
+ await client.query(`DROP DATABASE IF EXISTS "${config.database}"`);
1141
+ console.log(`\u2705 Database "${config.database}" dropped successfully.`);
1142
+ } catch (error) {
1143
+ console.error("\u274C Error dropping database:", error);
1144
+ throw error;
1145
+ } finally {
1146
+ await client.end();
1147
+ }
1148
+ }
1149
+ };
1150
+ function parseDatabaseUrl(url) {
1151
+ const regex = /postgres(?:ql)?:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/([^?]+)(\?.*)?/;
1152
+ const match = url.match(regex);
1153
+ if (!match) {
1154
+ throw new Error("Invalid PostgreSQL connection string");
1155
+ }
1156
+ return {
1157
+ user: decodeURIComponent(match[1]),
1158
+ password: decodeURIComponent(match[2]),
1159
+ host: match[3],
1160
+ port: parseInt(match[4], 10),
1161
+ database: match[5]
1162
+ };
1163
+ }
1164
+
1165
+ // src/core/adapters/sqlite.ts
1166
+ import * as fs3 from "fs";
1167
+ import * as path3 from "path";
1168
+ var SqliteAdapter = {
1169
+ name: "sqlite",
1170
+ matches(url) {
1171
+ return url.startsWith("file:");
1172
+ },
1173
+ async create(url) {
1174
+ const filePath = parseSqliteUrl(url);
1175
+ const dir = path3.dirname(filePath);
1176
+ try {
1177
+ if (!fs3.existsSync(dir)) {
1178
+ fs3.mkdirSync(dir, { recursive: true });
1179
+ }
1180
+ if (!fs3.existsSync(filePath)) {
1181
+ fs3.writeFileSync(filePath, "");
1182
+ console.log(`\u2705 SQLite database created at "${filePath}"`);
1183
+ } else {
1184
+ console.log(`\u26A0\uFE0F SQLite database already exists at "${filePath}"`);
1185
+ }
1186
+ } catch (error) {
1187
+ console.error("\u274C Error creating SQLite database:", error);
1188
+ throw error;
1189
+ }
1190
+ },
1191
+ async drop(url) {
1192
+ const filePath = parseSqliteUrl(url);
1193
+ try {
1194
+ if (fs3.existsSync(filePath)) {
1195
+ fs3.unlinkSync(filePath);
1196
+ console.log(`\u2705 SQLite database at "${filePath}" dropped successfully.`);
1197
+ } else {
1198
+ console.log(`\u26A0\uFE0F SQLite database does not exist at "${filePath}"`);
1199
+ }
1200
+ if (fs3.existsSync(`${filePath}-journal`)) {
1201
+ fs3.unlinkSync(`${filePath}-journal`);
1202
+ }
1203
+ if (fs3.existsSync(`${filePath}-wal`)) {
1204
+ fs3.unlinkSync(`${filePath}-wal`);
1205
+ }
1206
+ if (fs3.existsSync(`${filePath}-shm`)) {
1207
+ fs3.unlinkSync(`${filePath}-shm`);
1208
+ }
1209
+ } catch (error) {
1210
+ console.error("\u274C Error dropping SQLite database:", error);
1211
+ throw error;
1212
+ }
1213
+ }
1214
+ };
1215
+ function parseSqliteUrl(url) {
1216
+ let cleanPath = url.replace(/^file:/, "");
1217
+ if (!path3.isAbsolute(cleanPath)) {
1218
+ cleanPath = path3.resolve(process.cwd(), cleanPath);
1219
+ }
1220
+ return cleanPath;
1221
+ }
1222
+
1223
+ // src/core/adapters/index.ts
1224
+ var AdapterRegistry = class {
1225
+ constructor() {
1226
+ this.adapters = [];
1227
+ }
1228
+ register(adapter) {
1229
+ this.adapters.push(adapter);
1230
+ }
1231
+ getAdapter(url) {
1232
+ const adapter = this.adapters.find((a) => a.matches(url));
1233
+ if (!adapter) {
1234
+ throw new Error(`No database adapter found for URL: ${url}`);
1235
+ }
1236
+ return adapter;
1237
+ }
1238
+ };
1239
+ var registry = new AdapterRegistry();
1240
+ registry.register(PostgresAdapter);
1241
+ registry.register(SqliteAdapter);
1242
+ export {
1243
+ ExtendedPrismaClient,
1244
+ FlareBuilder,
1245
+ FlareClient,
1246
+ afterChange,
1247
+ afterCreate,
1248
+ afterDelete,
1249
+ afterUpdate,
1250
+ afterUpsert,
1251
+ beforeCreate,
1252
+ beforeDelete,
1253
+ beforeUpdate,
1254
+ createHooksExtension,
1255
+ registry as dbAdapterRegistry,
1256
+ hookRegistry_default as hookRegistry,
1257
+ loadCallbacks,
1258
+ modelRegistry,
1259
+ registerHooks,
1260
+ registerHooksLegacy
1261
+ };