query-builder-prisma 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +154 -2
- package/dist/index.cjs +636 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +481 -0
- package/dist/index.d.ts +481 -0
- package/dist/index.js +601 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
// src/core/conditions.ts
|
|
2
|
+
var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
3
|
+
function isSafeKey(key) {
|
|
4
|
+
return !DANGEROUS_KEYS.has(key);
|
|
5
|
+
}
|
|
6
|
+
function stripUndefined(obj) {
|
|
7
|
+
const result = {};
|
|
8
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
9
|
+
if (value !== void 0 && isSafeKey(key)) {
|
|
10
|
+
result[key] = value;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return result;
|
|
14
|
+
}
|
|
15
|
+
function deepStripUndefined(obj) {
|
|
16
|
+
const result = {};
|
|
17
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
18
|
+
if (value === void 0 || !isSafeKey(key)) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
result[key] = isDeepStrippable(value) ? deepStripUndefined(value) : Array.isArray(value) ? value.map((item) => isDeepStrippable(item) ? deepStripUndefined(item) : item) : value;
|
|
22
|
+
}
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
function isDeepStrippable(value) {
|
|
26
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
const proto = Object.getPrototypeOf(value);
|
|
30
|
+
return proto === Object.prototype || proto === null;
|
|
31
|
+
}
|
|
32
|
+
function isPlainObject(value) {
|
|
33
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
34
|
+
}
|
|
35
|
+
function expandDotPath(path, leaf) {
|
|
36
|
+
const keys = path.split(".").filter((key) => key.length > 0);
|
|
37
|
+
if (keys.length === 0 || keys.some((key) => !isSafeKey(key))) {
|
|
38
|
+
return {};
|
|
39
|
+
}
|
|
40
|
+
let current = leaf;
|
|
41
|
+
for (let i = keys.length - 1; i >= 0; i--) {
|
|
42
|
+
const key = keys[i];
|
|
43
|
+
if (key === void 0) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
current = { [key]: current };
|
|
47
|
+
}
|
|
48
|
+
return current;
|
|
49
|
+
}
|
|
50
|
+
function combineAnd(conditions, customWhere) {
|
|
51
|
+
const all = isPlainObject(customWhere) ? [customWhere, ...conditions] : [...conditions];
|
|
52
|
+
if (all.length === 0) {
|
|
53
|
+
return void 0;
|
|
54
|
+
}
|
|
55
|
+
if (all.length === 1) {
|
|
56
|
+
return all[0];
|
|
57
|
+
}
|
|
58
|
+
return { AND: all };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/core/pagination.ts
|
|
62
|
+
var DEFAULT_PAGE_SIZE = 10;
|
|
63
|
+
var MAX_PAGE_SIZE = 1e3;
|
|
64
|
+
var MAX_SKIP = 1e5;
|
|
65
|
+
function clampLimit(limit) {
|
|
66
|
+
if (!Number.isFinite(limit)) {
|
|
67
|
+
return DEFAULT_PAGE_SIZE;
|
|
68
|
+
}
|
|
69
|
+
return Math.min(Math.max(1, Math.floor(limit)), MAX_PAGE_SIZE);
|
|
70
|
+
}
|
|
71
|
+
function clampPage(page) {
|
|
72
|
+
if (!Number.isFinite(page)) {
|
|
73
|
+
return 1;
|
|
74
|
+
}
|
|
75
|
+
return Math.max(1, Math.floor(page));
|
|
76
|
+
}
|
|
77
|
+
function toOffsetArgs(page, limit) {
|
|
78
|
+
const safePage = clampPage(page);
|
|
79
|
+
const safeLimit = clampLimit(limit);
|
|
80
|
+
const skip = Math.min((safePage - 1) * safeLimit, MAX_SKIP);
|
|
81
|
+
return { take: safeLimit, skip, page: safePage, limit: safeLimit };
|
|
82
|
+
}
|
|
83
|
+
function buildPaginationMeta(total, page, limit) {
|
|
84
|
+
const safeLimit = clampLimit(limit);
|
|
85
|
+
const safePage = clampPage(page);
|
|
86
|
+
return {
|
|
87
|
+
page: safePage,
|
|
88
|
+
limit: safeLimit,
|
|
89
|
+
total,
|
|
90
|
+
totalPage: Math.ceil(total / safeLimit)
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function serializeCursorValue(value) {
|
|
94
|
+
return value instanceof Date ? value.toISOString() : String(value);
|
|
95
|
+
}
|
|
96
|
+
function toCursorArgs(plan) {
|
|
97
|
+
const sign = plan.direction === "backward" ? -1 : 1;
|
|
98
|
+
const take = sign * (clampLimit(plan.limit ?? DEFAULT_PAGE_SIZE) + 1);
|
|
99
|
+
if (plan.cursor === void 0) {
|
|
100
|
+
return { take };
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
take,
|
|
104
|
+
cursor: { [plan.cursorField]: plan.cursor },
|
|
105
|
+
skip: 1
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function buildCursorPage(rows, info) {
|
|
109
|
+
const { limit, direction, cursorField, hadCursor, exactFlags } = info;
|
|
110
|
+
const safeLimit = clampLimit(limit);
|
|
111
|
+
const hasExtra = rows.length > safeLimit;
|
|
112
|
+
const pageRows = hasExtra ? rows.slice(0, safeLimit) : rows;
|
|
113
|
+
const ordered = direction === "backward" ? [...pageRows].reverse() : pageRows;
|
|
114
|
+
const hasRows = rows.length > 0;
|
|
115
|
+
const hasNextPage = exactFlags?.hasNextPage ?? (direction === "forward" ? hasExtra : hadCursor && hasRows);
|
|
116
|
+
const hasPrevPage = exactFlags?.hasPrevPage ?? (direction === "backward" ? hasExtra : hadCursor && hasRows);
|
|
117
|
+
const first = ordered[0];
|
|
118
|
+
const last = ordered[ordered.length - 1];
|
|
119
|
+
return {
|
|
120
|
+
data: ordered,
|
|
121
|
+
meta: {
|
|
122
|
+
limit: safeLimit,
|
|
123
|
+
nextCursor: hasNextPage && last !== void 0 ? serializeCursorValue(last[cursorField]) : null,
|
|
124
|
+
prevCursor: hasPrevPage && first !== void 0 ? serializeCursorValue(first[cursorField]) : null,
|
|
125
|
+
hasNextPage,
|
|
126
|
+
hasPrevPage
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/dialects/capabilities.ts
|
|
132
|
+
var DIALECT_CAPABILITIES = {
|
|
133
|
+
postgresql: { caseInsensitiveMode: true, caseInsensitiveCollation: false },
|
|
134
|
+
cockroachdb: { caseInsensitiveMode: true, caseInsensitiveCollation: false },
|
|
135
|
+
mysql: { caseInsensitiveMode: false, caseInsensitiveCollation: true },
|
|
136
|
+
mariadb: { caseInsensitiveMode: false, caseInsensitiveCollation: true },
|
|
137
|
+
sqlite: { caseInsensitiveMode: false, caseInsensitiveCollation: false },
|
|
138
|
+
sqlserver: { caseInsensitiveMode: false, caseInsensitiveCollation: true },
|
|
139
|
+
mongodb: { caseInsensitiveMode: true, caseInsensitiveCollation: false },
|
|
140
|
+
/**
|
|
141
|
+
* Fallback used when no dialect is configured/detected. Only emits arguments
|
|
142
|
+
* that are valid on EVERY Prisma-supported database.
|
|
143
|
+
*/
|
|
144
|
+
universal: { caseInsensitiveMode: false, caseInsensitiveCollation: false }
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// src/dialects/detect.ts
|
|
148
|
+
function detectDialect() {
|
|
149
|
+
return void 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/dialects/registry.ts
|
|
153
|
+
var STATE_KEY = /* @__PURE__ */ Symbol.for("query-builder-prisma.dialect-state");
|
|
154
|
+
function state() {
|
|
155
|
+
const host = globalThis;
|
|
156
|
+
return host[STATE_KEY] ??= {
|
|
157
|
+
registry: new Map(Object.entries(DIALECT_CAPABILITIES)),
|
|
158
|
+
globalDialect: void 0,
|
|
159
|
+
detector: void 0
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function registerDialect(name, capabilities) {
|
|
163
|
+
state().registry.set(name, capabilities);
|
|
164
|
+
}
|
|
165
|
+
function configureQueryBuilder(options) {
|
|
166
|
+
state().globalDialect = options.dialect;
|
|
167
|
+
}
|
|
168
|
+
function setDialectDetector(detector) {
|
|
169
|
+
state().detector = detector;
|
|
170
|
+
}
|
|
171
|
+
function resolveDialect(explicit) {
|
|
172
|
+
const s = state();
|
|
173
|
+
const requested = explicit ?? s.globalDialect ?? s.detector?.() ?? detectDialect() ?? "universal";
|
|
174
|
+
const capabilities = s.registry.get(requested);
|
|
175
|
+
return capabilities ? { name: requested, capabilities } : { name: "universal", capabilities: DIALECT_CAPABILITIES.universal };
|
|
176
|
+
}
|
|
177
|
+
function resetDialectConfig() {
|
|
178
|
+
const s = state();
|
|
179
|
+
s.globalDialect = void 0;
|
|
180
|
+
s.detector = void 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// src/core/query-builder.ts
|
|
184
|
+
var NoResultError = class extends Error {
|
|
185
|
+
constructor() {
|
|
186
|
+
super("Query returned no rows");
|
|
187
|
+
this.name = "NoResultError";
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
var QueryBuilder = class {
|
|
191
|
+
delegate;
|
|
192
|
+
dialect;
|
|
193
|
+
strictNumbers;
|
|
194
|
+
andConditions = [];
|
|
195
|
+
customArgs = {};
|
|
196
|
+
/**
|
|
197
|
+
* The `initialArgs` seed `where` plus every `.custom({ where })`, AND-merged.
|
|
198
|
+
* Kept out of `customArgs` so a later `.custom({ where })` merges with the
|
|
199
|
+
* seed instead of replacing it (audit F2 — the seed carries safety filters).
|
|
200
|
+
*/
|
|
201
|
+
customWhere;
|
|
202
|
+
orderByArg;
|
|
203
|
+
selectArg;
|
|
204
|
+
includeArg;
|
|
205
|
+
page;
|
|
206
|
+
limit;
|
|
207
|
+
cursorActive = false;
|
|
208
|
+
cursor;
|
|
209
|
+
cursorLimit;
|
|
210
|
+
cursorDirection = "forward";
|
|
211
|
+
cursorField = "id";
|
|
212
|
+
cursorStrict = false;
|
|
213
|
+
/**
|
|
214
|
+
* @param delegate - The Prisma delegate to query (e.g. `prisma.user`).
|
|
215
|
+
* @param options - Optional seed args, a dialect override, or `strictNumbers`.
|
|
216
|
+
*/
|
|
217
|
+
constructor(delegate, options) {
|
|
218
|
+
this.delegate = delegate;
|
|
219
|
+
this.dialect = options?.dialect;
|
|
220
|
+
this.strictNumbers = options?.strictNumbers ?? false;
|
|
221
|
+
if (options?.initialArgs) {
|
|
222
|
+
const { where, ...rest } = options.initialArgs;
|
|
223
|
+
this.customArgs = { ...rest };
|
|
224
|
+
this.customWhere = where;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Case-insensitive `contains` across the provided string fields. No condition
|
|
229
|
+
* is added when `searchText` is empty or whitespace-only (audit G7).
|
|
230
|
+
*
|
|
231
|
+
* Dialect-aware: adds `mode: 'insensitive'` only where Prisma accepts it (see
|
|
232
|
+
* `caseInsensitiveMode` in `src/dialects/capabilities.ts`); everywhere else
|
|
233
|
+
* the bare `contains` relies on the database's collation, and where that is
|
|
234
|
+
* ASCII-only matching stays case-sensitive — Prisma offers no alternative
|
|
235
|
+
* there.
|
|
236
|
+
*/
|
|
237
|
+
search(params) {
|
|
238
|
+
const text = params.searchText?.trim();
|
|
239
|
+
const fields = params.fields;
|
|
240
|
+
if (text && fields.length > 0) {
|
|
241
|
+
const { capabilities } = resolveDialect(this.dialect);
|
|
242
|
+
this.andConditions.push({
|
|
243
|
+
OR: fields.map((field) => {
|
|
244
|
+
const match = { contains: text };
|
|
245
|
+
if (capabilities.caseInsensitiveMode) {
|
|
246
|
+
match.mode = "insensitive";
|
|
247
|
+
}
|
|
248
|
+
return { [field]: match };
|
|
249
|
+
})
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
return this;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Adds an equality/operator filter. Accepts a partial Prisma `where` object;
|
|
256
|
+
* `undefined` values are ignored so optional query params are safe to pass.
|
|
257
|
+
*/
|
|
258
|
+
filter(filters) {
|
|
259
|
+
const cleaned = stripUndefined(filters);
|
|
260
|
+
if (Object.keys(cleaned).length > 0) {
|
|
261
|
+
this.andConditions.push(cleaned);
|
|
262
|
+
}
|
|
263
|
+
return this;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Adds `gte`/`lte` range conditions. Keys may be dot-paths to reach relations
|
|
267
|
+
* or nested objects (e.g. `'avatar.size'`). Bounds left `undefined` are
|
|
268
|
+
* skipped; a field is only added when at least one bound is present.
|
|
269
|
+
*/
|
|
270
|
+
range(ranges) {
|
|
271
|
+
for (const [path, bound] of Object.entries(ranges)) {
|
|
272
|
+
const condition = {};
|
|
273
|
+
if (bound.from !== void 0) {
|
|
274
|
+
condition.gte = bound.from;
|
|
275
|
+
}
|
|
276
|
+
if (bound.to !== void 0) {
|
|
277
|
+
condition.lte = bound.to;
|
|
278
|
+
}
|
|
279
|
+
if (Object.keys(condition).length > 0) {
|
|
280
|
+
const expanded = expandDotPath(path, condition);
|
|
281
|
+
if (Object.keys(expanded).length > 0) {
|
|
282
|
+
this.andConditions.push(expanded);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return this;
|
|
287
|
+
}
|
|
288
|
+
/** Adds an arbitrary, fully-typed Prisma `where` condition. */
|
|
289
|
+
where(condition) {
|
|
290
|
+
const cleaned = stripUndefined(condition);
|
|
291
|
+
if (Object.keys(cleaned).length > 0) {
|
|
292
|
+
this.andConditions.push(cleaned);
|
|
293
|
+
}
|
|
294
|
+
return this;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Sets `orderBy` from a single key. No-op when `sortBy` is not provided.
|
|
298
|
+
* Dot paths expand like `.range()` (`'avatar.size'` → `{ avatar: { size } }`,
|
|
299
|
+
* audit F6). For multi-key or nested relation sorts use {@link orderBy}.
|
|
300
|
+
*/
|
|
301
|
+
sortBy(params) {
|
|
302
|
+
const { sortBy, sortOrder = "desc" } = params;
|
|
303
|
+
if (sortBy) {
|
|
304
|
+
const expanded = expandDotPath(sortBy, sortOrder);
|
|
305
|
+
if (Object.keys(expanded).length > 0) {
|
|
306
|
+
this.orderByArg = expanded;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return this;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Sets `orderBy` verbatim from the full Prisma shape: arrays for multi-key
|
|
313
|
+
* sorts, nested relation order objects. `.sortBy()` remains the single-key
|
|
314
|
+
* convenience — the latest of the two calls wins (audit G1).
|
|
315
|
+
*/
|
|
316
|
+
orderBy(order) {
|
|
317
|
+
this.orderByArg = order;
|
|
318
|
+
return this;
|
|
319
|
+
}
|
|
320
|
+
/** Configures `skip`/`take` from a 1-based page number and page size. */
|
|
321
|
+
paginate(params) {
|
|
322
|
+
this.assertPage(params.page);
|
|
323
|
+
this.assertLimit(params.limit);
|
|
324
|
+
this.page = params.page ?? 1;
|
|
325
|
+
this.limit = params.limit ?? DEFAULT_PAGE_SIZE;
|
|
326
|
+
return this;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Configures cursor-based ("seek") pagination. Use {@link executeWithCursor}
|
|
330
|
+
* to run the query and obtain `next`/`prev` cursors.
|
|
331
|
+
*
|
|
332
|
+
* - `cursor` is the value of `cursorField` from the edge row of the previous
|
|
333
|
+
* page (omit it for the first page). Strings, numbers and Dates are
|
|
334
|
+
* accepted; Dates serialize to ISO-8601 in the returned cursors (audit F4).
|
|
335
|
+
* - `direction` walks `'forward'` (next) or `'backward'` (previous).
|
|
336
|
+
* - `cursorField` is the unique, sequential column to seek on (default `id`).
|
|
337
|
+
* Pair it with a matching {@link sortBy} for stable ordering.
|
|
338
|
+
* - `strict` spends one extra 1-row probe query to make `hasNextPage` /
|
|
339
|
+
* `hasPrevPage` exact instead of optimistic (audit F10).
|
|
340
|
+
*
|
|
341
|
+
* Takes precedence over {@link paginate} when both are set.
|
|
342
|
+
*/
|
|
343
|
+
cursorPaginate(params) {
|
|
344
|
+
this.assertLimit(params.limit);
|
|
345
|
+
this.cursorActive = true;
|
|
346
|
+
this.cursor = params.cursor;
|
|
347
|
+
this.cursorLimit = params.limit;
|
|
348
|
+
this.cursorDirection = params.direction ?? "forward";
|
|
349
|
+
this.cursorField = params.cursorField ?? "id";
|
|
350
|
+
this.cursorStrict = params.strict ?? false;
|
|
351
|
+
return this;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Projects fields with Prisma `select`. The result type of `execute()` is
|
|
355
|
+
* narrowed to exactly the selected shape. Clears any active `.include()`.
|
|
356
|
+
*/
|
|
357
|
+
select(select) {
|
|
358
|
+
this.selectArg = select;
|
|
359
|
+
this.includeArg = void 0;
|
|
360
|
+
return this;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Eager-loads relations with Prisma `include`. The result type of `execute()`
|
|
364
|
+
* is widened with the included relations. Clears any active `.select()`.
|
|
365
|
+
*/
|
|
366
|
+
include(include) {
|
|
367
|
+
this.includeArg = include;
|
|
368
|
+
this.selectArg = void 0;
|
|
369
|
+
return this;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Merges raw `findMany` args, e.g. `cursor`, `distinct` or a custom `where`.
|
|
373
|
+
* Values that are `undefined` are stripped at any depth, so
|
|
374
|
+
* `custom({ distinct: undefined })` cannot overwrite an earlier value
|
|
375
|
+
* (audit F15). A custom `where` is AND-ed with the `initialArgs` seed and any
|
|
376
|
+
* previous custom `where` — it never replaces them (audit F2). Successive
|
|
377
|
+
* custom `where`s may nest `{ AND: [...] }` groups; Prisma treats that
|
|
378
|
+
* identically.
|
|
379
|
+
*/
|
|
380
|
+
custom(args) {
|
|
381
|
+
const { where, ...rest } = args;
|
|
382
|
+
this.customArgs = { ...this.customArgs, ...deepStripUndefined(rest) };
|
|
383
|
+
if (where !== void 0) {
|
|
384
|
+
const cleaned = isPlainObject(where) ? deepStripUndefined(where) : where;
|
|
385
|
+
const hasKeys = !isPlainObject(cleaned) || Object.keys(cleaned).length > 0;
|
|
386
|
+
if (hasKeys) {
|
|
387
|
+
this.customWhere = combineAnd([cleaned], this.customWhere);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return this;
|
|
391
|
+
}
|
|
392
|
+
/** The fully assembled `findMany` args. Useful for debugging or reuse. */
|
|
393
|
+
buildArgs() {
|
|
394
|
+
const args = { ...this.customArgs };
|
|
395
|
+
const where = this.buildWhere();
|
|
396
|
+
if (where) {
|
|
397
|
+
args.where = where;
|
|
398
|
+
}
|
|
399
|
+
if (this.orderByArg) {
|
|
400
|
+
args.orderBy = this.orderByArg;
|
|
401
|
+
}
|
|
402
|
+
if (this.selectArg) {
|
|
403
|
+
args.select = this.selectArg;
|
|
404
|
+
}
|
|
405
|
+
if (this.includeArg) {
|
|
406
|
+
args.include = this.includeArg;
|
|
407
|
+
}
|
|
408
|
+
if (this.cursorActive) {
|
|
409
|
+
const cursorArgs = toCursorArgs({
|
|
410
|
+
cursor: this.cursor,
|
|
411
|
+
limit: this.cursorLimit,
|
|
412
|
+
direction: this.cursorDirection,
|
|
413
|
+
cursorField: this.cursorField
|
|
414
|
+
});
|
|
415
|
+
args.take = cursorArgs.take;
|
|
416
|
+
if (cursorArgs.cursor) {
|
|
417
|
+
args.cursor = cursorArgs.cursor;
|
|
418
|
+
}
|
|
419
|
+
if (cursorArgs.skip !== void 0) {
|
|
420
|
+
args.skip = cursorArgs.skip;
|
|
421
|
+
}
|
|
422
|
+
} else if (this.limit !== void 0) {
|
|
423
|
+
const page = this.page ?? 1;
|
|
424
|
+
this.assertSkipCap(page, this.limit);
|
|
425
|
+
const offset = toOffsetArgs(page, this.limit);
|
|
426
|
+
args.take = offset.take;
|
|
427
|
+
args.skip = offset.skip;
|
|
428
|
+
}
|
|
429
|
+
return args;
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Executes the query and returns the rows, typed from `.select()`/`.include()`.
|
|
433
|
+
*
|
|
434
|
+
* Tip: for 100% Prisma-native inference you can always run the passthrough
|
|
435
|
+
* `prisma.user.findMany(qb.buildArgs())` — same args, Prisma's own overloads.
|
|
436
|
+
*/
|
|
437
|
+
async execute() {
|
|
438
|
+
const findMany = this.delegate.findMany.bind(this.delegate);
|
|
439
|
+
const data = await findMany(this.buildArgs());
|
|
440
|
+
return data;
|
|
441
|
+
}
|
|
442
|
+
/** Executes the query and returns the first row, or `null` when none matched. */
|
|
443
|
+
async first() {
|
|
444
|
+
const args = { ...this.buildArgs() };
|
|
445
|
+
args.take = 1;
|
|
446
|
+
delete args.skip;
|
|
447
|
+
delete args.cursor;
|
|
448
|
+
const findMany = this.delegate.findMany.bind(this.delegate);
|
|
449
|
+
const rows = await findMany(args);
|
|
450
|
+
const row = rows[0];
|
|
451
|
+
return row ?? null;
|
|
452
|
+
}
|
|
453
|
+
/** Like {@link first}, but throws {@link NoResultError} when nothing matched. */
|
|
454
|
+
async firstOrThrow() {
|
|
455
|
+
const row = await this.first();
|
|
456
|
+
if (row === null) {
|
|
457
|
+
throw new NoResultError();
|
|
458
|
+
}
|
|
459
|
+
return row;
|
|
460
|
+
}
|
|
461
|
+
/** Counts rows matching the current filters (ignores pagination/projection). */
|
|
462
|
+
async count() {
|
|
463
|
+
const where = this.buildWhere();
|
|
464
|
+
const countFn = this.delegate.count.bind(this.delegate);
|
|
465
|
+
return countFn(where ? { where } : {});
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Executes the query and returns the rows plus offset pagination metadata.
|
|
469
|
+
* When no pagination was configured, defaults to page 1, limit 10 so `meta`
|
|
470
|
+
* always describes the data actually returned. `count` and `findMany` run in
|
|
471
|
+
* parallel.
|
|
472
|
+
*
|
|
473
|
+
* Pass `{ includeTotal: false }` to skip the `COUNT` query entirely — `meta`
|
|
474
|
+
* then carries `total: null` / `totalPage: null` (audit G3).
|
|
475
|
+
*
|
|
476
|
+
* Throws when cursor pagination is configured — offset meta cannot describe a
|
|
477
|
+
* seek query, and the cursor over-fetch row would leak into `data` (audit F1).
|
|
478
|
+
* Use {@link executeWithCursor} instead.
|
|
479
|
+
*/
|
|
480
|
+
async executeWithMeta(opts) {
|
|
481
|
+
if (this.cursorActive) {
|
|
482
|
+
throw new Error(
|
|
483
|
+
"executeWithMeta() is offset-mode only: this builder has cursor pagination configured via cursorPaginate(). Use executeWithCursor() instead."
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
if (this.limit === void 0) {
|
|
487
|
+
this.paginate({ page: 1, limit: DEFAULT_PAGE_SIZE });
|
|
488
|
+
}
|
|
489
|
+
this.assertSkipCap(this.page ?? 1, this.limit ?? DEFAULT_PAGE_SIZE);
|
|
490
|
+
const offset = toOffsetArgs(this.page ?? 1, this.limit ?? DEFAULT_PAGE_SIZE);
|
|
491
|
+
if (opts?.includeTotal === false) {
|
|
492
|
+
const data2 = await this.execute();
|
|
493
|
+
return {
|
|
494
|
+
meta: { page: offset.page, limit: offset.limit, total: null, totalPage: null },
|
|
495
|
+
data: data2
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
const [total, data] = await Promise.all([this.count(), this.execute()]);
|
|
499
|
+
return {
|
|
500
|
+
meta: buildPaginationMeta(total, offset.page, offset.limit),
|
|
501
|
+
data
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Executes a cursor-paginated query (see {@link cursorPaginate}) and returns
|
|
506
|
+
* the page rows plus `next`/`prev` cursors. Rows are always returned in the
|
|
507
|
+
* configured sort order, regardless of pagination `direction`.
|
|
508
|
+
*
|
|
509
|
+
* Throws when a `.select()` projection drops `cursorField` — the page cursors
|
|
510
|
+
* are derived from that field and would come out as `undefined` (audit F5).
|
|
511
|
+
*/
|
|
512
|
+
async executeWithCursor() {
|
|
513
|
+
if (!this.cursorActive) {
|
|
514
|
+
this.cursorPaginate({});
|
|
515
|
+
}
|
|
516
|
+
this.assertCursorFieldProjected();
|
|
517
|
+
const limit = this.cursorLimit ?? DEFAULT_PAGE_SIZE;
|
|
518
|
+
const hadCursor = this.cursor !== void 0;
|
|
519
|
+
const findMany = this.delegate.findMany.bind(this.delegate);
|
|
520
|
+
const rows = await findMany(this.buildArgs());
|
|
521
|
+
let exactFlags;
|
|
522
|
+
if (this.cursorStrict && hadCursor) {
|
|
523
|
+
const probeArgs = {
|
|
524
|
+
take: this.cursorDirection === "forward" ? -1 : 1,
|
|
525
|
+
cursor: { [this.cursorField]: this.cursor },
|
|
526
|
+
skip: 1
|
|
527
|
+
};
|
|
528
|
+
const where = this.buildWhere();
|
|
529
|
+
if (where) {
|
|
530
|
+
probeArgs.where = where;
|
|
531
|
+
}
|
|
532
|
+
if (this.orderByArg) {
|
|
533
|
+
probeArgs.orderBy = this.orderByArg;
|
|
534
|
+
}
|
|
535
|
+
const probe = await findMany(probeArgs);
|
|
536
|
+
const hasEdgeRow = probe.length > 0;
|
|
537
|
+
exactFlags = this.cursorDirection === "forward" ? { hasPrevPage: hasEdgeRow } : { hasNextPage: hasEdgeRow };
|
|
538
|
+
}
|
|
539
|
+
const { data, meta } = buildCursorPage(rows, {
|
|
540
|
+
limit,
|
|
541
|
+
direction: this.cursorDirection,
|
|
542
|
+
cursorField: this.cursorField,
|
|
543
|
+
hadCursor,
|
|
544
|
+
exactFlags
|
|
545
|
+
});
|
|
546
|
+
return { meta, data };
|
|
547
|
+
}
|
|
548
|
+
/** strictNumbers: out-of-range limits throw instead of clamping (audit F12). */
|
|
549
|
+
assertLimit(limit) {
|
|
550
|
+
if (!this.strictNumbers || limit === void 0) {
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
if (!Number.isFinite(limit) || limit < 1 || limit > MAX_PAGE_SIZE) {
|
|
554
|
+
throw new Error(`strictNumbers: limit ${String(limit)} is outside [1, ${MAX_PAGE_SIZE}].`);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
assertPage(page) {
|
|
558
|
+
if (!this.strictNumbers || page === void 0) {
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
if (!Number.isFinite(page) || page < 1) {
|
|
562
|
+
throw new Error(`strictNumbers: page ${String(page)} must be a finite number >= 1.`);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
/** strictNumbers: a computed skip beyond MAX_SKIP throws (audit F18). */
|
|
566
|
+
assertSkipCap(page, limit) {
|
|
567
|
+
if (!this.strictNumbers) {
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
const skip = (clampPage(page) - 1) * clampLimit(limit);
|
|
571
|
+
if (skip > MAX_SKIP) {
|
|
572
|
+
throw new Error(
|
|
573
|
+
`strictNumbers: computed skip ${String(skip)} exceeds the ${String(MAX_SKIP)} cap \u2014 use cursor pagination for deep pages.`
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
/** A projection without the cursor field would derive cursors from `undefined` (audit F5). */
|
|
578
|
+
assertCursorFieldProjected() {
|
|
579
|
+
const select = this.selectArg ?? this.customArgs.select;
|
|
580
|
+
if (select && !select[this.cursorField]) {
|
|
581
|
+
throw new Error(
|
|
582
|
+
`executeWithCursor(): the projection drops cursorField "${this.cursorField}", so page cursors would be built from undefined. Keep the field in .select() or remove the projection.`
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
buildWhere() {
|
|
587
|
+
return combineAnd(this.andConditions, this.customWhere);
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
export {
|
|
591
|
+
DIALECT_CAPABILITIES,
|
|
592
|
+
NoResultError,
|
|
593
|
+
QueryBuilder,
|
|
594
|
+
configureQueryBuilder,
|
|
595
|
+
detectDialect,
|
|
596
|
+
registerDialect,
|
|
597
|
+
resetDialectConfig,
|
|
598
|
+
resolveDialect,
|
|
599
|
+
setDialectDetector
|
|
600
|
+
};
|
|
601
|
+
//# sourceMappingURL=index.js.map
|