@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.
@@ -0,0 +1,2 @@
1
+ import type { RateLimitConfig, QueryHandlerContext } from "./types";
2
+ export declare function checkRateLimit(cfg: RateLimitConfig, ctx: QueryHandlerContext): Promise<void>;
@@ -0,0 +1,14 @@
1
+ import { err } from "./helpers";
2
+ import { getQueryHandlerDefaults } from "./config";
3
+ // ─────────────────────────────────────────────────────────────────────────────
4
+ // Rate limit для queryHandler — поверх fixedWindowHit (или inject через configure).
5
+ // ─────────────────────────────────────────────────────────────────────────────
6
+ export async function checkRateLimit(cfg, ctx) {
7
+ const hit = getQueryHandlerDefaults().runtime.rateLimitHit;
8
+ if (hit(`qh:${cfg.key(ctx)}`, cfg.max, cfg.window)) {
9
+ if (cfg.onExceed)
10
+ cfg.onExceed();
11
+ else
12
+ err("Too many requests", 429);
13
+ }
14
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * In-memory fixed-window rate limit (общий Map на процесс).
3
+ * В кластере приложение может подменить через configureQueryHandler({ rateLimitHit }).
4
+ */
5
+ export type RateLimitHitFn = (key: string, max: number, windowMs: number) => boolean;
6
+ /** @returns true — лимит превышен */
7
+ export declare function fixedWindowHit(key: string, max: number, windowMs: number): boolean;
8
+ export declare function purgeRateLimitBuckets(): number;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * In-memory fixed-window rate limit (общий Map на процесс).
3
+ * В кластере приложение может подменить через configureQueryHandler({ rateLimitHit }).
4
+ */
5
+ const buckets = new Map();
6
+ /** @returns true — лимит превышен */
7
+ export function fixedWindowHit(key, max, windowMs) {
8
+ const now = Date.now();
9
+ const b = buckets.get(key);
10
+ if (b && b.reset > now) {
11
+ if (b.count >= max)
12
+ return true;
13
+ b.count++;
14
+ return false;
15
+ }
16
+ buckets.set(key, { count: 1, reset: now + windowMs });
17
+ return false;
18
+ }
19
+ export function purgeRateLimitBuckets() {
20
+ const now = Date.now();
21
+ let purged = 0;
22
+ for (const [k, v] of buckets) {
23
+ if (v.reset <= now) {
24
+ buckets.delete(k);
25
+ purged++;
26
+ }
27
+ }
28
+ return purged;
29
+ }
@@ -0,0 +1,2 @@
1
+ import type { RelationConfig } from "./types";
2
+ export declare function runHasManyRelations(items: unknown[], relationMap: Map<string, RelationConfig>, requested: string[], primaryKey: string): Promise<void>;
@@ -0,0 +1,64 @@
1
+ import { resolveFieldEntry, sortDecode } from "./helpers";
2
+ import { loadManyRelation, normalizeManyConfig } from "@crossfox/db/with-many";
3
+ // ─────────────────────────────────────────────────────────────────────────────
4
+ // hasMany — отдельный запрос с Promise.all
5
+ // ─────────────────────────────────────────────────────────────────────────────
6
+ export async function runHasManyRelations(items, relationMap, requested, primaryKey) {
7
+ if (!items.length)
8
+ return;
9
+ const toLoad = [...relationMap.entries()].filter(([name, cfg]) => cfg.model && (cfg.required || requested.includes(name)));
10
+ if (!toLoad.length)
11
+ return;
12
+ const independent = toLoad.filter(([, cfg]) => !cfg.dependsOn);
13
+ const dependent = toLoad.filter(([, cfg]) => cfg.dependsOn);
14
+ await Promise.all(independent.map(([name, cfg]) => runHasMany(name, cfg, items, primaryKey)));
15
+ for (const [name, cfg] of dependent) {
16
+ await runHasMany(name, cfg, items, primaryKey);
17
+ }
18
+ }
19
+ function relationToManyConfig(cfg, primaryKey) {
20
+ return normalizeManyConfig({
21
+ model: cfg.model,
22
+ on: [cfg.dataKey ?? primaryKey, cfg.foreign],
23
+ dependsOn: cfg.dependsOn,
24
+ as: cfg.as,
25
+ });
26
+ }
27
+ async function runHasMany(name, cfg, items, primaryKey) {
28
+ const root = items;
29
+ const owners = cfg.dependsOn
30
+ ? root.flatMap((r) => r[cfg.dependsOn] ?? [])
31
+ : root;
32
+ const manyCfg = relationToManyConfig(cfg, primaryKey);
33
+ const resultKey = cfg.as ?? name;
34
+ await loadManyRelation(owners, manyCfg, resultKey, undefined, async (relQ) => {
35
+ let q = relQ;
36
+ const selectFields = [];
37
+ if (cfg.fieldsForced?.length)
38
+ selectFields.push(...cfg.fieldsForced);
39
+ if (cfg.fields?.length) {
40
+ selectFields.push(...cfg.fields.map((f) => resolveFieldEntry(f)));
41
+ }
42
+ else if (cfg.fieldsDefault?.length) {
43
+ selectFields.push(...cfg.fieldsDefault);
44
+ }
45
+ if (selectFields.length)
46
+ q = q.select(selectFields);
47
+ if (cfg.filtersForced)
48
+ q = q.where(cfg.filtersForced);
49
+ if (cfg.filtersRequired)
50
+ q = q.where(cfg.filtersRequired);
51
+ if (cfg.sortsDefault) {
52
+ const [col, dir] = sortDecode(cfg.sortsDefault);
53
+ q = q.order(col, dir);
54
+ }
55
+ if (cfg.limit)
56
+ q = q.limit(cfg.limit);
57
+ if (cfg.query) {
58
+ const result = await cfg.query(q);
59
+ if (result !== false && result)
60
+ q = result;
61
+ }
62
+ return q;
63
+ });
64
+ }
@@ -0,0 +1,35 @@
1
+ export declare const typeQuerySchema: import("@sinclair/typebox").TObject<{
2
+ page: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
3
+ limit: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
4
+ field: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
5
+ search: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
6
+ sort: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
7
+ relation: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
8
+ after: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
9
+ before: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
10
+ scope: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
11
+ extend: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
12
+ filter: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TAny>>>;
13
+ count: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
14
+ v: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
15
+ }>;
16
+ export declare const queryTypeQuerySchema: {
17
+ query: import("@sinclair/typebox").TObject<{
18
+ page: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
19
+ limit: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
20
+ field: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
21
+ search: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
22
+ sort: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
23
+ relation: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
24
+ after: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
25
+ before: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
26
+ scope: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
27
+ extend: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
28
+ filter: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TAny>>>;
29
+ count: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
30
+ v: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
31
+ }>;
32
+ beforeHandle: ({ request }: {
33
+ request: Request;
34
+ }) => void;
35
+ };
package/dist/schema.js ADDED
@@ -0,0 +1,32 @@
1
+ import { t } from "elysia";
2
+ import { error } from "@crossfox/db";
3
+ // ─────────────────────────────────────────────────────────────────────────────
4
+ // Elysia query schema
5
+ // ─────────────────────────────────────────────────────────────────────────────
6
+ export const typeQuerySchema = t.Object({
7
+ page: t.Optional(t.Numeric()),
8
+ limit: t.Optional(t.Numeric()),
9
+ field: t.Optional(t.String()),
10
+ search: t.Optional(t.String()),
11
+ sort: t.Optional(t.String()),
12
+ relation: t.Optional(t.String()),
13
+ after: t.Optional(t.Numeric()),
14
+ before: t.Optional(t.Numeric()),
15
+ scope: t.Optional(t.String()),
16
+ extend: t.Optional(t.String()),
17
+ filter: t.Optional(t.Record(t.String(), t.Optional(t.Any()))),
18
+ count: t.Optional(t.Numeric()),
19
+ v: t.Optional(t.Numeric()),
20
+ });
21
+ export const queryTypeQuerySchema = {
22
+ query: typeQuerySchema,
23
+ beforeHandle: ({ request }) => {
24
+ const url = new URL(request.url);
25
+ const params = Array.from(url.searchParams.keys());
26
+ const allowedKeys = Object.keys(typeQuerySchema.properties);
27
+ const extraKeys = params.filter(p => !allowedKeys.includes(p.split('[')[0]));
28
+ if (extraKeys.length > 0) {
29
+ throw error(422, `Unknown parameters: ${extraKeys.join(', ')}`);
30
+ }
31
+ },
32
+ };
@@ -0,0 +1,267 @@
1
+ import type { QueryBuilder, SchemaFactory } from "@crossfox/db";
2
+ export type SchemaFactoryLike = ((alias?: string) => unknown) & {
3
+ readonly table: string;
4
+ readonly tableWithoutAlias: string;
5
+ readonly alias: string;
6
+ readonly primaryKey: string;
7
+ };
8
+ export type InferSchemaRow<M extends SchemaFactoryLike> = M extends SchemaFactory<infer TRow, any, any> ? TRow extends Record<string, unknown> ? TRow : Record<string, unknown> : Record<string, unknown>;
9
+ /** Имена scope из `createSchema({ scopes })` (если модель не сужена до `SchemaFactory<T>`). */
10
+ export type InferSchemaScopeNames<M extends SchemaFactoryLike> = M extends SchemaFactory<any, any, infer TScopes extends string> ? TScopes : string;
11
+ export type RowKeys<TRow extends Record<string, unknown>> = keyof TRow & string;
12
+ export type LooseRowKey<TRow extends Record<string, unknown>> = RowKeys<TRow> | (string & {});
13
+ export type SortValue<TRow extends Record<string, unknown>> = RowKeys<TRow> | `!${RowKeys<TRow>}` | `$${string}` | `!$${string}` | (string & {});
14
+ /** Строка колонки или { alias: 'SQL выражение' } */
15
+ export type FieldEntry = string | Record<string, string>;
16
+ export type FilterDefMap<TRow extends Record<string, unknown>> = {
17
+ [K in RowKeys<TRow>]?: FilterFieldConfig<TRow>;
18
+ } & {
19
+ [key: string]: FilterFieldConfig<TRow> | undefined;
20
+ };
21
+ export type ForcedWhereMap<TRow extends Record<string, unknown>> = Partial<{
22
+ [K in RowKeys<TRow>]: unknown;
23
+ }>;
24
+ export interface QueryHandlerUrlQuery {
25
+ page?: number;
26
+ limit?: number;
27
+ /**
28
+ * Сортировка. Префикс `!` = ASC, без = DESC.
29
+ * Кастомные: `$popular`, `!$weighted`.
30
+ */
31
+ sort?: string | string[];
32
+ search?: string;
33
+ filter?: Record<string, unknown>;
34
+ relation?: string | string[];
35
+ after?: number | string;
36
+ before?: number | string;
37
+ scope?: string | string[];
38
+ field?: string | string[];
39
+ extend?: string | string[];
40
+ /** `?count=0` — отключить total; `?count=1` — явно включить (по умолчанию total включён) */
41
+ count?: 0 | 1 | boolean;
42
+ /** ?v=1 — версия ответа */
43
+ v?: number;
44
+ }
45
+ export type QueryHandlerQueryArg = Partial<QueryHandlerUrlQuery> | Record<string, unknown>;
46
+ export interface QueryHandlerContext {
47
+ page: number;
48
+ limit: number;
49
+ filters: Record<string, unknown>;
50
+ sort: string[];
51
+ search: string | undefined;
52
+ fields: string[];
53
+ relations: string[];
54
+ scopes: string[];
55
+ extends: string[];
56
+ }
57
+ /** Узкий контекст для beforeQuery / afterQuery (наблюдатели). */
58
+ export type QueryHandlerHookCtx = Pick<QueryHandlerContext, 'page' | 'limit' | 'filters'>;
59
+ export type FilterFieldType = 'string' | 'number' | 'date' | 'boolean';
60
+ export type FilterOperator = 'between' | 'in' | 'nin' | 'like' | 'nlike' | 'gt' | 'lt' | 'lte' | 'gte' | 'eq' | 'neq' | 'null' | 'nnull';
61
+ /** enum: массив | модель по primaryKey | { model, field } | async функция */
62
+ export type FilterEnumConfig = unknown[] | SchemaFactoryLike | {
63
+ model: SchemaFactoryLike;
64
+ field: string;
65
+ } | ((value: unknown) => void | Promise<void>);
66
+ export type FilterFieldConfig<TRow extends Record<string, unknown> = Record<string, unknown>> = true | FilterFieldType | FilterFieldFullConfig<TRow> | ((q: QueryBuilder, value: unknown) => void | Promise<void>);
67
+ export interface FilterFieldFullConfig<TRow extends Record<string, unknown> = Record<string, unknown>> {
68
+ /** Тип для парсинга из URL. Автоопределяется по имени поля. */
69
+ type?: FilterFieldType;
70
+ /** Разрешённые операторы. Дефолт — по типу. */
71
+ operators?: FilterOperator[];
72
+ /** Допустимые значения: массив / модель / { model, field } / функция */
73
+ enum?: FilterEnumConfig;
74
+ /** Реальная колонка в БД если отличается от ключа */
75
+ column?: string;
76
+ /** Разрешить null */
77
+ nullable?: boolean;
78
+ /** Автоматический `in` если передан список через запятую */
79
+ multiple?: boolean;
80
+ /** Дефолт если клиент не передал */
81
+ default?: unknown;
82
+ /** Активен только если указан другой фильтр */
83
+ dependsOn?: LooseRowKey<TRow> | LooseRowKey<TRow>[];
84
+ /** Взаимоисключающие фильтры */
85
+ conflictsWith?: LooseRowKey<TRow> | LooseRowKey<TRow>[];
86
+ /** Устаревший — логирует warning в dev */
87
+ deprecated?: string;
88
+ /**
89
+ * Включить/выключить. false → 403.
90
+ * Функция вызывается с контекстом запроса.
91
+ */
92
+ enable?: boolean | ((ctx: QueryHandlerContext) => boolean | Promise<boolean>);
93
+ /** Очистка ДО валидации */
94
+ sanitize?: (value: unknown) => unknown;
95
+ /** Трансформация ПОСЛЕ валидации, перед SQL */
96
+ transform?: (value: unknown) => unknown;
97
+ /** Кастомная валидация */
98
+ validate?: (value: unknown, ctx: QueryHandlerContext) => void | Promise<void>;
99
+ /** Описание для OpenAPI */
100
+ description?: string;
101
+ /** TTL кэша enum-проверки по модели (ms) */
102
+ cache?: number;
103
+ }
104
+ export interface RelationConfig<TRow extends Record<string, unknown> = Record<string, unknown>> {
105
+ /** Имя join из схемы → LEFT JOIN */
106
+ with?: string;
107
+ /** Всегда подключать без ?relation= */
108
+ required?: boolean;
109
+ /** true = LEFT JOIN, false = INNER JOIN */
110
+ nullable?: boolean;
111
+ /** Модель для отдельного запроса */
112
+ model?: SchemaFactoryLike;
113
+ /** Внешний ключ в связанной таблице */
114
+ foreign?: string;
115
+ /** Ключ в основной таблице (дефолт — primaryKey) */
116
+ dataKey?: string;
117
+ /** Максимум записей (только hasMany) */
118
+ limit?: number;
119
+ fields?: FieldEntry[];
120
+ fieldsDefault?: string[];
121
+ fieldsForced?: string[];
122
+ filters?: FilterDefMap<TRow>;
123
+ filtersDefault?: ForcedWhereMap<TRow>;
124
+ filtersRequired?: ForcedWhereMap<TRow>;
125
+ filtersForced?: ForcedWhereMap<TRow>;
126
+ sorts?: SortValue<TRow> | ReadonlyArray<SortValue<TRow>>;
127
+ sortsDefault?: string;
128
+ /**
129
+ * Хук над QueryBuilder.
130
+ * return void/builder → стандартная логика поверх.
131
+ * return false → полная замена.
132
+ */
133
+ query?: (q: QueryBuilder) => QueryBuilder | void | false | Promise<QueryBuilder | void | false>;
134
+ /** Выполнять только после указанного relation */
135
+ dependsOn?: string | string[];
136
+ /** Псевдоним ключа в результирующем объекте */
137
+ as?: string;
138
+ }
139
+ export type RelationMapInput<TRow extends Record<string, unknown> = Record<string, unknown>> = {
140
+ [name: string]: RelationConfig<TRow> | false;
141
+ } | ReadonlyArray<string>;
142
+ export interface ExtendHooks {
143
+ before?: (q: QueryBuilder, ctx: QueryHandlerContext) => void | Promise<void>;
144
+ after?: (items: unknown[], ctx: QueryHandlerContext) => unknown[] | Promise<unknown[]>;
145
+ afterEach?: (item: unknown, index: number, ctx: QueryHandlerContext) => unknown | Promise<unknown>;
146
+ }
147
+ export type ScopeHandler = (q: QueryBuilder, param?: string, ctx?: QueryHandlerContext) => void | Promise<void>;
148
+ /** Shorthand в `scopes`: применить `q.scope(имя ключа)` */
149
+ export type ScopeShorthand = true;
150
+ export type ScopeDefMap<TSchemaScopeNames extends string = string> = {
151
+ [K in TSchemaScopeNames]?: ScopeHandler | ScopeShorthand | false;
152
+ } & {
153
+ [scopeName: string]: ScopeHandler | ScopeShorthand | false;
154
+ };
155
+ export type ApplyObject = Partial<Omit<QueryHandlerOptions, 'apply'>>;
156
+ export type ApplyFn = (options: QueryHandlerOptions) => ApplyObject | void | false | Promise<ApplyObject | void | false>;
157
+ export type ApplyEntry = ApplyObject | ApplyFn | null | false | undefined;
158
+ export interface CursorPaginateConfig {
159
+ field?: string;
160
+ /** По умолчанию `true`. `false` или `?count=0` отключают COUNT. */
161
+ allowCount?: boolean;
162
+ stable?: boolean;
163
+ }
164
+ export interface ChunkConfig {
165
+ size: number;
166
+ handler: (items: unknown[], index: number, meta: ChunkMeta) => void | Promise<void>;
167
+ onDone?: (meta: ChunkMeta) => void | Promise<void>;
168
+ }
169
+ export interface ChunkMeta {
170
+ processed: number;
171
+ total: number;
172
+ index: number;
173
+ }
174
+ export type RandomizeConfig = boolean | {
175
+ type: 'weighted';
176
+ field: string;
177
+ limit?: number;
178
+ } | {
179
+ type: 'custom';
180
+ fn: (q: QueryBuilder) => void;
181
+ sort?: (items: unknown[]) => unknown[];
182
+ };
183
+ export interface RateLimitConfig {
184
+ window: number;
185
+ max: number;
186
+ key: (ctx: QueryHandlerContext) => string;
187
+ onExceed?: () => never;
188
+ }
189
+ export interface QueryHandlerOptions<TRow extends Record<string, unknown> = Record<string, unknown>, TSchemaScopeNames extends string = string> {
190
+ query?: QueryBuilder | null;
191
+ apply?: ApplyEntry[];
192
+ fields?: ReadonlyArray<FieldEntry>;
193
+ fieldsDefault?: ReadonlyArray<string>;
194
+ fieldsForced?: ReadonlyArray<string>;
195
+ sorts?: SortValue<TRow> | ReadonlyArray<SortValue<TRow>>;
196
+ sortsDefault?: SortValue<TRow>;
197
+ sortsCustom?: Record<string, (q: QueryBuilder, direction: 'ASC' | 'DESC', ctx: QueryHandlerContext) => void | false | Promise<void | false>>;
198
+ search?: ReadonlyArray<LooseRowKey<TRow>> | ((q: QueryBuilder, search: string, ctx: QueryHandlerContext) => void | Promise<void>);
199
+ filters?: FilterDefMap<TRow>;
200
+ filtersDefault?: ForcedWhereMap<TRow>;
201
+ filtersRequired?: ForcedWhereMap<TRow>;
202
+ filtersForced?: ForcedWhereMap<TRow>;
203
+ requiredFilters?: ReadonlyArray<LooseRowKey<TRow>>;
204
+ strictFilters?: boolean;
205
+ /** Объект конфигов или shorthand: `['orders', 'profile']` → `{ with: name }` */
206
+ relations?: RelationMapInput<TRow>;
207
+ /** Подключать, если клиент не передал `?relation=` */
208
+ relationsDefault?: string | string[] | ReadonlyArray<string>;
209
+ cursorPaginate?: boolean | string | CursorPaginateConfig;
210
+ withTotal?: boolean;
211
+ maxLimit?: number;
212
+ /** `true` → `(q) => q.scope(имя ключа)` */
213
+ scopes?: ScopeDefMap<TSchemaScopeNames>;
214
+ /** Применять, если клиент не передал `?scope=` */
215
+ scopesDefault?: string | string[] | ReadonlyArray<string>;
216
+ extends?: Record<string, ExtendHooks | false>;
217
+ before?: (q: QueryBuilder, ctx: QueryHandlerContext) => void | Promise<void>;
218
+ beforeQuery?: (q: QueryBuilder, ctx: QueryHandlerHookCtx) => void | Promise<void>;
219
+ after?: (items: unknown[], ctx: QueryHandlerContext) => unknown[] | Promise<unknown[]>;
220
+ afterQuery?: (items: unknown[], total: number, ctx: QueryHandlerHookCtx) => void | Promise<void>;
221
+ afterEach?: (item: unknown, index: number, ctx: QueryHandlerContext) => unknown | Promise<unknown>;
222
+ transform?: (row: unknown, index: number) => unknown;
223
+ fieldsTransform?: Record<string, (value: unknown, item: unknown) => unknown>;
224
+ ignoreQueryFields?: boolean;
225
+ aggregate?: Record<string, string>;
226
+ groupBy?: ReadonlyArray<string>;
227
+ having?: Record<string, unknown>;
228
+ distinct?: boolean;
229
+ mode?: 'one' | 'many';
230
+ transaction?: unknown;
231
+ onEmpty?: 'error' | 'null' | ((ctx: QueryHandlerContext) => unknown[] | Promise<unknown[]>);
232
+ versions?: Record<number, (items: unknown[]) => unknown[]>;
233
+ chunk?: ChunkConfig;
234
+ randomize?: RandomizeConfig;
235
+ rateLimit?: RateLimitConfig;
236
+ cacheTtl?: number;
237
+ cacheKey?: string | ((ctx: QueryHandlerContext) => string);
238
+ meta?: boolean;
239
+ mock?: unknown[];
240
+ initial?: Partial<Omit<QueryHandlerUrlQuery, 'field' | 'filter'> & {
241
+ fields?: ReadonlyArray<LooseRowKey<TRow>>;
242
+ filters?: ForcedWhereMap<TRow>;
243
+ }>;
244
+ limits?: {
245
+ searchMinLength?: number;
246
+ searchMaxLength?: number;
247
+ allowedOperators?: FilterOperator[];
248
+ maxSortFields?: number;
249
+ emptyValues?: unknown[];
250
+ };
251
+ expose?: {
252
+ total?: boolean;
253
+ page?: boolean;
254
+ pageSize?: boolean;
255
+ meta?: boolean;
256
+ };
257
+ }
258
+ export type QueryHandlerCallOptions<M extends SchemaFactoryLike> = QueryHandlerOptions<InferSchemaRow<M>, InferSchemaScopeNames<M>>;
259
+ export type QueryHandlerBuilderOptions = QueryHandlerOptions & {
260
+ query: QueryBuilder;
261
+ };
262
+ export type QueryHandlerFilter<M extends SchemaFactoryLike> = FilterDefMap<InferSchemaRow<M>>;
263
+ export type QueryHandlerFilterForced<M extends SchemaFactoryLike> = ForcedWhereMap<InferSchemaRow<M>>;
264
+ export type QueryHandlerInitial<M extends SchemaFactoryLike> = QueryHandlerOptions<InferSchemaRow<M>>['initial'];
265
+ export type WorkerOptions = QueryHandlerOptions & {
266
+ model?: SchemaFactoryLike;
267
+ };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/util.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ /** Минимальные хелперы без зависимости от app utils. */
2
+ export declare function toArray<T>(value: T | T[]): T[];
3
+ export declare function spliter(str: string, separator: string): string[];
4
+ export declare function isAllowed(input: string[], whitelist: string[]): boolean;
package/dist/util.js ADDED
@@ -0,0 +1,11 @@
1
+ /** Минимальные хелперы без зависимости от app utils. */
2
+ export function toArray(value) {
3
+ return Array.isArray(value) ? value : value ? [value] : [];
4
+ }
5
+ export function spliter(str, separator) {
6
+ return str.split(separator).map((v) => v.trim());
7
+ }
8
+ export function isAllowed(input, whitelist) {
9
+ const whiteSet = new Set(whitelist);
10
+ return input.every((item) => whiteSet.has(item));
11
+ }
@@ -0,0 +1,19 @@
1
+ import type { QueryBuilder } from "@crossfox/db";
2
+ import type { WorkerOptions, QueryHandlerOptions, QueryHandlerContext, QueryHandlerHookCtx } from "./types";
3
+ export declare function queryHandlerWorker(query: import("./types").QueryHandlerQueryArg, optionsArgs?: WorkerOptions): QueryHandlerWorkerResult;
4
+ /** Публичный результат queryHandler (без Symbol brand в .d.ts). */
5
+ export type QueryHandlerWorkerResult = {
6
+ $q: QueryBuilder;
7
+ items: unknown[];
8
+ total: number;
9
+ nextId: unknown;
10
+ prevId: unknown;
11
+ context: QueryHandlerContext | null;
12
+ aggregate: Record<string, unknown> | null;
13
+ addField(name: string | string[]): QueryHandlerWorkerResult;
14
+ beforeQuery(fn: (q: QueryBuilder, ctx: QueryHandlerHookCtx) => void | Promise<void>): QueryHandlerWorkerResult;
15
+ when(condition: boolean | (() => boolean), fn: (options: QueryHandlerOptions) => Partial<QueryHandlerOptions> | void): QueryHandlerWorkerResult;
16
+ clone(patch: Partial<QueryHandlerOptions>): QueryHandlerWorkerResult;
17
+ query(): Promise<unknown[]>;
18
+ run(): Promise<Record<string, unknown>>;
19
+ };