@crossfox/query-handler 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/dist/worker.js ADDED
@@ -0,0 +1,560 @@
1
+ import { toArray, spliter, isAllowed } from "./util";
2
+ import { getQueryHandlerDefaults } from "./config";
3
+ import { SIMPLE_ID_RE, CUSTOM_SORT_RE, QUERY_HANDLER_BRAND } from "./constants";
4
+ import { err, mergeOptions, parsePositiveInt, resolveFieldEntry, fieldEntryName, qualifySelectField, qualifyWhereColumn, assertSafeColumnRef, sortDecode, isSortAllowed, normalizeRelationMap, normalizeScopeMap, resolveNameList, resolveItems, resolveWithTotal, resolveCursorAllowCount, } from "./helpers";
5
+ import { cacheGet, cacheSet } from "./cache";
6
+ import { checkRateLimit } from "./rate-limit";
7
+ import { runCursor } from "./cursor";
8
+ import { applyFilters } from "./filter";
9
+ import { runHasManyRelations } from "./relation";
10
+ // ─────────────────────────────────────────────────────────────────────────────
11
+ // Worker
12
+ // ─────────────────────────────────────────────────────────────────────────────
13
+ export function queryHandlerWorker(query, optionsArgs = {}) {
14
+ const globalDefaults = getQueryHandlerDefaults();
15
+ let options = {
16
+ maxLimit: globalDefaults.runtime.maxLimit,
17
+ strictFilters: globalDefaults.runtime.strictFilters,
18
+ limits: {
19
+ ...globalDefaults.runtime.limits,
20
+ ...optionsArgs.limits,
21
+ },
22
+ ...optionsArgs,
23
+ withTotal: optionsArgs.withTotal ?? globalDefaults.runtime.count ?? true,
24
+ };
25
+ const queryRaw = query;
26
+ const ignoreQueryFields = options.ignoreQueryFields
27
+ ?? globalDefaults.runtime.ignoreQueryFields
28
+ ?? false;
29
+ const queryForMerge = ignoreQueryFields
30
+ ? Object.fromEntries(Object.entries(queryRaw).filter(([k]) => k !== 'field'))
31
+ : queryRaw;
32
+ const initialMerged = (() => {
33
+ const src = options.initial;
34
+ if (!src)
35
+ return globalDefaults.initial;
36
+ const { fields: initialFields, filters: initialFilters, ...rest } = src;
37
+ return {
38
+ ...globalDefaults.initial,
39
+ ...rest,
40
+ ...(initialFields !== undefined ? { field: initialFields } : {}),
41
+ ...(initialFilters !== undefined ? { filter: initialFilters } : {}),
42
+ };
43
+ })();
44
+ let { page, limit, field, sort = [], search, filter = {}, relation: relationParam = [], after, before, scope: scopeParam = '', extend: extendParam = '', count: clientCount, v: versionParam, } = {
45
+ ...globalDefaults.queryMerge,
46
+ ...initialMerged,
47
+ ...queryForMerge,
48
+ };
49
+ page = parsePositiveInt(page, 'page') ?? globalDefaults.queryMerge.page;
50
+ limit = parsePositiveInt(limit, 'limit') ?? globalDefaults.queryMerge.limit;
51
+ options.withTotal = resolveWithTotal(options.withTotal, globalDefaults.runtime.count, clientCount);
52
+ // ── FIELD + MODEL (сразу — $q доступен до query()) ───────────
53
+ let resolvedField = [];
54
+ if (field?.length) {
55
+ resolvedField = toArray(typeof field === 'string' ? spliter(field, ',') : field);
56
+ }
57
+ else if (options.fieldsDefault?.length) {
58
+ resolvedField = [...options.fieldsDefault];
59
+ }
60
+ else if (options.fields?.length) {
61
+ resolvedField = [...options.fields];
62
+ }
63
+ if (options.fields?.length && resolvedField.length) {
64
+ const allowedNames = options.fields.map(fieldEntryName);
65
+ const requestedNames = resolvedField.map(fieldEntryName);
66
+ if (!isAllowed(requestedNames, allowedNames))
67
+ err('Field is not allowed: ' + requestedNames.join(', '));
68
+ }
69
+ if (!resolvedField.length)
70
+ resolvedField = ['*'];
71
+ let $q;
72
+ let baseRef;
73
+ let schemaRow;
74
+ if (options.model) {
75
+ schemaRow = options.model();
76
+ baseRef = schemaRow.alias || schemaRow.tableWithoutAlias;
77
+ const selectList = [
78
+ ...resolvedField.map(f => resolveFieldEntry(f, baseRef)),
79
+ ...(options.fieldsForced ?? []).map(f => qualifySelectField(f, baseRef)),
80
+ ];
81
+ $q = schemaRow.select(selectList);
82
+ }
83
+ else if (options.query) {
84
+ $q = options.query;
85
+ const selectList = [
86
+ ...resolvedField.map(f => resolveFieldEntry(f)),
87
+ ...(options.fieldsForced ?? []),
88
+ ];
89
+ if (selectList.length && selectList[0] !== '*')
90
+ $q.select(...selectList);
91
+ }
92
+ else {
93
+ err('Model or query is required', 500);
94
+ }
95
+ const beforeQueryHooks = [];
96
+ if (globalDefaults.beforeQuery)
97
+ beforeQueryHooks.push(globalDefaults.beforeQuery);
98
+ if (options.beforeQuery)
99
+ beforeQueryHooks.push(options.beforeQuery);
100
+ if (options.before) {
101
+ beforeQueryHooks.push((q, hookCtx) => options.before(q, hookCtx));
102
+ }
103
+ // ─────────────────────────────────────────────────────────────
104
+ // Handler object
105
+ // ─────────────────────────────────────────────────────────────
106
+ const handler = {
107
+ [QUERY_HANDLER_BRAND]: true,
108
+ $q,
109
+ items: [],
110
+ total: -1,
111
+ nextId: null,
112
+ prevId: null,
113
+ context: null,
114
+ aggregate: null,
115
+ // Защита от параллельных вызовов
116
+ _promise: null,
117
+ _cacheKey: null,
118
+ addField(name) {
119
+ const rawList = toArray(name).map(String);
120
+ if (options.fields?.length) {
121
+ const allowedNames = options.fields.map(fieldEntryName);
122
+ if (!isAllowed(rawList, allowedNames))
123
+ err('Cannot add disallowed field: ' + rawList.join(', '));
124
+ }
125
+ $q.select(...rawList.map(n => qualifySelectField(n, baseRef)));
126
+ return this;
127
+ },
128
+ beforeQuery(fn) {
129
+ beforeQueryHooks.push(fn);
130
+ return this;
131
+ },
132
+ /** Условный merge опций */
133
+ when(condition, fn) {
134
+ const cond = typeof condition === 'function' ? condition() : condition;
135
+ if (cond) {
136
+ const patch = fn(options);
137
+ if (patch)
138
+ options = mergeOptions(options, patch);
139
+ }
140
+ return this;
141
+ },
142
+ /** Клонировать handler с новыми опциями */
143
+ clone(patch) {
144
+ return queryHandlerWorker(query, mergeOptions(options, patch));
145
+ },
146
+ query() {
147
+ return (handler._promise ??= handler._run());
148
+ },
149
+ async run() {
150
+ if (!handler._promise)
151
+ await handler.query();
152
+ const expose = options.expose ?? {};
153
+ const isCursor = !!options.cursorPaginate;
154
+ const base = {
155
+ items: handler.items,
156
+ };
157
+ if (isCursor) {
158
+ const doCount = resolveCursorAllowCount(options.cursorPaginate, clientCount);
159
+ if (doCount && expose.total !== false)
160
+ base.total = handler.total;
161
+ base.nextId = handler.nextId;
162
+ base.prevId = handler.prevId;
163
+ base.hasMore = handler.nextId !== null;
164
+ if (expose.pageSize !== false)
165
+ base.pageSize = limit;
166
+ }
167
+ else if (options.withTotal) {
168
+ if (expose.total !== false)
169
+ base.total = handler.total;
170
+ if (expose.page !== false)
171
+ base.page = page;
172
+ if (expose.pageSize !== false)
173
+ base.pageSize = limit;
174
+ }
175
+ if (handler.aggregate)
176
+ base.aggregate = handler.aggregate;
177
+ if (options.meta && expose.meta !== false) {
178
+ base._meta = {
179
+ appliedFilters: handler.context?.filters,
180
+ appliedSort: handler.context?.sort,
181
+ fields: handler.context?.fields,
182
+ relations: handler.context?.relations,
183
+ };
184
+ }
185
+ return base;
186
+ },
187
+ // ─────────────────────────────────────────────────────────
188
+ // Внутренние методы
189
+ // ─────────────────────────────────────────────────────────
190
+ async _run() {
191
+ // ── Apply ─────────────────────────────────────────────
192
+ if (options.apply?.length) {
193
+ for (const entry of options.apply) {
194
+ if (!entry)
195
+ continue;
196
+ if (typeof entry === 'function') {
197
+ const patch = await entry(options);
198
+ if (patch && typeof patch === 'object') {
199
+ options = mergeOptions(options, patch);
200
+ }
201
+ }
202
+ else if (typeof entry === 'object') {
203
+ options = mergeOptions(options, entry);
204
+ }
205
+ }
206
+ }
207
+ // ── Mock ──────────────────────────────────────────────
208
+ if (options.mock) {
209
+ if (!globalDefaults.runtime.allowMock)
210
+ err('mock is not allowed in production');
211
+ handler.items = options.mock;
212
+ const mockCtx = buildEmptyCtx();
213
+ await handler._postProcess(mockCtx);
214
+ return handler.items;
215
+ }
216
+ // ── DISTINCT / TRANSACTION ────────────────────────────
217
+ if (options.distinct)
218
+ $q.distinct?.();
219
+ if (options.transaction)
220
+ $q.transaction?.(options.transaction);
221
+ // ── CURSOR CONFIG ─────────────────────────────────────
222
+ const cursorCfg = !options.cursorPaginate ? null
223
+ : options.cursorPaginate === true ? {}
224
+ : typeof options.cursorPaginate === 'string' ? { field: options.cursorPaginate }
225
+ : options.cursorPaginate;
226
+ const cursorField = cursorCfg?.field?.trim()
227
+ ? assertSafeColumnRef(cursorCfg.field, 'cursorPaginate.field')
228
+ : schemaRow?.primaryKey ?? 'id';
229
+ // ── RELATION: JOIN ────────────────────────────────────
230
+ const requestedRel = resolveNameList(relationParam, options.relationsDefault);
231
+ const relationMap = normalizeRelationMap(options.relations);
232
+ const scopeMap = normalizeScopeMap(options.scopes);
233
+ const appliedRelNames = [];
234
+ // required — всегда
235
+ for (const [name, cfg] of relationMap) {
236
+ if (cfg.required && cfg.with) {
237
+ $q.with(cfg.with);
238
+ if (cfg.fieldsForced?.length)
239
+ $q.select(...cfg.fieldsForced);
240
+ appliedRelNames.push(name);
241
+ }
242
+ }
243
+ // клиентские
244
+ for (const rel of requestedRel) {
245
+ if (!SIMPLE_ID_RE.test(rel))
246
+ err(`Relation name invalid: "${rel}"`);
247
+ const cfg = relationMap.get(rel);
248
+ if (!cfg)
249
+ err(`Relation "${rel}" is not allowed`);
250
+ if (cfg.with && !appliedRelNames.includes(rel)) {
251
+ if (!options.model)
252
+ err(`Relation "${rel}" requires model`, 500);
253
+ $q.with(cfg.with);
254
+ const selects = cfg.fields?.length
255
+ ? cfg.fields.map(f => resolveFieldEntry(f))
256
+ : cfg.fieldsDefault?.length
257
+ ? [...cfg.fieldsDefault]
258
+ : [];
259
+ if (selects.length)
260
+ $q.select(...selects);
261
+ if (cfg.fieldsForced?.length)
262
+ $q.select(...cfg.fieldsForced);
263
+ appliedRelNames.push(rel);
264
+ }
265
+ }
266
+ // ── SCOPE ─────────────────────────────────────────────
267
+ const appliedScopes = [];
268
+ const requestedScopes = resolveNameList(scopeParam, options.scopesDefault);
269
+ if (scopeMap.size && requestedScopes.length) {
270
+ for (const raw of requestedScopes) {
271
+ const idx = raw.indexOf(':');
272
+ const name = (idx === -1 ? raw : raw.slice(0, idx)).trim();
273
+ const param = idx === -1 ? undefined : raw.slice(idx + 1);
274
+ if (!SIMPLE_ID_RE.test(name))
275
+ err(`Scope name invalid: "${raw}"`);
276
+ const scopeFn = scopeMap.get(name);
277
+ if (scopeFn === undefined)
278
+ err(`Scope "${name}" is not allowed`);
279
+ await scopeFn($q, param);
280
+ appliedScopes.push(name);
281
+ }
282
+ }
283
+ // ── EXTEND: BEFORE ────────────────────────────────────
284
+ const requestedExtend = toArray(extendParam)
285
+ .flatMap((s) => s.split(','))
286
+ .map((s) => s.trim())
287
+ .filter(Boolean);
288
+ const appliedExtends = [];
289
+ if (options.extends && requestedExtend.length) {
290
+ const partialCtx = buildEmptyCtx();
291
+ for (const name of requestedExtend) {
292
+ if (!SIMPLE_ID_RE.test(name))
293
+ err(`Extend name invalid: "${name}"`);
294
+ const ext = options.extends[name];
295
+ if (!ext)
296
+ err(`Extend "${name}" is not allowed`);
297
+ if (ext.before) {
298
+ await ext.before($q, partialCtx);
299
+ }
300
+ appliedExtends.push(name);
301
+ }
302
+ }
303
+ // ── SORT ──────────────────────────────────────────────
304
+ const sortWhitelist = toArray(options.sorts).map(String);
305
+ const rawSort = toArray(sort).flatMap((s) => s.split(','));
306
+ const sortTokens = rawSort.length > 0 ? rawSort
307
+ : options.sortsDefault ? [String(options.sortsDefault)]
308
+ : sortWhitelist.length ? [sortWhitelist[0]]
309
+ : [];
310
+ const maxSortFields = options.limits?.maxSortFields;
311
+ if (maxSortFields && sortTokens.length > maxSortFields)
312
+ err(`Too many sort fields (max ${maxSortFields})`);
313
+ if (!cursorCfg) {
314
+ for (const token of sortTokens) {
315
+ const [col, dir] = sortDecode(token);
316
+ if (col === '*')
317
+ err('Sort column cannot be *');
318
+ if (CUSTOM_SORT_RE.test(token)) {
319
+ const customName = col.slice(1); // убираем $
320
+ const customFn = options.sortsCustom?.[customName];
321
+ if (!customFn)
322
+ err(`Custom sort "${customName}" is not supported`);
323
+ const partialCtx = buildEmptyCtx();
324
+ await customFn($q, dir, partialCtx);
325
+ }
326
+ else {
327
+ if (!isSortAllowed(col, sortWhitelist))
328
+ err(`Sort "${col}" not supported`);
329
+ $q.order(qualifyWhereColumn(col, baseRef), dir);
330
+ }
331
+ }
332
+ }
333
+ // ── SEARCH ────────────────────────────────────────────
334
+ if (search) {
335
+ const minLen = options.limits?.searchMinLength ?? 2;
336
+ const maxLen = options.limits?.searchMaxLength ?? 500;
337
+ if (search.length < minLen)
338
+ err(`Search query too short (min ${minLen})`);
339
+ if (search.length > maxLen)
340
+ err(`Search query too long (max ${maxLen})`);
341
+ if (!options.search)
342
+ err('Search is not supported');
343
+ const partialCtx = buildEmptyCtx();
344
+ if (typeof options.search === 'function') {
345
+ await options.search($q, search, partialCtx);
346
+ }
347
+ else {
348
+ await $q.search(toArray(options.search), search);
349
+ }
350
+ }
351
+ // ── FILTER ────────────────────────────────────────────
352
+ const partialCtx = {
353
+ page, limit, sort: sortTokens,
354
+ search: search ?? undefined,
355
+ fields: resolvedField.map(f => typeof f === 'string' ? f : Object.keys(f)[0]),
356
+ relations: appliedRelNames,
357
+ scopes: appliedScopes,
358
+ extends: appliedExtends,
359
+ };
360
+ const { appliedFilters } = await applyFilters($q, filter, options, partialCtx, baseRef);
361
+ // ── GROUPBY / HAVING ──────────────────────────────────
362
+ if (options.groupBy?.length)
363
+ $q.groupBy?.(...options.groupBy);
364
+ if (options.having)
365
+ $q.having?.(options.having);
366
+ // ── CONTEXT ───────────────────────────────────────────
367
+ const ctx = {
368
+ ...partialCtx,
369
+ filters: appliedFilters,
370
+ };
371
+ handler.context = ctx;
372
+ // ── BEFORE QUERY HOOKS ────────────────────────────────
373
+ if (beforeQueryHooks.length) {
374
+ const hookCtx = { page, limit, filters: appliedFilters };
375
+ for (const hook of beforeQueryHooks)
376
+ await hook($q, hookCtx);
377
+ }
378
+ // ── RATE LIMIT ────────────────────────────────────────
379
+ if (options.rateLimit)
380
+ await checkRateLimit(options.rateLimit, ctx);
381
+ // ── CACHE LOOKUP ──────────────────────────────────────
382
+ if (options.cacheTtl && options.cacheTtl > 0) {
383
+ const cacheKey = typeof options.cacheKey === 'function'
384
+ ? options.cacheKey(ctx)
385
+ : options.cacheKey ?? `qh:${baseRef ?? 'unknown'}:${JSON.stringify(ctx)}`;
386
+ const cached = cacheGet(cacheKey);
387
+ if (cached) {
388
+ handler.items = cached.value;
389
+ handler.total = cached.total;
390
+ return handler.items;
391
+ }
392
+ handler._cacheKey = cacheKey;
393
+ }
394
+ // ── LIMIT CHECK ───────────────────────────────────────
395
+ if (limit > (options.maxLimit ?? Infinity))
396
+ err(`Limit exceeds ${options.maxLimit}`);
397
+ // ── RANDOMIZE ─────────────────────────────────────────
398
+ if (options.randomize) {
399
+ const rnd = options.randomize;
400
+ if (rnd === true) {
401
+ $q.orderByRaw?.('RAND()');
402
+ }
403
+ else if (typeof rnd === 'object' && rnd.type === 'weighted') {
404
+ $q.orderByRaw?.(`RAND() * ${rnd.field} DESC`);
405
+ if (rnd.limit)
406
+ $q.limit(rnd.limit);
407
+ }
408
+ else if (typeof rnd === 'object' && rnd.type === 'custom') {
409
+ rnd.fn($q);
410
+ }
411
+ }
412
+ // ── CHUNK ─────────────────────────────────────────────
413
+ if (options.chunk) {
414
+ await handler._runChunk($q);
415
+ return handler.items;
416
+ }
417
+ // ── EXECUTE ───────────────────────────────────────────
418
+ if (cursorCfg !== null) {
419
+ const doCount = resolveCursorAllowCount(cursorCfg, clientCount);
420
+ const displayDir = sortTokens[0] ? sortDecode(sortTokens[0])[1] : 'DESC';
421
+ const res = await runCursor($q, after, before, limit, cursorField, { ...cursorCfg, allowCount: doCount }, displayDir);
422
+ handler.items = res.items;
423
+ handler.total = res.total;
424
+ handler.nextId = res.nextId;
425
+ handler.prevId = res.prevId;
426
+ }
427
+ else {
428
+ const res = options.withTotal
429
+ ? await $q.paginate(page, limit)
430
+ : { items: await $q.page(page, limit).run(), total: -2 };
431
+ handler.items = res.items;
432
+ handler.total = res.total;
433
+ }
434
+ // ── AGGREGATE ─────────────────────────────────────────
435
+ if (options.aggregate) {
436
+ const aggClone = $q.clone({ from: true, where: true, join: true, groupBy: true });
437
+ const aggSelects = Object.entries(options.aggregate)
438
+ .map(([alias, expr]) => `${expr} AS ${alias}`);
439
+ handler.aggregate = await aggClone.select(aggSelects).first() ?? {};
440
+ }
441
+ // ── CACHE SAVE ────────────────────────────────────────
442
+ if (options.cacheTtl && handler._cacheKey) {
443
+ cacheSet(handler._cacheKey, handler.items, handler.total, options.cacheTtl);
444
+ handler._cacheKey = null;
445
+ }
446
+ // ── hasMany relations ─────────────────────────────────
447
+ await runHasManyRelations(handler.items, relationMap, requestedRel, schemaRow?.primaryKey ?? 'id');
448
+ await handler._postProcess(ctx);
449
+ return handler.items;
450
+ },
451
+ async _postProcess(ctx) {
452
+ // ── EXTEND: AFTER / AFTER_EACH ────────────────────────
453
+ if (options.extends && ctx.extends.length) {
454
+ for (const name of ctx.extends) {
455
+ const ext = options.extends[name];
456
+ if (!ext)
457
+ continue;
458
+ const hooks = ext;
459
+ if (hooks.after) {
460
+ handler.items = await resolveItems(hooks.after(handler.items, ctx));
461
+ }
462
+ if (hooks.afterEach) {
463
+ handler.items = await Promise.all(handler.items.map((it, idx) => hooks.afterEach(it, idx, ctx)));
464
+ }
465
+ }
466
+ }
467
+ // ── AFTER HOOK ────────────────────────────────────────
468
+ if (options.after) {
469
+ const next = await resolveItems(options.after(handler.items, ctx));
470
+ handler.items = next ?? handler.items;
471
+ }
472
+ // ── AFTER EACH ────────────────────────────────────────
473
+ if (options.afterEach) {
474
+ handler.items = await Promise.all(handler.items.map((it, idx) => options.afterEach(it, idx, ctx)));
475
+ }
476
+ // ── ROW TRANSFORM ─────────────────────────────────────
477
+ if (options.transform) {
478
+ handler.items = handler.items.map(options.transform);
479
+ }
480
+ // ── AFTER QUERY (наблюдатель, без изменения items) ─────
481
+ const afterCtx = { page, limit, filters: ctx.filters };
482
+ if (globalDefaults.afterQuery)
483
+ await globalDefaults.afterQuery(handler.items, handler.total, afterCtx);
484
+ if (options.afterQuery)
485
+ await options.afterQuery(handler.items, handler.total, afterCtx);
486
+ // ── FIELD TRANSFORM ───────────────────────────────────
487
+ if (options.fieldsTransform) {
488
+ const transforms = options.fieldsTransform;
489
+ handler.items = handler.items.map((item) => {
490
+ const out = { ...item };
491
+ for (const [f, fn] of Object.entries(transforms)) {
492
+ if (f in out)
493
+ out[f] = fn(out[f], item);
494
+ }
495
+ return out;
496
+ });
497
+ }
498
+ // ── RANDOMIZE: кастомная досортировка ─────────────────
499
+ if (options.randomize &&
500
+ typeof options.randomize === 'object' &&
501
+ options.randomize.type === 'custom' &&
502
+ options.randomize.sort) {
503
+ handler.items = options.randomize.sort(handler.items);
504
+ }
505
+ // ── VERSIONS ──────────────────────────────────────────
506
+ if (options.versions && versionParam !== undefined) {
507
+ const transformer = options.versions[versionParam];
508
+ if (transformer)
509
+ handler.items = transformer(handler.items);
510
+ }
511
+ // ── ON EMPTY ──────────────────────────────────────────
512
+ if (!handler.items.length && options.onEmpty) {
513
+ if (options.onEmpty === 'error')
514
+ err('No results found', 404);
515
+ else if (options.onEmpty === 'null')
516
+ handler.items = [];
517
+ else
518
+ handler.items = await resolveItems(options.onEmpty(ctx));
519
+ }
520
+ },
521
+ async _runChunk($q) {
522
+ const cfg = options.chunk;
523
+ let offset = 0, processed = 0;
524
+ const total = options.withTotal
525
+ ? await $q.clone({ from: true, where: true, join: true }).count()
526
+ : -1;
527
+ while (true) {
528
+ const rows = await $q
529
+ .clone({ from: true, where: true, join: true, order: true })
530
+ .limit(cfg.size, offset)
531
+ .run();
532
+ if (!rows.length)
533
+ break;
534
+ processed += rows.length;
535
+ const index = Math.floor(offset / cfg.size);
536
+ await cfg.handler(rows, index, { processed, total, index });
537
+ offset += cfg.size;
538
+ if (rows.length < cfg.size)
539
+ break;
540
+ }
541
+ const finalIndex = Math.floor(offset / cfg.size);
542
+ if (cfg.onDone)
543
+ await cfg.onDone({ processed, total, index: finalIndex });
544
+ },
545
+ };
546
+ function buildEmptyCtx() {
547
+ return {
548
+ page, limit,
549
+ filters: {},
550
+ sort: [],
551
+ search: undefined,
552
+ fields: [],
553
+ relations: [],
554
+ scopes: [],
555
+ extends: [],
556
+ };
557
+ }
558
+ // Explicit any: declaration emit can't name private Symbol brand key.
559
+ return handler;
560
+ }
package/package.json ADDED
@@ -0,0 +1,92 @@
1
+ {
2
+ "name": "@crossfox/query-handler",
3
+ "version": "0.1.0",
4
+ "description": "HTTP list API layer on @crossfox/db: filters, sort, search, relations, cursor pagination, cache, optional Elysia plugin.",
5
+ "license": "PolyForm-Noncommercial-1.0.0",
6
+ "author": "Oleksii Fursov <nodepro777@gmail.com>",
7
+ "homepage": "https://crossfox.online",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/OleksiiFursov/packages.git",
11
+ "directory": "query-handler"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/OleksiiFursov/packages/issues",
15
+ "email": "nodepro777@gmail.com"
16
+ },
17
+ "type": "module",
18
+ "sideEffects": false,
19
+ "engines": {
20
+ "bun": ">=1.1.0"
21
+ },
22
+ "main": "./dist/index.js",
23
+ "types": "./dist/index.d.ts",
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/index.d.ts",
27
+ "import": "./dist/index.js",
28
+ "default": "./dist/index.js"
29
+ },
30
+ "./types": {
31
+ "types": "./dist/types.d.ts",
32
+ "import": "./dist/types.js",
33
+ "default": "./dist/types.js"
34
+ },
35
+ "./elysia": {
36
+ "types": "./dist/elysia.d.ts",
37
+ "import": "./dist/elysia.js",
38
+ "default": "./dist/elysia.js"
39
+ },
40
+ "./schema": {
41
+ "types": "./dist/schema.d.ts",
42
+ "import": "./dist/schema.js",
43
+ "default": "./dist/schema.js"
44
+ },
45
+ "./package.json": "./package.json"
46
+ },
47
+ "files": [
48
+ "dist",
49
+ "README.md",
50
+ "LICENSE"
51
+ ],
52
+ "scripts": {
53
+ "build": "tsc -p tsconfig.build.json",
54
+ "typecheck": "tsc --noEmit",
55
+ "test": "bun test tests/query-handler.unit.test.ts",
56
+ "test:integration": "bun test tests/query-handler.mysql-suite.ts",
57
+ "check": "bun run typecheck && bun run test",
58
+ "prepublishOnly": "bun run check && bun run build"
59
+ },
60
+ "peerDependencies": {
61
+ "@crossfox/db": ">=0.1.0",
62
+ "elysia": ">=1.4"
63
+ },
64
+ "peerDependenciesMeta": {
65
+ "elysia": {
66
+ "optional": true
67
+ }
68
+ },
69
+ "devDependencies": {
70
+ "@crossfox/db": "workspace:*",
71
+ "@types/bun": "^1.3.14",
72
+ "elysia": "^1.4.28",
73
+ "typescript": "6.0.3"
74
+ },
75
+ "keywords": [
76
+ "query-handler",
77
+ "query",
78
+ "filter",
79
+ "pagination",
80
+ "cursor",
81
+ "mysql",
82
+ "bun",
83
+ "elysia",
84
+ "orm",
85
+ "api",
86
+ "crossfox",
87
+ "@crossfox/db"
88
+ ],
89
+ "publishConfig": {
90
+ "access": "public"
91
+ }
92
+ }