@querykitjs/mongodb 0.0.0-stage → 1.0.1
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.en.md +144 -0
- package/README.md +143 -2
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +100 -0
- package/dist/index.d.ts +100 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/repository-CPlf87mC.d.cts +184 -0
- package/dist/repository-CPlf87mC.d.ts +184 -0
- package/dist/testing.cjs +2 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +24 -0
- package/dist/testing.d.ts +24 -0
- package/dist/testing.js +2 -0
- package/dist/testing.js.map +1 -0
- package/package.json +96 -4
- package/skills/querykit-mongodb/SKILL.md +97 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { ObjectId, Document, Collection } from 'mongodb';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import * as Core from '@querykitjs/core';
|
|
4
|
+
import { AllRows, MutationCount, MutationRows } from '@querykitjs/core';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `@querykitjs/mongodb` — public types. The DSL, filter and meta types come from `@querykitjs/core`
|
|
8
|
+
* (REQUIREMENTS R7); they are specialized here over a document type (`TDoc`), with the raw
|
|
9
|
+
* escape hatch bound to a native Mongo filter object (branded by core `raw()`, R19).
|
|
10
|
+
* The surface mirrors the other adapters, so one frontend request contract fits every backend.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Plain (lean) document type — what reads return. */
|
|
14
|
+
type Row<TDoc> = TDoc;
|
|
15
|
+
/**
|
|
16
|
+
* Insertable shape — the document minus auto-managed fields (`_id`, timestamps),
|
|
17
|
+
* with the rest optional (the zod schema owns `required` and defaults; that
|
|
18
|
+
* metadata isn't in the doc type, so unlike drizzle-pg we can't mark it here).
|
|
19
|
+
*/
|
|
20
|
+
type Insert<TDoc> = Partial<Omit<TDoc, "_id" | "createdAt" | "updatedAt">>;
|
|
21
|
+
/** Value accepted anywhere an id is expected. */
|
|
22
|
+
type Id = string | number | ObjectId;
|
|
23
|
+
/**
|
|
24
|
+
* Raw Mongo predicate fragment — the filter escape hatch (`TRaw`), a plain Mongo query
|
|
25
|
+
* object. Accepted only when server code brands it with core `raw()` (R19).
|
|
26
|
+
*/
|
|
27
|
+
type RawFilter<_TDoc = unknown> = Record<string, unknown>;
|
|
28
|
+
/** Union of a document's field keys. */
|
|
29
|
+
type FieldKey<TDoc> = keyof TDoc & string;
|
|
30
|
+
/**
|
|
31
|
+
* A field key that also accepts any `string`. Filter/sort keys arrive from the
|
|
32
|
+
* wire as plain strings and the adapter skips the ones it cannot resolve, so a
|
|
33
|
+
* validated payload can go straight into a repository call with no `as` cast.
|
|
34
|
+
* Autocomplete still suggests the document's real fields.
|
|
35
|
+
*/
|
|
36
|
+
type LooseFieldKey<TDoc> = Core.LooseKey<FieldKey<TDoc>>;
|
|
37
|
+
type FilterOperator = Core.FilterOperator;
|
|
38
|
+
type FilterScalar = Core.FilterScalar;
|
|
39
|
+
type FilterValue = Core.FilterValue;
|
|
40
|
+
type FieldCondition<TDoc = Record<string, unknown>> = Core.FieldCondition<LooseFieldKey<TDoc>>;
|
|
41
|
+
type AndGroup<TDoc = Record<string, unknown>> = Core.AndGroup<LooseFieldKey<TDoc>, RawFilter<TDoc>>;
|
|
42
|
+
type OrGroup<TDoc = Record<string, unknown>> = Core.OrGroup<LooseFieldKey<TDoc>, RawFilter<TDoc>>;
|
|
43
|
+
type NotGroup<TDoc = Record<string, unknown>> = Core.NotGroup<LooseFieldKey<TDoc>, RawFilter<TDoc>>;
|
|
44
|
+
type FilterNode<TDoc = Record<string, unknown>> = Core.FilterNode<LooseFieldKey<TDoc>, RawFilter<TDoc>>;
|
|
45
|
+
type Filter<TDoc = Record<string, unknown>> = Core.Filter<LooseFieldKey<TDoc>, RawFilter<TDoc>>;
|
|
46
|
+
type SortDirection = Core.SortDirection;
|
|
47
|
+
/** One sort field by typed field key (`{ key, direction }`) — autocomplete. */
|
|
48
|
+
type SortItem<TDoc = Record<string, unknown>> = Core.SortItem<LooseFieldKey<TDoc>>;
|
|
49
|
+
/**
|
|
50
|
+
* Sort — **always an array** of `{ key, direction }` (multi-field). This is the
|
|
51
|
+
* exact shape the querykit frontend sends (`@querykitjs/web` decodes its
|
|
52
|
+
* `sortType=-createdAt,id` URL string into it).
|
|
53
|
+
*/
|
|
54
|
+
type Sort<TDoc = Record<string, unknown>> = SortItem<TDoc>[];
|
|
55
|
+
type ColumnSelection<TDoc = Record<string, unknown>> = Partial<Record<FieldKey<TDoc>, boolean>>;
|
|
56
|
+
/** Relations (`with`): refused in this version unless the repository lists `relations` — no `$lookup` yet (task 017 L3). */
|
|
57
|
+
type WithRelations = Record<string, unknown>;
|
|
58
|
+
interface QueryParams<TDoc = Record<string, unknown>> {
|
|
59
|
+
filter?: Filter<TDoc>;
|
|
60
|
+
sort?: Sort<TDoc>;
|
|
61
|
+
columns?: ColumnSelection<TDoc>;
|
|
62
|
+
with?: WithRelations;
|
|
63
|
+
/** Include soft-deleted docs (schemas with a `deletedAt` path). */
|
|
64
|
+
withDeleted?: boolean;
|
|
65
|
+
}
|
|
66
|
+
/** Equality scope applied to every operation of a scoped repository. */
|
|
67
|
+
type Scope<TDoc = Record<string, unknown>> = Core.Scope<FieldKey<TDoc>>;
|
|
68
|
+
interface ByIdParams<TDoc = Record<string, unknown>> extends QueryParams<TDoc> {
|
|
69
|
+
/** Field to match against the id. Defaults to `"id"` (→ `_id`). */
|
|
70
|
+
idKey?: LooseFieldKey<TDoc>;
|
|
71
|
+
}
|
|
72
|
+
/** Options for {@link Repository.upsert} / {@link Repository.upsertMany}. */
|
|
73
|
+
type UpsertOptions<TDoc = Record<string, unknown>> = Core.UpsertOptions<FieldKey<TDoc>, Insert<TDoc>>;
|
|
74
|
+
/** Aggregate query specification (`count`/`sum`/`avg`/`min`/`max` + `groupBy`). */
|
|
75
|
+
type AggregateSpec<TDoc = Record<string, unknown>> = Core.AggregateSpec<FieldKey<TDoc>, Filter<TDoc>>;
|
|
76
|
+
/** One aggregated result row (group fields + aggregate values). */
|
|
77
|
+
type AggregateRow = Record<string, unknown>;
|
|
78
|
+
|
|
79
|
+
interface OffsetParams<TDoc = Record<string, unknown>> extends QueryParams<TDoc> {
|
|
80
|
+
page?: number;
|
|
81
|
+
perPage?: number;
|
|
82
|
+
/** `false` — no count query: `perPage + 1` rows are read, `total_items` / `total_pages` are `null` (task 013). Default `true`. */
|
|
83
|
+
withTotal?: boolean;
|
|
84
|
+
}
|
|
85
|
+
type OffsetMeta = Core.OffsetMeta;
|
|
86
|
+
interface OffsetResult<T> {
|
|
87
|
+
data: T[];
|
|
88
|
+
meta: OffsetMeta;
|
|
89
|
+
}
|
|
90
|
+
interface InfiniteParams<TDoc = Record<string, unknown>> extends QueryParams<TDoc> {
|
|
91
|
+
limit?: number;
|
|
92
|
+
offset?: number;
|
|
93
|
+
}
|
|
94
|
+
type InfiniteMeta = Core.InfiniteMeta;
|
|
95
|
+
interface InfiniteResult<T> {
|
|
96
|
+
data: T[];
|
|
97
|
+
meta: InfiniteMeta;
|
|
98
|
+
}
|
|
99
|
+
interface CursorParams<TDoc = Record<string, unknown>> extends QueryParams<TDoc> {
|
|
100
|
+
limit?: number;
|
|
101
|
+
cursor?: string | null;
|
|
102
|
+
/** Field the cursor walks over. Defaults to `"id"` (→ `_id`). */
|
|
103
|
+
cursorKey?: LooseFieldKey<TDoc>;
|
|
104
|
+
order?: SortDirection;
|
|
105
|
+
direction?: "forward" | "backward";
|
|
106
|
+
}
|
|
107
|
+
type CursorMeta = Core.CursorMeta;
|
|
108
|
+
interface CursorResult<T> {
|
|
109
|
+
data: T[];
|
|
110
|
+
meta: CursorMeta;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Per-repository options — the same names as the other adapters (REQUIREMENTS R11), plus the
|
|
115
|
+
* zod `schema` the driver does not have: field types for casting, unknown keys, the default
|
|
116
|
+
* `filterable` (equality on the schema's fields, R12).
|
|
117
|
+
*/
|
|
118
|
+
interface RepositoryOptions<TDoc = Document> {
|
|
119
|
+
/** The document's zod object schema — the one source of field types (task 017 Locked 2). */
|
|
120
|
+
schema: z.ZodObject;
|
|
121
|
+
/** Fields stored as `ObjectId` (cast from 24-hex strings). Default `["_id"]`. */
|
|
122
|
+
objectId?: readonly (FieldKey<TDoc> | "_id")[];
|
|
123
|
+
/** Applied to every read and write (RBAC / tenancy); a value may be `{ in: [...] }`. */
|
|
124
|
+
scope?: Scope<TDoc>;
|
|
125
|
+
/** The response always has exactly these fields; the client's `columns` is ignored. */
|
|
126
|
+
forcedColumns?: ColumnSelection<TDoc>;
|
|
127
|
+
/** The client's `columns` is intersected with this list. */
|
|
128
|
+
allowedColumns?: readonly FieldKey<TDoc>[];
|
|
129
|
+
/** Field → allowed operators (`"*"` all). Absent: equality (`=`, `in`) on the schema's fields. */
|
|
130
|
+
filterable?: Core.Filterable<FieldKey<TDoc> | "_id">;
|
|
131
|
+
/** Sortable fields. Absent: `_id` and the createdAt field. */
|
|
132
|
+
sortable?: readonly (FieldKey<TDoc> | "_id")[];
|
|
133
|
+
limits?: Partial<Core.QueryLimits>;
|
|
134
|
+
/** Server time limit per read, in ms (default: the registry's, 2000). */
|
|
135
|
+
maxTimeMS?: number;
|
|
136
|
+
}
|
|
137
|
+
type TrueKeys<T> = {
|
|
138
|
+
[K in keyof T]: T[K] extends true ? K : never;
|
|
139
|
+
}[keyof T];
|
|
140
|
+
/** `columns` narrows the row via `Pick`. */
|
|
141
|
+
type Result<TDoc, TColumns> = TColumns extends ColumnSelection<TDoc> ? ([TrueKeys<TColumns>] extends [never] ? TDoc : Pick<TDoc, TrueKeys<TColumns> & keyof TDoc>) : TDoc;
|
|
142
|
+
type Params<TBase, TColumns> = Omit<TBase, "columns"> & {
|
|
143
|
+
columns?: TColumns;
|
|
144
|
+
};
|
|
145
|
+
interface Repository<TDoc extends Document = Document> {
|
|
146
|
+
readonly collection: Collection<TDoc>;
|
|
147
|
+
findAll<const TColumns extends ColumnSelection<TDoc> | undefined = undefined>(params?: Params<QueryParams<TDoc>, TColumns>): Promise<Result<TDoc, TColumns>[]>;
|
|
148
|
+
findOne<const TColumns extends ColumnSelection<TDoc> | undefined = undefined>(params?: Params<QueryParams<TDoc>, TColumns>): Promise<Result<TDoc, TColumns> | undefined>;
|
|
149
|
+
findById<const TColumns extends ColumnSelection<TDoc> | undefined = undefined>(id: Id, params?: Params<ByIdParams<TDoc>, TColumns>): Promise<Result<TDoc, TColumns> | undefined>;
|
|
150
|
+
findList<const TColumns extends ColumnSelection<TDoc> | undefined = undefined>(params?: Params<OffsetParams<TDoc>, TColumns>): Promise<OffsetResult<Result<TDoc, TColumns>>>;
|
|
151
|
+
findInfinite<const TColumns extends ColumnSelection<TDoc> | undefined = undefined>(params?: Params<InfiniteParams<TDoc>, TColumns>): Promise<InfiniteResult<Result<TDoc, TColumns>>>;
|
|
152
|
+
findCursor<const TColumns extends ColumnSelection<TDoc> | undefined = undefined>(params?: Params<CursorParams<TDoc>, TColumns>): Promise<CursorResult<Result<TDoc, TColumns>>>;
|
|
153
|
+
count(filter?: Filter<TDoc>): Promise<number>;
|
|
154
|
+
exists(filter?: Filter<TDoc>): Promise<boolean>;
|
|
155
|
+
create(values: Insert<TDoc>): Promise<Row<TDoc>>;
|
|
156
|
+
createMany(values: Insert<TDoc>[]): Promise<Row<TDoc>[]>;
|
|
157
|
+
upsert(values: Insert<TDoc>, options: UpsertOptions<TDoc>): Promise<Row<TDoc>>;
|
|
158
|
+
upsertMany(values: Insert<TDoc>[], options: UpsertOptions<TDoc>): Promise<Row<TDoc>[]>;
|
|
159
|
+
updateById(id: Id, patch: Partial<Insert<TDoc>>, idKey?: FieldKey<TDoc>): Promise<Row<TDoc> | undefined>;
|
|
160
|
+
/** One `updateMany`; an empty filter is refused (`empty-filter`) unless it is `{ all: true }`. */
|
|
161
|
+
updateWhere(filter: Filter<TDoc> | AllRows, patch: Partial<Insert<TDoc>>, options?: {
|
|
162
|
+
returning?: false;
|
|
163
|
+
}): Promise<MutationCount>;
|
|
164
|
+
updateWhere(filter: Filter<TDoc> | AllRows, patch: Partial<Insert<TDoc>>, options: {
|
|
165
|
+
returning: {
|
|
166
|
+
limit: number;
|
|
167
|
+
};
|
|
168
|
+
}): Promise<MutationRows<Row<TDoc>>>;
|
|
169
|
+
deleteById(id: Id, idKey?: FieldKey<TDoc>): Promise<Row<TDoc> | undefined>;
|
|
170
|
+
deleteWhere(filter: Filter<TDoc> | AllRows, options?: {
|
|
171
|
+
returning?: false;
|
|
172
|
+
}): Promise<MutationCount>;
|
|
173
|
+
deleteWhere(filter: Filter<TDoc> | AllRows, options: {
|
|
174
|
+
returning: {
|
|
175
|
+
limit: number;
|
|
176
|
+
};
|
|
177
|
+
}): Promise<MutationRows<Row<TDoc>>>;
|
|
178
|
+
softDelete(id: Id, idKey?: FieldKey<TDoc>): Promise<Row<TDoc> | undefined>;
|
|
179
|
+
restore(id: Id, idKey?: FieldKey<TDoc>): Promise<Row<TDoc> | undefined>;
|
|
180
|
+
aggregate(spec: AggregateSpec<TDoc>): Promise<AggregateRow[]>;
|
|
181
|
+
scoped(scope: Scope<TDoc>): Repository<TDoc>;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export type { AggregateRow as A, ByIdParams as B, ColumnSelection as C, FieldKey as F, Id as I, LooseFieldKey as L, NotGroup as N, OffsetMeta as O, QueryParams as Q, RepositoryOptions as R, Scope as S, UpsertOptions as U, WithRelations as W, Repository as a, RawFilter as b, AggregateSpec as c, AndGroup as d, CursorMeta as e, CursorParams as f, CursorResult as g, FieldCondition as h, Filter as i, FilterNode as j, FilterOperator as k, FilterScalar as l, FilterValue as m, InfiniteMeta as n, InfiniteParams as o, InfiniteResult as p, Insert as q, OffsetParams as r, OffsetResult as s, OrGroup as t, Result as u, Row as v, Sort as w, SortDirection as x, SortItem as y };
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { ObjectId, Document, Collection } from 'mongodb';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import * as Core from '@querykitjs/core';
|
|
4
|
+
import { AllRows, MutationCount, MutationRows } from '@querykitjs/core';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `@querykitjs/mongodb` — public types. The DSL, filter and meta types come from `@querykitjs/core`
|
|
8
|
+
* (REQUIREMENTS R7); they are specialized here over a document type (`TDoc`), with the raw
|
|
9
|
+
* escape hatch bound to a native Mongo filter object (branded by core `raw()`, R19).
|
|
10
|
+
* The surface mirrors the other adapters, so one frontend request contract fits every backend.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Plain (lean) document type — what reads return. */
|
|
14
|
+
type Row<TDoc> = TDoc;
|
|
15
|
+
/**
|
|
16
|
+
* Insertable shape — the document minus auto-managed fields (`_id`, timestamps),
|
|
17
|
+
* with the rest optional (the zod schema owns `required` and defaults; that
|
|
18
|
+
* metadata isn't in the doc type, so unlike drizzle-pg we can't mark it here).
|
|
19
|
+
*/
|
|
20
|
+
type Insert<TDoc> = Partial<Omit<TDoc, "_id" | "createdAt" | "updatedAt">>;
|
|
21
|
+
/** Value accepted anywhere an id is expected. */
|
|
22
|
+
type Id = string | number | ObjectId;
|
|
23
|
+
/**
|
|
24
|
+
* Raw Mongo predicate fragment — the filter escape hatch (`TRaw`), a plain Mongo query
|
|
25
|
+
* object. Accepted only when server code brands it with core `raw()` (R19).
|
|
26
|
+
*/
|
|
27
|
+
type RawFilter<_TDoc = unknown> = Record<string, unknown>;
|
|
28
|
+
/** Union of a document's field keys. */
|
|
29
|
+
type FieldKey<TDoc> = keyof TDoc & string;
|
|
30
|
+
/**
|
|
31
|
+
* A field key that also accepts any `string`. Filter/sort keys arrive from the
|
|
32
|
+
* wire as plain strings and the adapter skips the ones it cannot resolve, so a
|
|
33
|
+
* validated payload can go straight into a repository call with no `as` cast.
|
|
34
|
+
* Autocomplete still suggests the document's real fields.
|
|
35
|
+
*/
|
|
36
|
+
type LooseFieldKey<TDoc> = Core.LooseKey<FieldKey<TDoc>>;
|
|
37
|
+
type FilterOperator = Core.FilterOperator;
|
|
38
|
+
type FilterScalar = Core.FilterScalar;
|
|
39
|
+
type FilterValue = Core.FilterValue;
|
|
40
|
+
type FieldCondition<TDoc = Record<string, unknown>> = Core.FieldCondition<LooseFieldKey<TDoc>>;
|
|
41
|
+
type AndGroup<TDoc = Record<string, unknown>> = Core.AndGroup<LooseFieldKey<TDoc>, RawFilter<TDoc>>;
|
|
42
|
+
type OrGroup<TDoc = Record<string, unknown>> = Core.OrGroup<LooseFieldKey<TDoc>, RawFilter<TDoc>>;
|
|
43
|
+
type NotGroup<TDoc = Record<string, unknown>> = Core.NotGroup<LooseFieldKey<TDoc>, RawFilter<TDoc>>;
|
|
44
|
+
type FilterNode<TDoc = Record<string, unknown>> = Core.FilterNode<LooseFieldKey<TDoc>, RawFilter<TDoc>>;
|
|
45
|
+
type Filter<TDoc = Record<string, unknown>> = Core.Filter<LooseFieldKey<TDoc>, RawFilter<TDoc>>;
|
|
46
|
+
type SortDirection = Core.SortDirection;
|
|
47
|
+
/** One sort field by typed field key (`{ key, direction }`) — autocomplete. */
|
|
48
|
+
type SortItem<TDoc = Record<string, unknown>> = Core.SortItem<LooseFieldKey<TDoc>>;
|
|
49
|
+
/**
|
|
50
|
+
* Sort — **always an array** of `{ key, direction }` (multi-field). This is the
|
|
51
|
+
* exact shape the querykit frontend sends (`@querykitjs/web` decodes its
|
|
52
|
+
* `sortType=-createdAt,id` URL string into it).
|
|
53
|
+
*/
|
|
54
|
+
type Sort<TDoc = Record<string, unknown>> = SortItem<TDoc>[];
|
|
55
|
+
type ColumnSelection<TDoc = Record<string, unknown>> = Partial<Record<FieldKey<TDoc>, boolean>>;
|
|
56
|
+
/** Relations (`with`): refused in this version unless the repository lists `relations` — no `$lookup` yet (task 017 L3). */
|
|
57
|
+
type WithRelations = Record<string, unknown>;
|
|
58
|
+
interface QueryParams<TDoc = Record<string, unknown>> {
|
|
59
|
+
filter?: Filter<TDoc>;
|
|
60
|
+
sort?: Sort<TDoc>;
|
|
61
|
+
columns?: ColumnSelection<TDoc>;
|
|
62
|
+
with?: WithRelations;
|
|
63
|
+
/** Include soft-deleted docs (schemas with a `deletedAt` path). */
|
|
64
|
+
withDeleted?: boolean;
|
|
65
|
+
}
|
|
66
|
+
/** Equality scope applied to every operation of a scoped repository. */
|
|
67
|
+
type Scope<TDoc = Record<string, unknown>> = Core.Scope<FieldKey<TDoc>>;
|
|
68
|
+
interface ByIdParams<TDoc = Record<string, unknown>> extends QueryParams<TDoc> {
|
|
69
|
+
/** Field to match against the id. Defaults to `"id"` (→ `_id`). */
|
|
70
|
+
idKey?: LooseFieldKey<TDoc>;
|
|
71
|
+
}
|
|
72
|
+
/** Options for {@link Repository.upsert} / {@link Repository.upsertMany}. */
|
|
73
|
+
type UpsertOptions<TDoc = Record<string, unknown>> = Core.UpsertOptions<FieldKey<TDoc>, Insert<TDoc>>;
|
|
74
|
+
/** Aggregate query specification (`count`/`sum`/`avg`/`min`/`max` + `groupBy`). */
|
|
75
|
+
type AggregateSpec<TDoc = Record<string, unknown>> = Core.AggregateSpec<FieldKey<TDoc>, Filter<TDoc>>;
|
|
76
|
+
/** One aggregated result row (group fields + aggregate values). */
|
|
77
|
+
type AggregateRow = Record<string, unknown>;
|
|
78
|
+
|
|
79
|
+
interface OffsetParams<TDoc = Record<string, unknown>> extends QueryParams<TDoc> {
|
|
80
|
+
page?: number;
|
|
81
|
+
perPage?: number;
|
|
82
|
+
/** `false` — no count query: `perPage + 1` rows are read, `total_items` / `total_pages` are `null` (task 013). Default `true`. */
|
|
83
|
+
withTotal?: boolean;
|
|
84
|
+
}
|
|
85
|
+
type OffsetMeta = Core.OffsetMeta;
|
|
86
|
+
interface OffsetResult<T> {
|
|
87
|
+
data: T[];
|
|
88
|
+
meta: OffsetMeta;
|
|
89
|
+
}
|
|
90
|
+
interface InfiniteParams<TDoc = Record<string, unknown>> extends QueryParams<TDoc> {
|
|
91
|
+
limit?: number;
|
|
92
|
+
offset?: number;
|
|
93
|
+
}
|
|
94
|
+
type InfiniteMeta = Core.InfiniteMeta;
|
|
95
|
+
interface InfiniteResult<T> {
|
|
96
|
+
data: T[];
|
|
97
|
+
meta: InfiniteMeta;
|
|
98
|
+
}
|
|
99
|
+
interface CursorParams<TDoc = Record<string, unknown>> extends QueryParams<TDoc> {
|
|
100
|
+
limit?: number;
|
|
101
|
+
cursor?: string | null;
|
|
102
|
+
/** Field the cursor walks over. Defaults to `"id"` (→ `_id`). */
|
|
103
|
+
cursorKey?: LooseFieldKey<TDoc>;
|
|
104
|
+
order?: SortDirection;
|
|
105
|
+
direction?: "forward" | "backward";
|
|
106
|
+
}
|
|
107
|
+
type CursorMeta = Core.CursorMeta;
|
|
108
|
+
interface CursorResult<T> {
|
|
109
|
+
data: T[];
|
|
110
|
+
meta: CursorMeta;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Per-repository options — the same names as the other adapters (REQUIREMENTS R11), plus the
|
|
115
|
+
* zod `schema` the driver does not have: field types for casting, unknown keys, the default
|
|
116
|
+
* `filterable` (equality on the schema's fields, R12).
|
|
117
|
+
*/
|
|
118
|
+
interface RepositoryOptions<TDoc = Document> {
|
|
119
|
+
/** The document's zod object schema — the one source of field types (task 017 Locked 2). */
|
|
120
|
+
schema: z.ZodObject;
|
|
121
|
+
/** Fields stored as `ObjectId` (cast from 24-hex strings). Default `["_id"]`. */
|
|
122
|
+
objectId?: readonly (FieldKey<TDoc> | "_id")[];
|
|
123
|
+
/** Applied to every read and write (RBAC / tenancy); a value may be `{ in: [...] }`. */
|
|
124
|
+
scope?: Scope<TDoc>;
|
|
125
|
+
/** The response always has exactly these fields; the client's `columns` is ignored. */
|
|
126
|
+
forcedColumns?: ColumnSelection<TDoc>;
|
|
127
|
+
/** The client's `columns` is intersected with this list. */
|
|
128
|
+
allowedColumns?: readonly FieldKey<TDoc>[];
|
|
129
|
+
/** Field → allowed operators (`"*"` all). Absent: equality (`=`, `in`) on the schema's fields. */
|
|
130
|
+
filterable?: Core.Filterable<FieldKey<TDoc> | "_id">;
|
|
131
|
+
/** Sortable fields. Absent: `_id` and the createdAt field. */
|
|
132
|
+
sortable?: readonly (FieldKey<TDoc> | "_id")[];
|
|
133
|
+
limits?: Partial<Core.QueryLimits>;
|
|
134
|
+
/** Server time limit per read, in ms (default: the registry's, 2000). */
|
|
135
|
+
maxTimeMS?: number;
|
|
136
|
+
}
|
|
137
|
+
type TrueKeys<T> = {
|
|
138
|
+
[K in keyof T]: T[K] extends true ? K : never;
|
|
139
|
+
}[keyof T];
|
|
140
|
+
/** `columns` narrows the row via `Pick`. */
|
|
141
|
+
type Result<TDoc, TColumns> = TColumns extends ColumnSelection<TDoc> ? ([TrueKeys<TColumns>] extends [never] ? TDoc : Pick<TDoc, TrueKeys<TColumns> & keyof TDoc>) : TDoc;
|
|
142
|
+
type Params<TBase, TColumns> = Omit<TBase, "columns"> & {
|
|
143
|
+
columns?: TColumns;
|
|
144
|
+
};
|
|
145
|
+
interface Repository<TDoc extends Document = Document> {
|
|
146
|
+
readonly collection: Collection<TDoc>;
|
|
147
|
+
findAll<const TColumns extends ColumnSelection<TDoc> | undefined = undefined>(params?: Params<QueryParams<TDoc>, TColumns>): Promise<Result<TDoc, TColumns>[]>;
|
|
148
|
+
findOne<const TColumns extends ColumnSelection<TDoc> | undefined = undefined>(params?: Params<QueryParams<TDoc>, TColumns>): Promise<Result<TDoc, TColumns> | undefined>;
|
|
149
|
+
findById<const TColumns extends ColumnSelection<TDoc> | undefined = undefined>(id: Id, params?: Params<ByIdParams<TDoc>, TColumns>): Promise<Result<TDoc, TColumns> | undefined>;
|
|
150
|
+
findList<const TColumns extends ColumnSelection<TDoc> | undefined = undefined>(params?: Params<OffsetParams<TDoc>, TColumns>): Promise<OffsetResult<Result<TDoc, TColumns>>>;
|
|
151
|
+
findInfinite<const TColumns extends ColumnSelection<TDoc> | undefined = undefined>(params?: Params<InfiniteParams<TDoc>, TColumns>): Promise<InfiniteResult<Result<TDoc, TColumns>>>;
|
|
152
|
+
findCursor<const TColumns extends ColumnSelection<TDoc> | undefined = undefined>(params?: Params<CursorParams<TDoc>, TColumns>): Promise<CursorResult<Result<TDoc, TColumns>>>;
|
|
153
|
+
count(filter?: Filter<TDoc>): Promise<number>;
|
|
154
|
+
exists(filter?: Filter<TDoc>): Promise<boolean>;
|
|
155
|
+
create(values: Insert<TDoc>): Promise<Row<TDoc>>;
|
|
156
|
+
createMany(values: Insert<TDoc>[]): Promise<Row<TDoc>[]>;
|
|
157
|
+
upsert(values: Insert<TDoc>, options: UpsertOptions<TDoc>): Promise<Row<TDoc>>;
|
|
158
|
+
upsertMany(values: Insert<TDoc>[], options: UpsertOptions<TDoc>): Promise<Row<TDoc>[]>;
|
|
159
|
+
updateById(id: Id, patch: Partial<Insert<TDoc>>, idKey?: FieldKey<TDoc>): Promise<Row<TDoc> | undefined>;
|
|
160
|
+
/** One `updateMany`; an empty filter is refused (`empty-filter`) unless it is `{ all: true }`. */
|
|
161
|
+
updateWhere(filter: Filter<TDoc> | AllRows, patch: Partial<Insert<TDoc>>, options?: {
|
|
162
|
+
returning?: false;
|
|
163
|
+
}): Promise<MutationCount>;
|
|
164
|
+
updateWhere(filter: Filter<TDoc> | AllRows, patch: Partial<Insert<TDoc>>, options: {
|
|
165
|
+
returning: {
|
|
166
|
+
limit: number;
|
|
167
|
+
};
|
|
168
|
+
}): Promise<MutationRows<Row<TDoc>>>;
|
|
169
|
+
deleteById(id: Id, idKey?: FieldKey<TDoc>): Promise<Row<TDoc> | undefined>;
|
|
170
|
+
deleteWhere(filter: Filter<TDoc> | AllRows, options?: {
|
|
171
|
+
returning?: false;
|
|
172
|
+
}): Promise<MutationCount>;
|
|
173
|
+
deleteWhere(filter: Filter<TDoc> | AllRows, options: {
|
|
174
|
+
returning: {
|
|
175
|
+
limit: number;
|
|
176
|
+
};
|
|
177
|
+
}): Promise<MutationRows<Row<TDoc>>>;
|
|
178
|
+
softDelete(id: Id, idKey?: FieldKey<TDoc>): Promise<Row<TDoc> | undefined>;
|
|
179
|
+
restore(id: Id, idKey?: FieldKey<TDoc>): Promise<Row<TDoc> | undefined>;
|
|
180
|
+
aggregate(spec: AggregateSpec<TDoc>): Promise<AggregateRow[]>;
|
|
181
|
+
scoped(scope: Scope<TDoc>): Repository<TDoc>;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export type { AggregateRow as A, ByIdParams as B, ColumnSelection as C, FieldKey as F, Id as I, LooseFieldKey as L, NotGroup as N, OffsetMeta as O, QueryParams as Q, RepositoryOptions as R, Scope as S, UpsertOptions as U, WithRelations as W, Repository as a, RawFilter as b, AggregateSpec as c, AndGroup as d, CursorMeta as e, CursorParams as f, CursorResult as g, FieldCondition as h, Filter as i, FilterNode as j, FilterOperator as k, FilterScalar as l, FilterValue as m, InfiniteMeta as n, InfiniteParams as o, InfiniteResult as p, Insert as q, OffsetParams as r, OffsetResult as s, OrGroup as t, Result as u, Row as v, Sort as w, SortDirection as x, SortItem as y };
|
package/dist/testing.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/internal/explain-key.ts","../src/testing.ts"],"names":["EXPLAIN","explainFind","repository","params","explain"],"mappings":"aAKO,IAAMA,CAAAA,CAAU,MAAA,CAAO,GAAA,CAAI,0BAA0B,CAAA,CCiBrD,SAASC,CAAAA,CAAmCC,CAAAA,CAA8BC,CAAAA,CAA4B,EAAC,CAAsB,CAClI,IAAMC,CAAAA,CAAWF,CAAAA,CAAsCF,CAAO,CAAA,CAC9D,GAAI,CAACI,CAAAA,CAAS,MAAM,IAAI,SAAA,CAAU,mDAAmD,CAAA,CACrF,OAAOA,CAAAA,CAAQD,CAAqB,CACtC","file":"testing.cjs","sourcesContent":["/**\n * The key of a repository's hidden `explain` method — shared by the repository (main entry)\n * and `@querykitjs/mongodb/testing`, which imports only this key, not the repository code.\n * `Symbol.for`, so the two entries' bundles agree on it.\n */\nexport const EXPLAIN = Symbol.for(\"querykit.mongodb.explain\");\n","/**\n * `@querykitjs/mongodb/testing` — test helpers, kept out of the main entry (REQUIREMENTS R14).\n *\n * @example\n * ```ts\n * import { explainFind } from \"@querykitjs/mongodb/testing\";\n *\n * const plan = await explainFind(users, { filter: [{ key: \"email\", operation: \"%_\", value: \"ali\" }] });\n * expect(JSON.stringify(plan)).toContain(\"IXSCAN\"); // the prefix search uses the index\n * ```\n */\nimport type { Document } from \"mongodb\";\n\nimport { EXPLAIN } from \"./internal/explain-key\";\nimport type { QueryParams, Repository } from \"./types\";\n\ntype Explainable = { [EXPLAIN]?: (params: QueryParams) => Promise<Document> };\n\n/**\n * The MongoDB query plan (`explain(\"queryPlanner\")`) of `repository.findAll(params)` — the same\n * policy checks and compiled filter, so a test can assert that a request uses an index.\n */\nexport function explainFind<TDoc extends Document>(repository: Repository<TDoc>, params: QueryParams<TDoc> = {}): Promise<Document> {\n const explain = (repository as unknown as Explainable)[EXPLAIN];\n if (!explain) throw new TypeError(\"explainFind: not a @querykitjs/mongodb repository\");\n return explain(params as QueryParams);\n}\n"]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Document } from 'mongodb';
|
|
2
|
+
import { Repository, QueryParams } from './index.cjs';
|
|
3
|
+
import 'zod';
|
|
4
|
+
import '@querykitjs/core';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `@querykitjs/mongodb/testing` — test helpers, kept out of the main entry (REQUIREMENTS R14).
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { explainFind } from "@querykitjs/mongodb/testing";
|
|
12
|
+
*
|
|
13
|
+
* const plan = await explainFind(users, { filter: [{ key: "email", operation: "%_", value: "ali" }] });
|
|
14
|
+
* expect(JSON.stringify(plan)).toContain("IXSCAN"); // the prefix search uses the index
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The MongoDB query plan (`explain("queryPlanner")`) of `repository.findAll(params)` — the same
|
|
20
|
+
* policy checks and compiled filter, so a test can assert that a request uses an index.
|
|
21
|
+
*/
|
|
22
|
+
declare function explainFind<TDoc extends Document>(repository: Repository<TDoc>, params?: QueryParams<TDoc>): Promise<Document>;
|
|
23
|
+
|
|
24
|
+
export { explainFind };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Document } from 'mongodb';
|
|
2
|
+
import { Repository, QueryParams } from './index.js';
|
|
3
|
+
import 'zod';
|
|
4
|
+
import '@querykitjs/core';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `@querykitjs/mongodb/testing` — test helpers, kept out of the main entry (REQUIREMENTS R14).
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { explainFind } from "@querykitjs/mongodb/testing";
|
|
12
|
+
*
|
|
13
|
+
* const plan = await explainFind(users, { filter: [{ key: "email", operation: "%_", value: "ali" }] });
|
|
14
|
+
* expect(JSON.stringify(plan)).toContain("IXSCAN"); // the prefix search uses the index
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The MongoDB query plan (`explain("queryPlanner")`) of `repository.findAll(params)` — the same
|
|
20
|
+
* policy checks and compiled filter, so a test can assert that a request uses an index.
|
|
21
|
+
*/
|
|
22
|
+
declare function explainFind<TDoc extends Document>(repository: Repository<TDoc>, params?: QueryParams<TDoc>): Promise<Document>;
|
|
23
|
+
|
|
24
|
+
export { explainFind };
|
package/dist/testing.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/internal/explain-key.ts","../src/testing.ts"],"names":["EXPLAIN","explainFind","repository","params","explain"],"mappings":"AAKO,IAAMA,CAAAA,CAAU,MAAA,CAAO,GAAA,CAAI,0BAA0B,CAAA,CCiBrD,SAASC,CAAAA,CAAmCC,CAAAA,CAA8BC,CAAAA,CAA4B,EAAC,CAAsB,CAClI,IAAMC,CAAAA,CAAWF,CAAAA,CAAsCF,CAAO,CAAA,CAC9D,GAAI,CAACI,CAAAA,CAAS,MAAM,IAAI,SAAA,CAAU,mDAAmD,CAAA,CACrF,OAAOA,CAAAA,CAAQD,CAAqB,CACtC","file":"testing.js","sourcesContent":["/**\n * The key of a repository's hidden `explain` method — shared by the repository (main entry)\n * and `@querykitjs/mongodb/testing`, which imports only this key, not the repository code.\n * `Symbol.for`, so the two entries' bundles agree on it.\n */\nexport const EXPLAIN = Symbol.for(\"querykit.mongodb.explain\");\n","/**\n * `@querykitjs/mongodb/testing` — test helpers, kept out of the main entry (REQUIREMENTS R14).\n *\n * @example\n * ```ts\n * import { explainFind } from \"@querykitjs/mongodb/testing\";\n *\n * const plan = await explainFind(users, { filter: [{ key: \"email\", operation: \"%_\", value: \"ali\" }] });\n * expect(JSON.stringify(plan)).toContain(\"IXSCAN\"); // the prefix search uses the index\n * ```\n */\nimport type { Document } from \"mongodb\";\n\nimport { EXPLAIN } from \"./internal/explain-key\";\nimport type { QueryParams, Repository } from \"./types\";\n\ntype Explainable = { [EXPLAIN]?: (params: QueryParams) => Promise<Document> };\n\n/**\n * The MongoDB query plan (`explain(\"queryPlanner\")`) of `repository.findAll(params)` — the same\n * policy checks and compiled filter, so a test can assert that a request uses an index.\n */\nexport function explainFind<TDoc extends Document>(repository: Repository<TDoc>, params: QueryParams<TDoc> = {}): Promise<Document> {\n const explain = (repository as unknown as Explainable)[EXPLAIN];\n if (!explain) throw new TypeError(\"explainFind: not a @querykitjs/mongodb repository\");\n return explain(params as QueryParams);\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,98 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@querykitjs/mongodb",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Advanced filtering + flexible pagination repository layer for the official MongoDB driver.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "Suhrobbek Soatov",
|
|
8
|
+
"url": "https://soatov.uz",
|
|
9
|
+
"email": "svkhrobbeck@gmail.com"
|
|
10
|
+
},
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/svkhrobbeck/querykit.git",
|
|
15
|
+
"directory": "packages/mongodb"
|
|
16
|
+
},
|
|
17
|
+
"homepage": "https://github.com/svkhrobbeck/querykit/tree/main/packages/mongodb#readme",
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/svkhrobbeck/querykit/issues"
|
|
20
|
+
},
|
|
21
|
+
"main": "./dist/index.cjs",
|
|
22
|
+
"module": "./dist/index.js",
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"import": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"default": "./dist/index.js"
|
|
29
|
+
},
|
|
30
|
+
"require": {
|
|
31
|
+
"types": "./dist/index.d.cts",
|
|
32
|
+
"default": "./dist/index.cjs"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"./testing": {
|
|
36
|
+
"import": {
|
|
37
|
+
"types": "./dist/testing.d.ts",
|
|
38
|
+
"default": "./dist/testing.js"
|
|
39
|
+
},
|
|
40
|
+
"require": {
|
|
41
|
+
"types": "./dist/testing.d.cts",
|
|
42
|
+
"default": "./dist/testing.cjs"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"typesVersions": {
|
|
47
|
+
"*": {
|
|
48
|
+
"testing": [
|
|
49
|
+
"./dist/testing.d.ts"
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"files": [
|
|
54
|
+
"dist",
|
|
55
|
+
"skills",
|
|
56
|
+
"README.md",
|
|
57
|
+
"README.en.md",
|
|
58
|
+
"LICENSE"
|
|
59
|
+
],
|
|
60
|
+
"sideEffects": false,
|
|
61
|
+
"publishConfig": {
|
|
62
|
+
"access": "public"
|
|
63
|
+
},
|
|
64
|
+
"engines": {
|
|
65
|
+
"node": ">=22.12"
|
|
66
|
+
},
|
|
67
|
+
"scripts": {
|
|
68
|
+
"build": "tsup",
|
|
69
|
+
"dev": "tsup --watch",
|
|
70
|
+
"typecheck": "tsc --noEmit",
|
|
71
|
+
"test:smoke": "bun --preload ./test/bun-v8-shim.ts test/smoke.ts",
|
|
72
|
+
"test:unit": "bun --preload ./test/bun-v8-shim.ts test/where.ts && bun --preload ./test/bun-v8-shim.ts test/policy.ts",
|
|
73
|
+
"test:types": "tsc --noEmit"
|
|
74
|
+
},
|
|
75
|
+
"dependencies": {
|
|
76
|
+
"@querykitjs/core": "^2.0.0"
|
|
77
|
+
},
|
|
78
|
+
"peerDependencies": {
|
|
79
|
+
"mongodb": "^6.21 || ^7.7",
|
|
80
|
+
"zod": "^4"
|
|
81
|
+
},
|
|
82
|
+
"devDependencies": {
|
|
83
|
+
"mongodb": "7.7.0",
|
|
84
|
+
"mongodb-memory-server": "11.3.0",
|
|
85
|
+
"zod": "4.6.7"
|
|
86
|
+
},
|
|
87
|
+
"keywords": [
|
|
88
|
+
"mongodb",
|
|
89
|
+
"repository",
|
|
90
|
+
"pagination",
|
|
91
|
+
"cursor",
|
|
92
|
+
"keyset",
|
|
93
|
+
"filter",
|
|
94
|
+
"query",
|
|
95
|
+
"zod",
|
|
96
|
+
"typescript"
|
|
97
|
+
]
|
|
98
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: querykit-mongodb
|
|
3
|
+
description: Use when code builds or calls a querykit repository over the official MongoDB driver (@querykitjs/mongodb) - createRegistry(client), registry.repository(collection, { schema }), filterable / sortable / limits policy, findList / findInfinite / findCursor, updateWhere / deleteWhere, scope, transactions, or explainFind from @querykitjs/mongodb/testing.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Safety rules
|
|
7
|
+
|
|
8
|
+
- DO pass the document's zod object as `schema` and list ObjectId fields in `objectId` (default `["_id"]`); field types, unknown keys and casting come only from that schema.
|
|
9
|
+
- DO list what a client may filter on in `filterable` (field → operators, `"*"` only on purpose). Without it a client gets equality (`=`, `in`) on the projection fields only; anything else is `QueryKitError` `forbidden-key` / `forbidden-operator` before the driver runs.
|
|
10
|
+
- DO list sortable fields in `sortable`; the default is `_id` and the createdAt field.
|
|
11
|
+
- DON'T pass a plain Mongo object as a filter node from request data. Server code brands one with `raw()` from `@querykitjs/core`; an unbranded node is `forbidden-key`, and `raw()` is not checked by `filterable`.
|
|
12
|
+
- DON'T expect a client value to act as an operator: an object value (`{ "$where": … }`, `{ "$gt": "" }`) is `invalid-value` and never reaches MongoDB.
|
|
13
|
+
- DON'T send `with`: relations are not built in this version (no `$lookup`); every `with` key is `forbidden-key`, `site: "with"`.
|
|
14
|
+
- DO keep `maxTimeMS` (default `2000`, per registry or repository) and map `QueryKitTimeoutError` to 503/504.
|
|
15
|
+
- DO keep the request limits (core `DEFAULT_QUERY_LIMITS`; LIKE patterns with more than `2` wildcards are `limit-exceeded`); raise one with `limits`, never turn it off.
|
|
16
|
+
- DON'T call `updateWhere` / `deleteWhere` with an empty filter to mean "all rows" — it is `empty-filter`; pass `{ all: true }` on purpose.
|
|
17
|
+
- DO set `cursorSecret` on the registry when cursor tokens leave the server; an unsigned token is then refused.
|
|
18
|
+
- DO validate a write body with your zod schema first: writes store values as given.
|
|
19
|
+
|
|
20
|
+
## When to use
|
|
21
|
+
|
|
22
|
+
- A backend on the plain `mongodb` driver (no Mongoose) that serves querykit list requests from `@querykitjs/web`.
|
|
23
|
+
- Tenant / RBAC scoping (`scope`, `repo.scoped(...)`), soft delete, bulk writes, aggregation over one collection.
|
|
24
|
+
- Asserting in tests that a request uses an index (`explainFind`).
|
|
25
|
+
|
|
26
|
+
With Mongoose models use `@querykitjs/mongoose`; the request contract is the same.
|
|
27
|
+
|
|
28
|
+
## Setup
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import type { MongoClient, ObjectId } from "mongodb";
|
|
32
|
+
import { z } from "zod";
|
|
33
|
+
import { createRegistry } from "@querykitjs/mongodb";
|
|
34
|
+
|
|
35
|
+
declare const client: MongoClient;
|
|
36
|
+
|
|
37
|
+
const UserSchema = z.object({ _id: z.string(), name: z.string(), status: z.enum(["active", "blocked"]), createdAt: z.date() });
|
|
38
|
+
type User = { _id: ObjectId; name: string; status: "active" | "blocked"; createdAt: Date };
|
|
39
|
+
|
|
40
|
+
const qk = createRegistry(client, { maxPerPage: 100 });
|
|
41
|
+
export const users = qk.repository(client.db("app").collection<User>("users"), {
|
|
42
|
+
schema: UserSchema,
|
|
43
|
+
forcedColumns: { _id: true, name: true, status: true, createdAt: true },
|
|
44
|
+
filterable: { status: ["=", "in"], name: ["startsWith"] },
|
|
45
|
+
sortable: ["_id", "createdAt", "name"],
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Common tasks
|
|
50
|
+
|
|
51
|
+
### Serve a list request
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import type { WireOffsetParams } from "@querykitjs/core";
|
|
55
|
+
import type { Repository } from "@querykitjs/mongodb";
|
|
56
|
+
|
|
57
|
+
declare const users: Repository<{ _id: unknown; name: string; status: string; createdAt: Date }>;
|
|
58
|
+
|
|
59
|
+
export async function list(body: WireOffsetParams) {
|
|
60
|
+
return users.findList({ ...body, withTotal: false }); // meta.total_items is null, no count query
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Scope every call to one tenant
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import type { Repository } from "@querykitjs/mongodb";
|
|
68
|
+
|
|
69
|
+
declare const orders: Repository<{ _id: unknown; shop_id: unknown; total: number }>;
|
|
70
|
+
|
|
71
|
+
const shopOrders = orders.scoped({ shop_id: { in: ["65a1b2c3d4e5f60718293a4b"] } });
|
|
72
|
+
await shopOrders.updateWhere([{ key: "total", operation: ">", value: 0 }], { total: 0 });
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Check an index in a test
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
import { explainFind } from "@querykitjs/mongodb/testing";
|
|
79
|
+
import type { Repository } from "@querykitjs/mongodb";
|
|
80
|
+
|
|
81
|
+
declare const users: Repository<{ _id: unknown; name: string }>;
|
|
82
|
+
|
|
83
|
+
const plan = await explainFind(users, { filter: [{ key: "name", operation: "startsWith", value: "ali" }] });
|
|
84
|
+
console.log(JSON.stringify(plan).includes("IXSCAN"));
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Request contract
|
|
88
|
+
|
|
89
|
+
The same JSON shape as every querykit package (`@querykitjs/core`): conditions `{ key, operation?, value? }`, groups `{ and }` / `{ or }` / `{ not }` (a flat array = AND), `sort` always `[{ key, direction? }]`, offset `page` / `perPage` / `withTotal`, infinite `limit` / `offset`, cursor `limit` / `cursor` / `cursorKey` / `order` / `direction` (no `sort`), snake_case meta (`total_items`, `has_next`, `next_cursor`, …). `id` means `_id`. Refusals are `QueryKitError` with `{ source, site, key, operation?, reason }`.
|
|
90
|
+
|
|
91
|
+
## Mistakes to avoid
|
|
92
|
+
|
|
93
|
+
- Forgetting `objectId` for a reference field such as `shop_id` — a 24-hex string then never matches the stored `ObjectId`.
|
|
94
|
+
- Expecting a numeric `id` to match: MongoDB ids are ObjectIds, so `{ key: "id", value: 2 }` is `invalid-value`.
|
|
95
|
+
- Opening `contains` / `endsWith` on a large collection — they cannot use an index.
|
|
96
|
+
- Running `registry.transaction` on a standalone `mongod` — transactions need a replica set.
|
|
97
|
+
- Importing `explainFind` from `@querykitjs/mongodb` — it lives only in `@querykitjs/mongodb/testing`.
|