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.
- package/dist/cli/db-create.cjs +240 -0
- package/dist/cli/db-create.d.cts +1 -0
- package/dist/cli/db-create.d.ts +1 -0
- package/dist/cli/db-create.js +217 -0
- package/dist/cli/db-drop.cjs +263 -0
- package/dist/cli/db-drop.d.cts +1 -0
- package/dist/cli/db-drop.d.ts +1 -0
- package/dist/cli/db-drop.js +240 -0
- package/dist/cli/db-migrate.cjs +318 -0
- package/dist/cli/db-migrate.d.cts +1 -0
- package/dist/cli/db-migrate.d.ts +1 -0
- package/dist/cli/db-migrate.js +295 -0
- package/dist/cli/db-reset.cjs +110 -0
- package/dist/cli/db-reset.d.cts +1 -0
- package/dist/cli/db-reset.d.ts +1 -0
- package/dist/cli/db-reset.js +87 -0
- package/dist/cli/db-seed.cjs +87 -0
- package/dist/cli/db-seed.d.cts +1 -0
- package/dist/cli/db-seed.d.ts +1 -0
- package/dist/cli/db-seed.js +64 -0
- package/dist/cli/index.cjs +352 -0
- package/dist/cli/index.d.cts +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +328 -0
- package/dist/core/flareBuilder.cjs +681 -0
- package/dist/core/flareBuilder.d.cts +402 -0
- package/dist/core/flareBuilder.d.ts +402 -0
- package/dist/core/flareBuilder.js +658 -0
- package/dist/core/hooks.cjs +243 -0
- package/dist/core/hooks.d.cts +13 -0
- package/dist/core/hooks.d.ts +13 -0
- package/dist/core/hooks.js +209 -0
- package/dist/generated.cjs +31 -0
- package/dist/generated.d.cts +4 -0
- package/dist/generated.d.ts +4 -0
- package/dist/generated.js +6 -0
- package/dist/index.cjs +1315 -0
- package/dist/index.d.cts +237 -0
- package/dist/index.d.ts +237 -0
- package/dist/index.js +1261 -0
- package/dist/prisma.types-nGNe1CG8.d.cts +201 -0
- package/dist/prisma.types-nGNe1CG8.d.ts +201 -0
- package/license.md +21 -0
- package/package.json +115 -0
- 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
|
+
};
|