@querykitjs/mongoose 1.0.0 → 2.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/README.en.md +121 -1
- package/README.md +121 -1
- package/dist/index.cjs +134 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +90 -50
- package/dist/index.d.ts +90 -50
- package/dist/index.js +135 -38
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Types, Model, Connection, ClientSession } from 'mongoose';
|
|
2
2
|
import * as _querykitjs_core from '@querykitjs/core';
|
|
3
|
+
import { SkippedCondition } from '@querykitjs/core';
|
|
3
4
|
export { f } from '@querykitjs/core';
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -9,10 +10,8 @@ export { f } from '@querykitjs/core';
|
|
|
9
10
|
* escape-hatch bound to a native Mongo `FilterQuery`.
|
|
10
11
|
*
|
|
11
12
|
* The public surface mirrors `@querykitjs/drizzle-pg` so the same frontend
|
|
12
|
-
* request contract works regardless of backend
|
|
13
|
-
*
|
|
14
|
-
* NOTE: Stage 1 exposes only read methods (+ scoped). Writes/upsert/aggregate/
|
|
15
|
-
* transaction land in stage 2.
|
|
13
|
+
* request contract works regardless of backend: same option names, same
|
|
14
|
+
* semantics, one shared source in core.
|
|
16
15
|
*/
|
|
17
16
|
|
|
18
17
|
/** Plain (lean) document type — what reads return. */
|
|
@@ -33,18 +32,25 @@ type Id = string | number | Types.ObjectId;
|
|
|
33
32
|
type RawFilter<_TDoc = unknown> = Record<string, unknown>;
|
|
34
33
|
/** Union of a document's field keys. */
|
|
35
34
|
type FieldKey<TDoc> = keyof TDoc & string;
|
|
35
|
+
/**
|
|
36
|
+
* A field key that also accepts any `string`. Filter/sort keys arrive from the
|
|
37
|
+
* wire as plain strings and the adapter skips the ones it cannot resolve, so a
|
|
38
|
+
* validated payload can go straight into a repository call with no `as` cast.
|
|
39
|
+
* Autocomplete still suggests the document's real fields.
|
|
40
|
+
*/
|
|
41
|
+
type LooseFieldKey<TDoc> = _querykitjs_core.LooseKey<FieldKey<TDoc>>;
|
|
36
42
|
type FilterOperator = _querykitjs_core.FilterOperator;
|
|
37
43
|
type FilterScalar = _querykitjs_core.FilterScalar;
|
|
38
44
|
type FilterValue = _querykitjs_core.FilterValue;
|
|
39
|
-
type FieldCondition<TDoc = Record<string, unknown>> = _querykitjs_core.FieldCondition<
|
|
40
|
-
type AndGroup<TDoc = Record<string, unknown>> = _querykitjs_core.AndGroup<
|
|
41
|
-
type OrGroup<TDoc = Record<string, unknown>> = _querykitjs_core.OrGroup<
|
|
42
|
-
type NotGroup<TDoc = Record<string, unknown>> = _querykitjs_core.NotGroup<
|
|
43
|
-
type FilterNode<TDoc = Record<string, unknown>> = _querykitjs_core.FilterNode<
|
|
44
|
-
type Filter<TDoc = Record<string, unknown>> = _querykitjs_core.Filter<
|
|
45
|
+
type FieldCondition<TDoc = Record<string, unknown>> = _querykitjs_core.FieldCondition<LooseFieldKey<TDoc>>;
|
|
46
|
+
type AndGroup<TDoc = Record<string, unknown>> = _querykitjs_core.AndGroup<LooseFieldKey<TDoc>, RawFilter<TDoc>>;
|
|
47
|
+
type OrGroup<TDoc = Record<string, unknown>> = _querykitjs_core.OrGroup<LooseFieldKey<TDoc>, RawFilter<TDoc>>;
|
|
48
|
+
type NotGroup<TDoc = Record<string, unknown>> = _querykitjs_core.NotGroup<LooseFieldKey<TDoc>, RawFilter<TDoc>>;
|
|
49
|
+
type FilterNode<TDoc = Record<string, unknown>> = _querykitjs_core.FilterNode<LooseFieldKey<TDoc>, RawFilter<TDoc>>;
|
|
50
|
+
type Filter<TDoc = Record<string, unknown>> = _querykitjs_core.Filter<LooseFieldKey<TDoc>, RawFilter<TDoc>>;
|
|
45
51
|
type SortDirection = _querykitjs_core.SortDirection;
|
|
46
52
|
/** One sort field by typed field key (`{ key, direction }`) — autocomplete. */
|
|
47
|
-
type SortItem<TDoc = Record<string, unknown>> = _querykitjs_core.SortItem<
|
|
53
|
+
type SortItem<TDoc = Record<string, unknown>> = _querykitjs_core.SortItem<LooseFieldKey<TDoc>>;
|
|
48
54
|
/**
|
|
49
55
|
* Sort — **always an array** of `{ key, direction }` (multi-field). This is the
|
|
50
56
|
* exact shape the querykit frontend sends (`@querykitjs/web` decodes its
|
|
@@ -63,36 +69,15 @@ interface QueryParams<TDoc = Record<string, unknown>> {
|
|
|
63
69
|
withDeleted?: boolean;
|
|
64
70
|
}
|
|
65
71
|
/** Equality scope applied to every operation of a scoped repository. */
|
|
66
|
-
type Scope<TDoc = Record<string, unknown>> =
|
|
72
|
+
type Scope<TDoc = Record<string, unknown>> = _querykitjs_core.Scope<FieldKey<TDoc>>;
|
|
67
73
|
interface ByIdParams<TDoc = Record<string, unknown>> extends QueryParams<TDoc> {
|
|
68
74
|
/** Field to match against the id. Defaults to `"id"` (→ `_id`). */
|
|
69
|
-
idKey?:
|
|
75
|
+
idKey?: LooseFieldKey<TDoc>;
|
|
70
76
|
}
|
|
71
77
|
/** Options for {@link Repository.upsert} / {@link Repository.upsertMany}. */
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
/** Fields to update on conflict. Defaults to the inserted values minus `target`. */
|
|
76
|
-
set?: Partial<Insert<TDoc>>;
|
|
77
|
-
}
|
|
78
|
-
type AggKeys<TDoc> = FieldKey<TDoc> | FieldKey<TDoc>[];
|
|
79
|
-
/** Aggregate query specification (mirrors drizzle-pg). */
|
|
80
|
-
interface AggregateSpec<TDoc = Record<string, unknown>> {
|
|
81
|
-
filter?: Filter<TDoc>;
|
|
82
|
-
/** Group by these field(s); each appears in the output rows. */
|
|
83
|
-
groupBy?: AggKeys<TDoc>;
|
|
84
|
-
/** `$sum:1` → `count`. */
|
|
85
|
-
count?: boolean;
|
|
86
|
-
/** `$sum` → `sum_<field>`. */
|
|
87
|
-
sum?: AggKeys<TDoc>;
|
|
88
|
-
/** `$avg` → `avg_<field>`. */
|
|
89
|
-
avg?: AggKeys<TDoc>;
|
|
90
|
-
/** `$min` → `min_<field>`. */
|
|
91
|
-
min?: AggKeys<TDoc>;
|
|
92
|
-
/** `$max` → `max_<field>`. */
|
|
93
|
-
max?: AggKeys<TDoc>;
|
|
94
|
-
withDeleted?: boolean;
|
|
95
|
-
}
|
|
78
|
+
type UpsertOptions<TDoc = Record<string, unknown>> = _querykitjs_core.UpsertOptions<FieldKey<TDoc>, Insert<TDoc>>;
|
|
79
|
+
/** Aggregate query specification (`count`/`sum`/`avg`/`min`/`max` + `groupBy`). */
|
|
80
|
+
type AggregateSpec<TDoc = Record<string, unknown>> = _querykitjs_core.AggregateSpec<FieldKey<TDoc>, Filter<TDoc>>;
|
|
96
81
|
/** One aggregated result row (group fields + aggregate values). */
|
|
97
82
|
type AggregateRow = Record<string, unknown>;
|
|
98
83
|
interface OffsetParams<TDoc = Record<string, unknown>> extends QueryParams<TDoc> {
|
|
@@ -117,7 +102,7 @@ interface CursorParams<TDoc = Record<string, unknown>> extends QueryParams<TDoc>
|
|
|
117
102
|
limit?: number;
|
|
118
103
|
cursor?: string | null;
|
|
119
104
|
/** Field the cursor walks over. Defaults to `"id"` (→ `_id`). */
|
|
120
|
-
cursorKey?:
|
|
105
|
+
cursorKey?: LooseFieldKey<TDoc>;
|
|
121
106
|
order?: SortDirection;
|
|
122
107
|
direction?: "forward" | "backward";
|
|
123
108
|
}
|
|
@@ -133,10 +118,37 @@ type DocOf<M> = M extends Model<infer T> ? T : never;
|
|
|
133
118
|
type RelationDocs<TRel extends RelationMap> = {
|
|
134
119
|
[K in keyof TRel]: DocOf<TRel[K]>;
|
|
135
120
|
};
|
|
136
|
-
/**
|
|
137
|
-
|
|
121
|
+
/**
|
|
122
|
+
* Options for `registry.repository(model, options)`.
|
|
123
|
+
*
|
|
124
|
+
* Philosophy: the guard lives in the **repository**, not the route — `scope`
|
|
125
|
+
* (RBAC) already worked that way and projection joins it. Same names and same
|
|
126
|
+
* semantics as the drizzle-pg adapter's `RepositoryOptions`.
|
|
127
|
+
*
|
|
128
|
+
* ⚠️ `forcedColumns`/`allowedColumns` affect **read** methods only
|
|
129
|
+
* (`findAll`/`findOne`/`findById`/`findList`/`findInfinite`/`findCursor` +
|
|
130
|
+
* `aggregate`). Write methods (`create`, `upsert`, `updateById`, `softDelete`, …)
|
|
131
|
+
* return the full `Row` by type contract — trimming it at runtime would make the
|
|
132
|
+
* type a lie. Re-read with `findById`, or map it yourself, before handing a write
|
|
133
|
+
* result to a client.
|
|
134
|
+
*/
|
|
135
|
+
interface RepositoryOptions<TDoc = any, TRel extends RelationMap = RelationMap> {
|
|
138
136
|
/** Declare populatable relations (field → model) so `with` types the result. */
|
|
139
137
|
relations?: TRel;
|
|
138
|
+
/** Constant equality filter added to every read/write (RBAC / multi-tenancy). */
|
|
139
|
+
scope?: Scope<TDoc>;
|
|
140
|
+
/**
|
|
141
|
+
* Forced projection — the client's `columns` is **ignored entirely**. Keeps a
|
|
142
|
+
* password out of a `users` response without a `{ ...params, columns }` trick in
|
|
143
|
+
* every route. Must select at least one field, else building the repository throws.
|
|
144
|
+
*/
|
|
145
|
+
forcedColumns?: ColumnSelection<TDoc>;
|
|
146
|
+
/**
|
|
147
|
+
* Allowlist — the client's `columns` is intersected with it. An empty
|
|
148
|
+
* intersection yields the allowlist itself, **never** the full document.
|
|
149
|
+
* An empty array throws (a projection selecting nothing means "everything").
|
|
150
|
+
*/
|
|
151
|
+
allowedColumns?: readonly FieldKey<TDoc>[];
|
|
140
152
|
}
|
|
141
153
|
type TrueKeys<T> = {
|
|
142
154
|
[K in keyof T]: T[K] extends true ? K : never;
|
|
@@ -154,7 +166,7 @@ type Params<TBase, TColumns, TWith> = Omit<TBase, "columns" | "with"> & {
|
|
|
154
166
|
with?: TWith;
|
|
155
167
|
};
|
|
156
168
|
/**
|
|
157
|
-
*
|
|
169
|
+
* Model-scoped repository. Read methods infer their
|
|
158
170
|
* return type from the `columns` argument, mirroring `@querykitjs/drizzle-pg`.
|
|
159
171
|
*/
|
|
160
172
|
interface Repository<TDoc = Record<string, unknown>, TRel extends Record<string, unknown> = Record<never, never>> {
|
|
@@ -192,12 +204,35 @@ interface Repository<TDoc = Record<string, unknown>, TRel extends Record<string,
|
|
|
192
204
|
aggregate(spec: AggregateSpec<TDoc>): Promise<AggregateRow[]>;
|
|
193
205
|
scoped(scope: Scope<TDoc>): Repository<TDoc, TRel>;
|
|
194
206
|
}
|
|
195
|
-
/** {@link createRegistry} options — pagination defaults. */
|
|
207
|
+
/** {@link createRegistry} options — pagination defaults and bounds. */
|
|
196
208
|
interface RegistryOptions {
|
|
197
209
|
/** `findList` default page size (core `DEFAULT_PER_PAGE` = 20). */
|
|
198
210
|
defaultPerPage?: number;
|
|
199
211
|
/** `findInfinite`/`findCursor` default limit (core `DEFAULT_LIMIT` = 20). */
|
|
200
212
|
defaultLimit?: number;
|
|
213
|
+
/**
|
|
214
|
+
* Upper bound for `perPage` (core `DEFAULT_MAX_PER_PAGE` = 200). Applied even
|
|
215
|
+
* when validation was bypassed — defense in depth. Disable with `Infinity`.
|
|
216
|
+
*/
|
|
217
|
+
maxPerPage?: number;
|
|
218
|
+
/** Upper bound for `limit` (`findInfinite`/`findCursor`, core `DEFAULT_MAX_LIMIT` = 200). */
|
|
219
|
+
maxLimit?: number;
|
|
220
|
+
/**
|
|
221
|
+
* **Strict mode.** An unknown filter/sort key, or a value the operator cannot
|
|
222
|
+
* use, is no longer dropped silently — a `QueryKitError` is thrown (the backend
|
|
223
|
+
* maps it to a 400). Defaults to `false`, keeping the legacy (DbService)
|
|
224
|
+
* behaviour that dozens of existing endpoints rely on.
|
|
225
|
+
*
|
|
226
|
+
* Why it matters: a filter is meant to **narrow** a result set, so a mistyped
|
|
227
|
+
* key that vanishes silently makes the endpoint return **more** data than
|
|
228
|
+
* intended, with nothing in the log to show it.
|
|
229
|
+
*/
|
|
230
|
+
strict?: boolean;
|
|
231
|
+
/**
|
|
232
|
+
* Called for every dropped condition (also when `strict` is off). Migration
|
|
233
|
+
* path: watch with the hook first, flip `strict: true` once the log is clean.
|
|
234
|
+
*/
|
|
235
|
+
onSkippedCondition?: (info: _querykitjs_core.SkippedCondition) => void;
|
|
201
236
|
}
|
|
202
237
|
/** Extends a base repository with custom, model-specific methods (drizzle-pg style). */
|
|
203
238
|
type RepositoryExtender<TDoc, TRel extends Record<string, unknown>, TExt extends Record<string, unknown>> = (base: Repository<TDoc, TRel>) => TExt;
|
|
@@ -208,9 +243,9 @@ interface Registry {
|
|
|
208
243
|
/** …extended with custom methods (2nd-arg extender fn — same as drizzle-pg). */
|
|
209
244
|
repository<TDoc, TExt extends Record<string, unknown>>(model: Model<TDoc>, extend: RepositoryExtender<TDoc, Record<never, never>, TExt>): Repository<TDoc> & TExt;
|
|
210
245
|
/** …with typed relations so populated `with` fields are typed. */
|
|
211
|
-
repository<TDoc, TRel extends RelationMap>(model: Model<TDoc>, options: RepositoryOptions<TRel>): Repository<TDoc, RelationDocs<TRel>>;
|
|
246
|
+
repository<TDoc, TRel extends RelationMap>(model: Model<TDoc>, options: RepositoryOptions<TDoc, TRel>): Repository<TDoc, RelationDocs<TRel>>;
|
|
212
247
|
/** …with both typed relations and custom methods. */
|
|
213
|
-
repository<TDoc, TRel extends RelationMap, TExt extends Record<string, unknown>>(model: Model<TDoc>, options: RepositoryOptions<TRel>, extend: RepositoryExtender<TDoc, RelationDocs<TRel>, TExt>): Repository<TDoc, RelationDocs<TRel>> & TExt;
|
|
248
|
+
repository<TDoc, TRel extends RelationMap, TExt extends Record<string, unknown>>(model: Model<TDoc>, options: RepositoryOptions<TDoc, TRel>, extend: RepositoryExtender<TDoc, RelationDocs<TRel>, TExt>): Repository<TDoc, RelationDocs<TRel>> & TExt;
|
|
214
249
|
/** Runs `fn` inside a MongoDB transaction (requires a replica set). */
|
|
215
250
|
transaction<T>(fn: () => Promise<T>): Promise<T>;
|
|
216
251
|
}
|
|
@@ -238,16 +273,20 @@ interface RepoRuntime {
|
|
|
238
273
|
getSession: () => ClientSession | undefined;
|
|
239
274
|
defaultPerPage: number;
|
|
240
275
|
defaultLimit: number;
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
276
|
+
/** Upper bound for `perPage`, applied even when validation was bypassed. */
|
|
277
|
+
maxPerPage: number;
|
|
278
|
+
/** Upper bound for `limit` (`findInfinite`/`findCursor`). */
|
|
279
|
+
maxLimit: number;
|
|
280
|
+
/** Throw `QueryKitError` instead of dropping an unresolvable condition. */
|
|
281
|
+
strict: boolean;
|
|
282
|
+
/** Called for every dropped condition (also when `strict` is off). */
|
|
283
|
+
onSkippedCondition?: (info: SkippedCondition) => void;
|
|
245
284
|
}
|
|
246
285
|
/**
|
|
247
286
|
* Build a model-scoped repository. `runtime` (session getter + defaults) comes
|
|
248
287
|
* from the registry, so this file never touches the connection directly.
|
|
249
288
|
*/
|
|
250
|
-
declare function buildRepository<TDoc>(runtime: RepoRuntime, model: Model<TDoc>, config?:
|
|
289
|
+
declare function buildRepository<TDoc>(runtime: RepoRuntime, model: Model<TDoc>, config?: RepositoryOptions<TDoc>): Repository<TDoc>;
|
|
251
290
|
|
|
252
291
|
/**
|
|
253
292
|
* Typed, ergonomic filter builders for a document type (field-name autocomplete).
|
|
@@ -276,6 +315,7 @@ declare function createFilters<TDoc>(): {
|
|
|
276
315
|
in: (key: FieldKey<TDoc>, value: Array<string | number>) => _querykitjs_core.FieldCondition<FieldKey<TDoc>>;
|
|
277
316
|
notIn: (key: FieldKey<TDoc>, value: Array<string | number>) => _querykitjs_core.FieldCondition<FieldKey<TDoc>>;
|
|
278
317
|
between: (key: FieldKey<TDoc>, min: _querykitjs_core.FilterScalar | Date, max: _querykitjs_core.FilterScalar | Date) => _querykitjs_core.FieldCondition<FieldKey<TDoc>>;
|
|
318
|
+
notBetween: (key: FieldKey<TDoc>, min: _querykitjs_core.FilterScalar | Date, max: _querykitjs_core.FilterScalar | Date) => _querykitjs_core.FieldCondition<FieldKey<TDoc>>;
|
|
279
319
|
range: (key: FieldKey<TDoc>, from: _querykitjs_core.FilterScalar | Date, to: _querykitjs_core.FilterScalar | Date) => _querykitjs_core.FieldCondition<FieldKey<TDoc>>[];
|
|
280
320
|
isNull: (key: FieldKey<TDoc>) => _querykitjs_core.FieldCondition<FieldKey<TDoc>>;
|
|
281
321
|
isNotNull: (key: FieldKey<TDoc>) => _querykitjs_core.FieldCondition<FieldKey<TDoc>>;
|
|
@@ -284,4 +324,4 @@ declare function createFilters<TDoc>(): {
|
|
|
284
324
|
not: (node: _querykitjs_core.FilterNode<FieldKey<TDoc>, RawFilter<_TDoc>>) => _querykitjs_core.NotGroup<FieldKey<TDoc>, RawFilter<_TDoc>>;
|
|
285
325
|
};
|
|
286
326
|
|
|
287
|
-
export { type AggregateRow, type AggregateSpec, type AndGroup, type ByIdParams, type ColumnSelection, type CursorMeta, type CursorParams, type CursorResult, type FieldCondition, type FieldKey, type Filter, type FilterNode, type FilterOperator, type FilterScalar, type FilterValue, type Id, type InfiniteMeta, type InfiniteParams, type InfiniteResult, type Insert, type NotGroup, type OffsetMeta, type OffsetParams, type OffsetResult, type OrGroup, type QueryParams, type RawFilter, type Registry, type RegistryOptions, type RelationDocs, type RelationMap, type
|
|
327
|
+
export { type AggregateRow, type AggregateSpec, type AndGroup, type ByIdParams, type ColumnSelection, type CursorMeta, type CursorParams, type CursorResult, type FieldCondition, type FieldKey, type Filter, type FilterNode, type FilterOperator, type FilterScalar, type FilterValue, type Id, type InfiniteMeta, type InfiniteParams, type InfiniteResult, type Insert, type LooseFieldKey, type NotGroup, type OffsetMeta, type OffsetParams, type OffsetResult, type OrGroup, type QueryParams, type RawFilter, type Registry, type RegistryOptions, type RelationDocs, type RelationMap, type RepoRuntime, type Repository, type RepositoryExtender, type RepositoryOptions, type Result, type Row, type Scope, type Sort, type SortDirection, type SortItem, type UpsertOptions, type WithRelations, buildRepository, createFilters, createRegistry };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEFAULT_LIMIT, DEFAULT_PER_PAGE, createFilters as createFilters$1 } from '@querykitjs/core';
|
|
1
|
+
import { TEXT_FILTER_OPERATORS, QueryKitError, DEFAULT_MAX_LIMIT, DEFAULT_MAX_PER_PAGE, DEFAULT_LIMIT, DEFAULT_PER_PAGE, createFilters as createFilters$1 } from '@querykitjs/core';
|
|
2
2
|
export { f } from '@querykitjs/core';
|
|
3
3
|
import { AsyncLocalStorage } from 'async_hooks';
|
|
4
4
|
|
|
@@ -22,6 +22,44 @@ function castValue(model, field, value) {
|
|
|
22
22
|
return value;
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
|
+
var TEXT_OPERATORS = new Set(TEXT_FILTER_OPERATORS);
|
|
26
|
+
var INVALID_VALUE = /* @__PURE__ */ Symbol("querykit.invalid-value");
|
|
27
|
+
var isTextOperator = (operator) => TEXT_OPERATORS.has(operator);
|
|
28
|
+
function isDatePath(model, field) {
|
|
29
|
+
return model.schema.path(field)?.instance === "Date";
|
|
30
|
+
}
|
|
31
|
+
function castForPath(model, field, value) {
|
|
32
|
+
const path = model.schema.path(field);
|
|
33
|
+
if (!path || path.instance !== "Date" || value === null || value === void 0) return value;
|
|
34
|
+
if (value instanceof Date) return Number.isNaN(value.getTime()) ? INVALID_VALUE : value;
|
|
35
|
+
try {
|
|
36
|
+
const cast = path.cast(value);
|
|
37
|
+
if (cast instanceof Date) return Number.isNaN(cast.getTime()) ? INVALID_VALUE : cast;
|
|
38
|
+
return cast ?? INVALID_VALUE;
|
|
39
|
+
} catch {
|
|
40
|
+
return INVALID_VALUE;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function coerceCondition(model, field, operator, value) {
|
|
44
|
+
if (value === void 0 || TEXT_OPERATORS.has(operator)) return value;
|
|
45
|
+
if (Array.isArray(value)) {
|
|
46
|
+
const items = [];
|
|
47
|
+
for (const item of value) {
|
|
48
|
+
const cast2 = castForPath(model, field, item);
|
|
49
|
+
if (cast2 === INVALID_VALUE) return INVALID_VALUE;
|
|
50
|
+
items.push(cast2);
|
|
51
|
+
}
|
|
52
|
+
return items;
|
|
53
|
+
}
|
|
54
|
+
const cast = castForPath(model, field, value);
|
|
55
|
+
return cast === INVALID_VALUE ? INVALID_VALUE : cast;
|
|
56
|
+
}
|
|
57
|
+
function reportSkip(diag, info) {
|
|
58
|
+
if (!diag) return;
|
|
59
|
+
const full = { ...info, source: diag.source };
|
|
60
|
+
if (diag.strict) throw new QueryKitError(full);
|
|
61
|
+
diag.onSkipped?.(full);
|
|
62
|
+
}
|
|
25
63
|
|
|
26
64
|
// src/internal/operators.ts
|
|
27
65
|
var esc = (s) => String(s ?? "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -83,32 +121,47 @@ function combine(kind, parts) {
|
|
|
83
121
|
if (valid.length === 1) return valid[0];
|
|
84
122
|
return { [kind]: valid };
|
|
85
123
|
}
|
|
86
|
-
function buildCondition(model, cond) {
|
|
124
|
+
function buildCondition(model, cond, diag) {
|
|
87
125
|
const operator = cond.operation ?? "=";
|
|
88
126
|
const build = operators[operator];
|
|
89
127
|
if (!build) throw new Error(`Unsupported filter operator: "${operator}"`);
|
|
90
128
|
const field = resolveField(model, cond.key);
|
|
91
|
-
if (!field)
|
|
92
|
-
|
|
93
|
-
|
|
129
|
+
if (!field) {
|
|
130
|
+
reportSkip(diag, { site: "filter", key: cond.key, operation: operator, reason: "unknown-key" });
|
|
131
|
+
return void 0;
|
|
132
|
+
}
|
|
133
|
+
if (isTextOperator(operator) && isDatePath(model, field)) {
|
|
134
|
+
reportSkip(diag, { site: "filter", key: cond.key, operation: operator, reason: "invalid-value" });
|
|
135
|
+
return void 0;
|
|
136
|
+
}
|
|
137
|
+
const value = coerceCondition(model, field, operator, cond.value);
|
|
138
|
+
if (value === INVALID_VALUE) {
|
|
139
|
+
reportSkip(diag, { site: "filter", key: cond.key, operation: operator, reason: "invalid-value" });
|
|
140
|
+
return void 0;
|
|
141
|
+
}
|
|
142
|
+
const fragment = build(value);
|
|
143
|
+
if (fragment === void 0) {
|
|
144
|
+
reportSkip(diag, { site: "filter", key: cond.key, operation: operator, reason: "invalid-value" });
|
|
145
|
+
return void 0;
|
|
146
|
+
}
|
|
94
147
|
return { [field]: fragment };
|
|
95
148
|
}
|
|
96
|
-
function buildNode(model, node) {
|
|
97
|
-
if (isFieldCondition(node)) return buildCondition(model, node);
|
|
149
|
+
function buildNode(model, node, diag) {
|
|
150
|
+
if (isFieldCondition(node)) return buildCondition(model, node, diag);
|
|
98
151
|
if (isObject(node)) {
|
|
99
152
|
if ("and" in node)
|
|
100
153
|
return combine(
|
|
101
154
|
"$and",
|
|
102
|
-
node.and.map((n) => buildNode(model, n))
|
|
155
|
+
node.and.map((n) => buildNode(model, n, diag))
|
|
103
156
|
);
|
|
104
157
|
if ("or" in node)
|
|
105
158
|
return combine(
|
|
106
159
|
"$or",
|
|
107
|
-
node.or.map((n) => buildNode(model, n))
|
|
160
|
+
node.or.map((n) => buildNode(model, n, diag))
|
|
108
161
|
);
|
|
109
162
|
if ("not" in node) {
|
|
110
163
|
const notNode = node.not;
|
|
111
|
-
const inner = buildNode(model, notNode);
|
|
164
|
+
const inner = buildNode(model, notNode, diag);
|
|
112
165
|
if (!inner) return void 0;
|
|
113
166
|
if (isFieldCondition(notNode)) {
|
|
114
167
|
const op = notNode.operation ?? "=";
|
|
@@ -123,14 +176,14 @@ function buildNode(model, node) {
|
|
|
123
176
|
}
|
|
124
177
|
return void 0;
|
|
125
178
|
}
|
|
126
|
-
function buildWhere(model, filter) {
|
|
179
|
+
function buildWhere(model, filter, diag) {
|
|
127
180
|
if (filter === void 0) return void 0;
|
|
128
181
|
if (Array.isArray(filter))
|
|
129
182
|
return combine(
|
|
130
183
|
"$and",
|
|
131
|
-
filter.map((c) => buildNode(model, c))
|
|
184
|
+
filter.map((c) => buildNode(model, c, diag))
|
|
132
185
|
);
|
|
133
|
-
return buildNode(model, filter);
|
|
186
|
+
return buildNode(model, filter, diag);
|
|
134
187
|
}
|
|
135
188
|
|
|
136
189
|
// src/internal/order-by.ts
|
|
@@ -138,11 +191,12 @@ function normalize(sort) {
|
|
|
138
191
|
if (!sort) return [];
|
|
139
192
|
return sort.filter((item) => item && item.key).map((item) => ({ key: item.key, direction: item.direction ?? "asc" }));
|
|
140
193
|
}
|
|
141
|
-
function buildSort(model, sort) {
|
|
194
|
+
function buildSort(model, sort, diag) {
|
|
142
195
|
const spec = {};
|
|
143
196
|
for (const item of normalize(sort)) {
|
|
144
197
|
const field = resolveField(model, item.key);
|
|
145
198
|
if (field) spec[field] = item.direction === "desc" ? -1 : 1;
|
|
199
|
+
else reportSkip(diag, { site: "sort", key: item.key, reason: "unknown-key" });
|
|
146
200
|
}
|
|
147
201
|
if (Object.keys(spec).length === 0) {
|
|
148
202
|
if (hasPath(model, "createdAt")) spec.createdAt = -1;
|
|
@@ -169,6 +223,7 @@ function decodeCursor(cursor) {
|
|
|
169
223
|
}
|
|
170
224
|
|
|
171
225
|
// src/repository.ts
|
|
226
|
+
var clampPageSize = (requested, fallback, max) => Math.min(max, Math.max(1, Math.trunc(requested ?? fallback)));
|
|
172
227
|
function buildRepository(runtime, model, config = {}) {
|
|
173
228
|
const m = model;
|
|
174
229
|
const hasDeletedAt = hasPath(m, "deletedAt");
|
|
@@ -176,12 +231,24 @@ function buildRepository(runtime, model, config = {}) {
|
|
|
176
231
|
const stampUpdatedAt = hasPath(m, "updatedAt") && !timestampsOn;
|
|
177
232
|
const forUpdate = (patch) => stampUpdatedAt ? { ...patch, updatedAt: /* @__PURE__ */ new Date() } : patch;
|
|
178
233
|
const scopeConditions = config.scope ? Object.entries(config.scope).map(([key, value]) => ({ key, operation: "=", value })) : [];
|
|
234
|
+
for (const condition of scopeConditions) {
|
|
235
|
+
if (!resolveField(m, condition.key)) {
|
|
236
|
+
throw new QueryKitError({ source: m.modelName, site: "filter", key: condition.key, reason: "unknown-key" });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
179
239
|
const scopeWhere = scopeConditions.length ? buildWhere(m, scopeConditions) : void 0;
|
|
180
|
-
const
|
|
240
|
+
const diag = {
|
|
241
|
+
source: m.modelName,
|
|
242
|
+
strict: runtime.strict,
|
|
243
|
+
onSkipped: runtime.onSkippedCondition
|
|
244
|
+
};
|
|
245
|
+
const sortSpec = (sort) => buildSort(m, sort, diag);
|
|
181
246
|
const composeWhere = (userFilter, withDeleted) => {
|
|
182
|
-
const parts = [
|
|
183
|
-
(
|
|
184
|
-
|
|
247
|
+
const parts = [
|
|
248
|
+
buildWhere(m, userFilter, diag),
|
|
249
|
+
scopeWhere,
|
|
250
|
+
hasDeletedAt && !withDeleted ? { deletedAt: { $eq: null } } : void 0
|
|
251
|
+
].filter((p) => p !== void 0);
|
|
185
252
|
if (parts.length === 0) return {};
|
|
186
253
|
if (parts.length === 1) return parts[0];
|
|
187
254
|
return { $and: parts };
|
|
@@ -192,9 +259,24 @@ function buildRepository(runtime, model, config = {}) {
|
|
|
192
259
|
if (Array.isArray(filter)) return [...filter, idCondition];
|
|
193
260
|
return { and: [filter, idCondition] };
|
|
194
261
|
};
|
|
262
|
+
const truthyKeys = (selection) => Object.entries(selection).filter(([, on]) => on).map(([key]) => key);
|
|
263
|
+
const forcedKeys = config.forcedColumns ? truthyKeys(config.forcedColumns) : void 0;
|
|
264
|
+
if (config.forcedColumns && forcedKeys.length === 0) {
|
|
265
|
+
throw new Error("buildRepository: forcedColumns must select at least one field (an empty selection would return the full document).");
|
|
266
|
+
}
|
|
267
|
+
const allowed = config.allowedColumns ? new Set(config.allowedColumns) : void 0;
|
|
268
|
+
if (config.allowedColumns && allowed.size === 0) {
|
|
269
|
+
throw new Error("buildRepository: allowedColumns must list at least one field (an empty allowlist would return the full document).");
|
|
270
|
+
}
|
|
271
|
+
const guarded = Boolean(forcedKeys ?? allowed);
|
|
272
|
+
const permits = (key) => forcedKeys ? forcedKeys.includes(key) : allowed ? allowed.has(key) : true;
|
|
273
|
+
const intersectAllowed = (requested) => {
|
|
274
|
+
const hit = requested.filter((key) => allowed.has(key));
|
|
275
|
+
return hit.length > 0 ? hit : [...allowed];
|
|
276
|
+
};
|
|
195
277
|
const projection = (columns) => {
|
|
196
|
-
|
|
197
|
-
const keys =
|
|
278
|
+
const requested = columns ? truthyKeys(columns) : [];
|
|
279
|
+
const keys = forcedKeys ?? (allowed ? intersectAllowed(requested) : requested);
|
|
198
280
|
if (keys.length === 0) return void 0;
|
|
199
281
|
const proj = {};
|
|
200
282
|
let includesId = false;
|
|
@@ -280,7 +362,7 @@ function buildRepository(runtime, model, config = {}) {
|
|
|
280
362
|
},
|
|
281
363
|
async findList(params = {}) {
|
|
282
364
|
const page = Math.max(1, Math.trunc(params.page ?? 1));
|
|
283
|
-
const perPage =
|
|
365
|
+
const perPage = clampPageSize(params.perPage, runtime.defaultPerPage, runtime.maxPerPage);
|
|
284
366
|
const where = composeWhere(params.filter, params.withDeleted);
|
|
285
367
|
const [data, total_items] = await Promise.all([
|
|
286
368
|
runFind(where, { sort: sortSpec(params.sort), columns: params.columns, with: params.with, limit: perPage, skip: (page - 1) * perPage }),
|
|
@@ -300,7 +382,7 @@ function buildRepository(runtime, model, config = {}) {
|
|
|
300
382
|
};
|
|
301
383
|
},
|
|
302
384
|
async findInfinite(params = {}) {
|
|
303
|
-
const limit =
|
|
385
|
+
const limit = clampPageSize(params.limit, runtime.defaultLimit, runtime.maxLimit);
|
|
304
386
|
const offset = Math.max(0, Math.trunc(params.offset ?? 0));
|
|
305
387
|
const rows = await runFind(composeWhere(params.filter, params.withDeleted), {
|
|
306
388
|
sort: sortSpec(params.sort),
|
|
@@ -317,12 +399,12 @@ function buildRepository(runtime, model, config = {}) {
|
|
|
317
399
|
};
|
|
318
400
|
},
|
|
319
401
|
async findCursor(params = {}) {
|
|
320
|
-
const limit =
|
|
402
|
+
const limit = clampPageSize(params.limit, runtime.defaultLimit, runtime.maxLimit);
|
|
321
403
|
const cursorKey = params.cursorKey ?? "id";
|
|
322
404
|
const order = params.order ?? "asc";
|
|
323
405
|
const direction = params.direction ?? "forward";
|
|
324
406
|
const field = resolveField(m, cursorKey);
|
|
325
|
-
if (!field) throw new
|
|
407
|
+
if (!field) throw new QueryKitError({ source: diag.source, site: "cursorKey", key: cursorKey, reason: "unknown-key" });
|
|
326
408
|
const decoded = decodeCursor(params.cursor);
|
|
327
409
|
const cursorValue = decoded !== void 0 ? castValue(m, field, decoded) : void 0;
|
|
328
410
|
const baseWhere = composeWhere(params.filter, params.withDeleted);
|
|
@@ -331,6 +413,7 @@ function buildRepository(runtime, model, config = {}) {
|
|
|
331
413
|
const where = seek ? Object.keys(baseWhere).length ? { $and: [baseWhere, seek] } : seek : baseWhere;
|
|
332
414
|
const selected = projection(params.columns);
|
|
333
415
|
const proj = selected ? { ...selected, [field]: 1 } : void 0;
|
|
416
|
+
const leaksCursorField = guarded && selected !== void 0 && !permits(cursorKey) && !permits(field);
|
|
334
417
|
let q = m.find(where, proj).sort({ [field]: ascInQuery ? 1 : -1 }).limit(limit + 1);
|
|
335
418
|
q = applyPopulate(q, params.with);
|
|
336
419
|
const rows = await attachSession(q).lean().exec();
|
|
@@ -341,16 +424,20 @@ function buildRepository(runtime, model, config = {}) {
|
|
|
341
424
|
const last = pageRows[pageRows.length - 1];
|
|
342
425
|
const has_next = direction === "forward" ? hasExtra : cursorValue !== void 0;
|
|
343
426
|
const has_prev = direction === "forward" ? cursorValue !== void 0 : hasExtra;
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
next_cursor: has_next && last ? encodeCursor(last[field]) : null,
|
|
351
|
-
prev_cursor: has_prev && first ? encodeCursor(first[field]) : null
|
|
352
|
-
}
|
|
427
|
+
const meta = {
|
|
428
|
+
limit,
|
|
429
|
+
has_next,
|
|
430
|
+
has_prev,
|
|
431
|
+
next_cursor: has_next && last ? encodeCursor(last[field]) : null,
|
|
432
|
+
prev_cursor: has_prev && first ? encodeCursor(first[field]) : null
|
|
353
433
|
};
|
|
434
|
+
if (leaksCursorField) {
|
|
435
|
+
pageRows = pageRows.map((row) => {
|
|
436
|
+
const { [field]: _cursor, ...rest } = row;
|
|
437
|
+
return rest;
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
return { data: pageRows, meta };
|
|
354
441
|
},
|
|
355
442
|
count(filter) {
|
|
356
443
|
return countWhere(composeWhere(filter));
|
|
@@ -428,7 +515,12 @@ function buildRepository(runtime, model, config = {}) {
|
|
|
428
515
|
return row ?? void 0;
|
|
429
516
|
},
|
|
430
517
|
async aggregate(spec) {
|
|
431
|
-
const
|
|
518
|
+
const aggregable = (key) => {
|
|
519
|
+
const field = permits(key) ? resolveField(m, key) : void 0;
|
|
520
|
+
if (!field) reportSkip(diag, { site: "aggregate", key, reason: "unknown-key" });
|
|
521
|
+
return field;
|
|
522
|
+
};
|
|
523
|
+
const groupKeys = toArr(spec.groupBy).filter((key) => Boolean(aggregable(key)));
|
|
432
524
|
const rawMatch = composeWhere(spec.filter, spec.withDeleted);
|
|
433
525
|
let match = rawMatch;
|
|
434
526
|
try {
|
|
@@ -437,12 +529,12 @@ function buildRepository(runtime, model, config = {}) {
|
|
|
437
529
|
match = q.getFilter();
|
|
438
530
|
} catch {
|
|
439
531
|
}
|
|
440
|
-
const groupId = groupKeys.length ? Object.fromEntries(groupKeys.map((k) => [k, `$${
|
|
532
|
+
const groupId = groupKeys.length ? Object.fromEntries(groupKeys.map((k) => [k, `$${aggregable(k) ?? k}`])) : null;
|
|
441
533
|
const group = { _id: groupId };
|
|
442
534
|
if (spec.count) group.count = { $sum: 1 };
|
|
443
535
|
const addAgg = (op, keys, prefix) => {
|
|
444
536
|
for (const k of keys) {
|
|
445
|
-
const field =
|
|
537
|
+
const field = aggregable(k);
|
|
446
538
|
if (field) group[`${prefix}_${k}`] = { [op]: `$${field}` };
|
|
447
539
|
}
|
|
448
540
|
};
|
|
@@ -476,11 +568,16 @@ function createRegistry(connection, options = {}) {
|
|
|
476
568
|
const runtime = {
|
|
477
569
|
getSession: () => ctx.get()?.session,
|
|
478
570
|
defaultPerPage: options.defaultPerPage ?? DEFAULT_PER_PAGE,
|
|
479
|
-
defaultLimit: options.defaultLimit ?? DEFAULT_LIMIT
|
|
571
|
+
defaultLimit: options.defaultLimit ?? DEFAULT_LIMIT,
|
|
572
|
+
maxPerPage: options.maxPerPage ?? DEFAULT_MAX_PER_PAGE,
|
|
573
|
+
maxLimit: options.maxLimit ?? DEFAULT_MAX_LIMIT,
|
|
574
|
+
strict: options.strict ?? false,
|
|
575
|
+
onSkippedCondition: options.onSkippedCondition
|
|
480
576
|
};
|
|
481
577
|
function repository(model, arg2, arg3) {
|
|
578
|
+
const repoOptions = typeof arg2 === "function" ? void 0 : arg2;
|
|
482
579
|
const extend = typeof arg2 === "function" ? arg2 : arg3;
|
|
483
|
-
const base = buildRepository(runtime, model);
|
|
580
|
+
const base = buildRepository(runtime, model, repoOptions);
|
|
484
581
|
return extend ? { ...base, ...extend(base) } : base;
|
|
485
582
|
}
|
|
486
583
|
const transaction = async (fn) => {
|