@nxgt/mongo 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Steve Tsala
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,275 @@
1
+ # @nxgt/mongo
2
+
3
+ A typed MongoDB collection, from one Zod schema: the schema types every read
4
+ and write, and the same schema becomes the collection's `$jsonSchema`
5
+ validator, applied idempotently. On top of it, a repository with pagination,
6
+ transactions, optimistic locking, soft delete, audit stamps, and MongoDB's
7
+ errors turned into ones you can catch.
8
+
9
+ It wraps the official `mongodb` driver, which stays a peer dependency: the
10
+ driver's collection is one property away at any time.
11
+
12
+ ## Install
13
+
14
+ ```sh
15
+ bun add @nxgt/mongo mongodb zod
16
+ ```
17
+
18
+ `mongodb` (>=7) and `zod` (>=4.6.5) are peer dependencies, so your application
19
+ decides their versions and there is only ever one copy of each.
20
+
21
+ ## Setup
22
+
23
+ ```ts
24
+ import { MongoClient } from 'mongodb';
25
+ import { defineCollection, id, timestamps, softDelete, optimisticLock, actors } from '@nxgt/mongo';
26
+ import { z } from 'zod';
27
+
28
+ export const users = defineCollection({
29
+ name: 'users',
30
+ schema: z.object({
31
+ _id: id(),
32
+ email: z.email(),
33
+ name: z.string().nullable().default(null),
34
+ ...timestamps(),
35
+ ...softDelete(),
36
+ ...optimisticLock(),
37
+ ...actors(),
38
+ }),
39
+ indexes: [{ key: { email: 1 }, unique: true, name: 'users_email_unique' }],
40
+ });
41
+
42
+ const client = await MongoClient.connect(process.env.MONGO_URL);
43
+ const db = client.db('app');
44
+ ```
45
+
46
+ ## Definition
47
+
48
+ `defineCollection` takes the collection's name, the Zod schema of its
49
+ documents, its indexes as the driver describes them, and how its validator is
50
+ applied.
51
+
52
+ The schema is the one source of truth. `z.output` is what a read gives back,
53
+ `z.input` what a write takes: a field with a default — `_id`, `createdAt`,
54
+ `version` — is optional to write and always there once read.
55
+
56
+ The field helpers are ordinary Zod schemas, so a collection can take some of
57
+ them, all of them, or none:
58
+
59
+ | helper | fields | what the repository does with them |
60
+ | --- | --- | --- |
61
+ | `id()` | `_id` | a fresh `ObjectId` on create |
62
+ | `objectId()` | — | an `ObjectId`, declared as `bsonType: 'objectId'` |
63
+ | `timestamps()` | `createdAt`, `updatedAt` | sets `updatedAt` on every update |
64
+ | `softDelete()` | `deletedAt` | `delete` sets it, reads leave those documents out |
65
+ | `optimisticLock()` | `version` | raised on every update; `expectedVersion` checks it |
66
+ | `actors(schema?)` | `createdBy`, `updatedBy`, `deletedBy` | stamped from `repository.as(actor)` |
67
+
68
+ ## Sync
69
+
70
+ `sync` creates the collection with its validator, writes the validator when it
71
+ changed, creates the indexes that are missing, and rebuilds those whose options
72
+ changed. Run it twice and the second run sends nothing.
73
+
74
+ ```ts
75
+ import { syncCollections } from '@nxgt/mongo';
76
+
77
+ const reports = await syncCollections(db, [users, teams]);
78
+ // [{ name: 'users', created: true, validator: 'created',
79
+ // indexes: { created: ['users_email_unique'], recreated: [], dropped: [], unchanged: [] } }]
80
+
81
+ // What a deploy would do, without doing it:
82
+ await syncCollections(db, [users, teams], { dryRun: true });
83
+ ```
84
+
85
+ It is a deployment step, not a request-time one: writing a validator runs
86
+ `collMod`, which needs the `dbAdmin` role that an application's own user does
87
+ not have, and neither it nor an index build may run inside a transaction.
88
+
89
+ `validation: { level: 'off' }` writes no validator at all, and removes one that
90
+ is already there. `{ action: 'warn' }` logs a document that fails instead of
91
+ refusing it, which is how a validator is rolled out onto a collection that is
92
+ already full.
93
+
94
+ ## Documents
95
+
96
+ ```ts
97
+ import { createRepository } from '@nxgt/mongo';
98
+
99
+ const repo = createRepository(db, users);
100
+
101
+ const ada = await repo.create({ email: 'ada@example.com' });
102
+ // → { _id: ObjectId, email, name: null, createdAt: Date, version: 0, … }
103
+
104
+ await repo.findById(ada._id); // the document, or undefined
105
+ await repo.getById(ada._id); // or NotFoundError
106
+ await repo.findFirst({ email: 'ada@example.com' });
107
+ await repo.findMany({ filter: { name: null }, sort: { createdAt: -1 }, limit: 10 });
108
+ await repo.count({ name: null });
109
+ await repo.exists({ email: 'ada@example.com' });
110
+
111
+ await repo.update(ada._id, { name: 'Ada' }); // checked field by field
112
+ await repo.update(ada._id, { $inc: { logins: 1 } }); // MongoDB's operators too
113
+ await repo.updateMany({ name: null }, { name: 'unknown' });
114
+
115
+ await repo.delete(ada._id); // soft, on a schema with deletedAt
116
+ await repo.restore(ada._id);
117
+ await repo.hardDelete(ada._id); // really gone
118
+ ```
119
+
120
+ `create` checks the document against the schema before sending it, which is
121
+ also what fills its defaults. `update` checks each field of a patch — the
122
+ driver's own `UpdateFilter` is intersected with `Document` and accepts any key
123
+ whatsoever, including a typo.
124
+
125
+ ## Pagination
126
+
127
+ ```ts
128
+ const page = await repo.paginate({ filter: { name: null }, page: 2, pageSize: 20 });
129
+ // { items, total, page, pageSize, pageCount }
130
+
131
+ let after: string | null = null;
132
+ do {
133
+ const page = await repo.paginateByCursor({ after, limit: 100, orderBy: 'createdAt', direction: 'desc' });
134
+ send(page.items);
135
+ after = page.nextCursor;
136
+ } while (after);
137
+ ```
138
+
139
+ The cursor is a keyset on the field and `_id`, which breaks its ties: no
140
+ document is repeated or skipped while the collection is written to, where
141
+ `skip` would do both. It is opaque and URL-safe, and it survives `ObjectId`,
142
+ `Date` and `bigint` values. It is encoded, not signed.
143
+
144
+ ## Transactions
145
+
146
+ ```ts
147
+ import { withTransaction } from '@nxgt/mongo';
148
+
149
+ await withTransaction(client, async (session) => {
150
+ const team = await teams.with(session).create({ name: 'Core' });
151
+ await users.with(session).update(userId, { teamId: team._id });
152
+ });
153
+ ```
154
+
155
+ **Every operation has to be given the session.** MongoDB has no ambient
156
+ session: a write that was not given one runs outside the transaction and is not
157
+ rolled back with it. `repository.with(session)` is how a repository takes it,
158
+ and it returns a new repository rather than changing the one you have.
159
+
160
+ Given a session that is already in a transaction, `withTransaction` joins it.
161
+ MongoDB has no savepoints, so an inner failure takes the whole transaction
162
+ down.
163
+
164
+ ## Optimistic locking
165
+
166
+ On a schema with `optimisticLock()`, every update raises `version`. Pass the
167
+ version you read and the update only applies while the document is still that
168
+ one:
169
+
170
+ ```ts
171
+ const user = await repo.getById(id);
172
+ try {
173
+ await repo.update(id, { name: 'Ada' }, { expectedVersion: user.version });
174
+ } catch (error) {
175
+ if (error instanceof OptimisticLockError) {
176
+ // error.expectedVersion, error.actualVersion: someone else wrote first
177
+ }
178
+ }
179
+ ```
180
+
181
+ ## Errors
182
+
183
+ Every method turns a MongoDB error into one of this package's, so an
184
+ application never reads a numeric code:
185
+
186
+ | error | `code` | when |
187
+ | --- | --- | --- |
188
+ | `NotFoundError` | `NOT_FOUND` | a method by `_id` matched nothing |
189
+ | `ConflictError` | `CONFLICT` | a unique index refused the write (`E11000`) |
190
+ | `ValidationError` | `VALIDATION` | the collection's validator refused it (121) |
191
+ | `OptimisticLockError` | `OPTIMISTIC_LOCK` | `expectedVersion` no longer matches |
192
+ | `InvalidCursorError` | `INVALID_CURSOR` | a cursor this package did not write |
193
+ | `DataError` | `DATABASE` | any other server error, with its `serverCode` |
194
+
195
+ `ConflictError` carries `index`, `keys` and, when the server gives them,
196
+ `values`. `ValidationError` carries `issues`, MongoDB's `errInfo` flattened
197
+ into `{ path, reason, specifiedAs, consideredValue }`. Anything that is not a
198
+ server error reaches you untouched.
199
+
200
+ ## Not included
201
+
202
+ - **No aggregation helpers.** `repository.collection` is the driver's
203
+ collection: `.aggregate()`, `.watch()` and the rest are there.
204
+ - **No migrations.** `sync` brings the schema and the indexes in line; it never
205
+ rewrites a document.
206
+ - **No connection management.** The client is yours to open and close.
207
+
208
+ ## API
209
+
210
+ | export | what it is |
211
+ | --- | --- |
212
+ | `defineCollection(config)` | a collection: name, schema, indexes, validation |
213
+ | `id`, `objectId`, `timestamps`, `softDelete`, `optimisticLock`, `actors` | the field helpers |
214
+ | `createRepository(db, definition, options?)` | the typed repository |
215
+ | `syncCollection`, `syncCollections` | create and bring in line, with `dryRun` |
216
+ | `withTransaction(clientOrSession, fn, options?)` | a transaction, joined when nested |
217
+ | `toMongoJsonSchema(schema)` | a Zod schema as a MongoDB `$jsonSchema` |
218
+ | `encodeCursor`, `decodeCursor`, `pageWindow`, `toPage` | the pagination pieces |
219
+ | `DataError` and its subclasses, `toDataError` | the errors |
220
+ | `diffIndexes`, `normalizeIndex`, `validationMatches` | what `sync` compares with |
221
+
222
+ `RepositoryOptions` turns the behaviours off one by one: `softDelete`,
223
+ `touchUpdatedAt`, `optimisticLock`, `validate: 'off'`, `maxPageSize`.
224
+
225
+ ## Traps
226
+
227
+ - **There is no ambient session.** An operation inside `withTransaction` that
228
+ was not given the session is not part of the transaction. Use
229
+ `repository.with(session)` for every one of them.
230
+ - **`$jsonSchema` is not JSON Schema.** MongoDB rejects `$ref`, `$schema`,
231
+ `default`, `format` and `id`, has no `integer` type, and treats a keyword it
232
+ does not know as an error rather than ignoring it. `toMongoJsonSchema`
233
+ inlines every `$ref` and keeps only what MongoDB knows — so **a recursive
234
+ schema cannot be a validator**, and it throws rather than writing one that
235
+ would be refused.
236
+ - **A whole number is not an `int` past 32 bits.** `z.int()` becomes
237
+ `bsonType: ['int', 'long', 'double']` with `multipleOf: 1`, because the
238
+ driver sends a large integer as a double. A validator that asked for `int`
239
+ alone would refuse a number your own schema accepts.
240
+ - **The validator is strict about unknown fields.** `z.object()` gives
241
+ `additionalProperties: false`, so a field that is written but not in the
242
+ schema is refused. `z.looseObject()` is the way out.
243
+ - **Existing documents are never checked** until they are modified: adding a
244
+ validator to a full collection refuses nothing retroactively, and
245
+ `validationLevel: 'moderate'` keeps it that way for updates too.
246
+ - **Writing a validator needs `dbAdmin`.** `collMod` is not granted by
247
+ `readWrite`. Sync with a deployment credential, not the application's.
248
+ - **A duplicate key from a bulk write carries no values.** MongoDB puts
249
+ `keyPattern` and `keyValue` on a single write's error only; for
250
+ `createMany`, `ConflictError.keys` is parsed out of the message and `values`
251
+ is `undefined`.
252
+ - **`updateMany` and `deleteMany` refuse an empty filter.** Pass
253
+ `{ _id: { $exists: true } }` to mean every document, so that a filter built
254
+ from a variable that came out empty cannot rewrite the collection.
255
+ - **Rebuilding an index drops it first.** MongoDB cannot alter an index in
256
+ place, so `sync` drops and recreates one whose options changed: there is a
257
+ window with no index, and on a large collection the rebuild is not free.
258
+ - **`validate: 'off'` also turns the defaults off.** Nothing fills `_id`,
259
+ `createdAt` or `version` any more, because filling them is what parsing does.
260
+ - **The driver retries a transaction's callback** on a transient error, for up
261
+ to 120 seconds, so `fn` must be safe to run twice — and must not swallow
262
+ errors, or the driver cannot tell whether the transaction was aborted.
263
+ - **`session.abortTransaction()` inside the callback resolves.**
264
+ `withTransaction` returns the callback's value; it does not throw.
265
+
266
+ ## Testing against a real MongoDB
267
+
268
+ Transactions need a replica set, which a standalone `mongod` is not. This
269
+ package's own specs run against a single-node replica set from
270
+ `mongodb-memory-server-core`, with no Docker; the same works in any consumer's
271
+ test suite.
272
+
273
+ ## License
274
+
275
+ MIT
@@ -0,0 +1,82 @@
1
+ import type { IndexDescription, ObjectId } from 'mongodb';
2
+ import type { z } from 'zod';
3
+ /** What MongoDB does with a document that fails the validator. */
4
+ export type ValidationAction = 'error' | 'warn';
5
+ /**
6
+ * Which documents the validator applies to. `off` writes no validator at all;
7
+ * `moderate` exempts documents that were already invalid from updates.
8
+ */
9
+ export type ValidationLevel = 'off' | 'moderate' | 'strict';
10
+ export interface ValidationConfig {
11
+ /** Default `'strict'`. */
12
+ level?: ValidationLevel;
13
+ /** Default `'error'`. `'warn'` logs and lets the write through. */
14
+ action?: ValidationAction;
15
+ }
16
+ /** What `defineCollection` takes. */
17
+ export interface CollectionConfig<Schema extends z.ZodObject> {
18
+ /** The collection's name on the server. */
19
+ name: string;
20
+ /**
21
+ * The documents, as they are stored: `z.output` is what a read gives back,
22
+ * `z.input` what a write takes. It must have an `_id`.
23
+ */
24
+ schema: Schema;
25
+ /** The indexes `sync` creates, as the driver describes them. */
26
+ indexes?: readonly IndexDescription[];
27
+ /** The `$jsonSchema` validator `sync` writes from the schema. */
28
+ validation?: ValidationConfig;
29
+ }
30
+ /** A collection, as `defineCollection` returns it: frozen, with its defaults. */
31
+ export interface CollectionDefinition<Schema extends z.ZodObject = z.ZodObject> extends Readonly<CollectionConfig<Schema>> {
32
+ readonly indexes: readonly IndexDescription[];
33
+ readonly validation: Required<ValidationConfig>;
34
+ }
35
+ /** Any definition, whatever its documents. */
36
+ export type AnyCollectionDefinition = CollectionDefinition<any>;
37
+ /** The documents of a definition, as they are read back. */
38
+ export type DocumentOf<Def> = Def extends {
39
+ schema: infer Schema;
40
+ } ? Schema extends z.ZodType ? z.output<Schema> : never : never;
41
+ /** What a write takes: the documents before their defaults are filled. */
42
+ export type NewDocumentOf<Def> = Def extends {
43
+ schema: infer Schema;
44
+ } ? Schema extends z.ZodType ? z.input<Schema> : never : never;
45
+ /** The type of `_id`. */
46
+ export type IdOf<Def> = DocumentOf<Def> extends {
47
+ _id: infer Id;
48
+ } ? Id : ObjectId;
49
+ /** A field of the documents, as a top-level key. */
50
+ export type FieldOf<Def> = keyof DocumentOf<Def> & string;
51
+ /**
52
+ * Defines a collection: its name, the Zod schema of its documents, its
53
+ * indexes, and how its validator is applied.
54
+ *
55
+ * The schema is the one source: it types every read and write, and `sync`
56
+ * derives the collection's `$jsonSchema` validator from it.
57
+ *
58
+ * ```ts
59
+ * export const users = defineCollection({
60
+ * name: 'users',
61
+ * schema: z.object({
62
+ * _id: id(),
63
+ * email: z.email(),
64
+ * ...timestamps(),
65
+ * ...softDelete(),
66
+ * }),
67
+ * indexes: [{ key: { email: 1 }, unique: true, name: 'users_email_unique' }],
68
+ * });
69
+ * ```
70
+ */
71
+ export declare function defineCollection<Schema extends z.ZodObject>(config: CollectionConfig<Schema>): CollectionDefinition<Schema>;
72
+ /** Which of the fields the repository knows about a definition's schema has. */
73
+ export declare function stampsOf(definition: AnyCollectionDefinition): {
74
+ createdAt: boolean;
75
+ updatedAt: boolean;
76
+ deletedAt: boolean;
77
+ version: boolean;
78
+ createdBy: boolean;
79
+ updatedBy: boolean;
80
+ deletedBy: boolean;
81
+ };
82
+ //# sourceMappingURL=define-collection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"define-collection.d.ts","sourceRoot":"","sources":["../../src/definition/define-collection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC1D,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAE7B,kEAAkE;AAClE,MAAM,MAAM,gBAAgB,GAAG,OAAO,GAAG,MAAM,CAAC;AAEhD;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,UAAU,GAAG,QAAQ,CAAC;AAE5D,MAAM,WAAW,gBAAgB;IAChC,0BAA0B;IAC1B,KAAK,CAAC,EAAE,eAAe,CAAC;IACxB,mEAAmE;IACnE,MAAM,CAAC,EAAE,gBAAgB,CAAC;CAC1B;AAED,qCAAqC;AACrC,MAAM,WAAW,gBAAgB,CAAC,MAAM,SAAS,CAAC,CAAC,SAAS;IAC3D,2CAA2C;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,OAAO,CAAC,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACtC,iEAAiE;IACjE,UAAU,CAAC,EAAE,gBAAgB,CAAC;CAC9B;AAED,iFAAiF;AACjF,MAAM,WAAW,oBAAoB,CAAC,MAAM,SAAS,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,SAAS,CAC7E,SAAQ,QAAQ,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC;IAC1C,QAAQ,CAAC,OAAO,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC9C,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,gBAAgB,CAAC,CAAC;CAChD;AAED,8CAA8C;AAC9C,MAAM,MAAM,uBAAuB,GAAG,oBAAoB,CAAC,GAAG,CAAC,CAAC;AAEhE,4DAA4D;AAC5D,MAAM,MAAM,UAAU,CAAC,GAAG,IAAI,GAAG,SAAS;IAAE,MAAM,EAAE,MAAM,MAAM,CAAA;CAAE,GAC/D,MAAM,SAAS,CAAC,CAAC,OAAO,GACvB,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,GAChB,KAAK,GACN,KAAK,CAAC;AAET,0EAA0E;AAC1E,MAAM,MAAM,aAAa,CAAC,GAAG,IAAI,GAAG,SAAS;IAAE,MAAM,EAAE,MAAM,MAAM,CAAA;CAAE,GAClE,MAAM,SAAS,CAAC,CAAC,OAAO,GACvB,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,GACf,KAAK,GACN,KAAK,CAAC;AAET,yBAAyB;AACzB,MAAM,MAAM,IAAI,CAAC,GAAG,IACnB,UAAU,CAAC,GAAG,CAAC,SAAS;IAAE,GAAG,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,EAAE,GAAG,QAAQ,CAAC;AAE3D,oDAAoD;AACpD,MAAM,MAAM,OAAO,CAAC,GAAG,IAAI,MAAM,UAAU,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC;AAE1D;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,SAAS,CAAC,CAAC,SAAS,EAC1D,MAAM,EAAE,gBAAgB,CAAC,MAAM,CAAC,GAC9B,oBAAoB,CAAC,MAAM,CAAC,CAgB9B;AAED,gFAAgF;AAChF,wBAAgB,QAAQ,CAAC,UAAU,EAAE,uBAAuB,GAAG;IAC9D,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;CACnB,CAYA"}
@@ -0,0 +1,57 @@
1
+ import { ObjectId } from 'mongodb';
2
+ import { z } from 'zod';
3
+ /**
4
+ * An `ObjectId`, declared to MongoDB as `bsonType: 'objectId'`. JSON Schema
5
+ * has no type for one, so the metadata is how the validator learns of it.
6
+ */
7
+ export declare function objectId(): z.ZodCustom<ObjectId, ObjectId>;
8
+ /**
9
+ * `_id`, filled with a new `ObjectId` when a document is created: optional to
10
+ * write, always there once read.
11
+ */
12
+ export declare function id(): z.ZodDefault<z.ZodCustom<ObjectId, ObjectId>>;
13
+ /**
14
+ * `createdAt` and `updatedAt`, filled on create. A repository sets `updatedAt`
15
+ * on every update.
16
+ */
17
+ export declare function timestamps(): {
18
+ createdAt: z.ZodDefault<z.ZodDate>;
19
+ updatedAt: z.ZodDefault<z.ZodDate>;
20
+ };
21
+ /**
22
+ * `deletedAt`, `null` while the document is live. A repository on a collection
23
+ * with it soft-deletes, and leaves deleted documents out of every read.
24
+ */
25
+ export declare function softDelete(): {
26
+ deletedAt: z.ZodDefault<z.ZodNullable<z.ZodDate>>;
27
+ };
28
+ /**
29
+ * `version`, raised by one on every update. A repository with it takes
30
+ * `expectedVersion` and throws `OptimisticLockError` when it no longer
31
+ * matches.
32
+ */
33
+ export declare function optimisticLock(): {
34
+ version: z.ZodDefault<z.ZodInt>;
35
+ };
36
+ /**
37
+ * `createdBy`, `updatedBy` and `deletedBy`, stamped from the actor a
38
+ * repository was given with `as(actor)`. The actor's own type is the schema
39
+ * passed in, an `ObjectId` by default.
40
+ */
41
+ export declare function actors<Actor extends z.ZodType = ReturnType<typeof objectId>>(actor?: Actor): {
42
+ createdBy: z.ZodDefault<z.ZodNullable<Actor>>;
43
+ updatedBy: z.ZodDefault<z.ZodNullable<Actor>>;
44
+ deletedBy: z.ZodDefault<z.ZodNullable<Actor>>;
45
+ };
46
+ /** The fields the repository gives a meaning to, by name. */
47
+ export declare const STAMP_FIELDS: {
48
+ readonly id: "_id";
49
+ readonly createdAt: "createdAt";
50
+ readonly updatedAt: "updatedAt";
51
+ readonly deletedAt: "deletedAt";
52
+ readonly version: "version";
53
+ readonly createdBy: "createdBy";
54
+ readonly updatedBy: "updatedBy";
55
+ readonly deletedBy: "deletedBy";
56
+ };
57
+ //# sourceMappingURL=fields.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fields.d.ts","sourceRoot":"","sources":["../../src/definition/fields.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAWxB;;;GAGG;AACH,wBAAgB,QAAQ,oCAIvB;AAED;;;GAGG;AACH,wBAAgB,EAAE,kDAEjB;AAED;;;GAGG;AACH,wBAAgB,UAAU;;;EAKzB;AAED;;;GAGG;AACH,wBAAgB,UAAU;;EAEzB;AAED;;;;GAIG;AACH,wBAAgB,cAAc;;EAE7B;AAED;;;;GAIG;AACH,wBAAgB,MAAM,CAAC,KAAK,SAAS,CAAC,CAAC,OAAO,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,EAC3E,KAAK,GAAE,KAAsC;;;;EAO7C;AAED,6DAA6D;AAC7D,eAAO,MAAM,YAAY;;;;;;;;;CASf,CAAC"}
@@ -0,0 +1,27 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * Every keyword MongoDB's `$jsonSchema` knows. It **rejects** a document that
4
+ * uses any other, rather than ignoring it, so anything not in here is dropped
5
+ * on the way out.
6
+ */
7
+ export declare const MONGO_JSON_SCHEMA_KEYWORDS: ReadonlySet<string>;
8
+ /**
9
+ * A Zod schema as a MongoDB `$jsonSchema`, ready for a collection's validator.
10
+ *
11
+ * `z.toJSONSchema` alone is not one: MongoDB rejects `$schema`, `$ref`,
12
+ * `definitions`, `default`, `format` and `id`, has no `integer` type, and
13
+ * treats a keyword it does not know as an error rather than ignoring it. This
14
+ * resolves every `$ref` by inlining it, keeps only the keywords MongoDB
15
+ * knows, and maps `integer`.
16
+ *
17
+ * `Date` and `ObjectId` have no JSON Schema type: they are declared with
18
+ * `bsonType`, which `date()` and `objectId()` already carry in their metadata.
19
+ * Any schema can do the same with `.meta({ bsonType: 'decimal' })`.
20
+ *
21
+ * ```ts
22
+ * toMongoJsonSchema(z.object({ _id: objectId(), email: z.string() }));
23
+ * // { type: 'object', properties: { … }, required: ['_id', 'email'], … }
24
+ * ```
25
+ */
26
+ export declare function toMongoJsonSchema(schema: z.ZodType): Record<string, unknown>;
27
+ //# sourceMappingURL=json-schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json-schema.d.ts","sourceRoot":"","sources":["../../src/definition/json-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;GAIG;AACH,eAAO,MAAM,0BAA0B,EAAE,WAAW,CAAC,MAAM,CA8BzD,CAAC;AA4FH;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAsB5E"}
@@ -0,0 +1,92 @@
1
+ /** What went wrong, as a string a caller can switch on. */
2
+ export type DataErrorCode = 'DATABASE' | 'NOT_FOUND' | 'CONFLICT' | 'VALIDATION' | 'OPTIMISTIC_LOCK' | 'INVALID_CURSOR';
3
+ /** One reason a document failed the collection's `$jsonSchema` validator. */
4
+ export interface ValidationIssue {
5
+ /** The dotted path of the field, empty for the document itself. */
6
+ path: string;
7
+ /** The rule it broke: `bsonType`, `required`, `minimum`… */
8
+ reason: string;
9
+ /** What the schema asked for, as MongoDB reports it. */
10
+ specifiedAs?: unknown;
11
+ /** The value that was refused, when the server names it. */
12
+ consideredValue?: unknown;
13
+ /** Its BSON type, when the server names it: `string`, `int`, `double`… */
14
+ consideredType?: string;
15
+ /** The schema's `description` for the field, when it has one. */
16
+ description?: string;
17
+ }
18
+ export interface DataErrorOptions {
19
+ collection?: string | undefined;
20
+ /** The `_id` a method by id was given. */
21
+ id?: unknown;
22
+ /** MongoDB's numeric error code: 11000, 121, 26… */
23
+ serverCode?: number | undefined;
24
+ /** MongoDB's `codeName`, which write errors do not carry. */
25
+ serverCodeName?: string | undefined;
26
+ /** The index a conflict names, when the server names one. */
27
+ index?: string | undefined;
28
+ /** The fields the error is about: an index's keys, or a validator's paths. */
29
+ keys?: string[];
30
+ /** Those fields' values, when the server gives them. */
31
+ values?: Record<string, unknown> | undefined;
32
+ issues?: ValidationIssue[];
33
+ expectedVersion?: number | undefined;
34
+ actualVersion?: number | undefined;
35
+ cause?: unknown;
36
+ }
37
+ /**
38
+ * What this package throws. Every method turns a driver error into one of
39
+ * these, so an application catches `ConflictError` instead of reading `11000`
40
+ * off an error whose shape changes with the operation that produced it.
41
+ *
42
+ * A driver error that is none of them reaches the caller as it is.
43
+ */
44
+ export declare class DataError extends Error {
45
+ name: string;
46
+ readonly code: DataErrorCode;
47
+ readonly collection: string | undefined;
48
+ readonly id: unknown;
49
+ readonly serverCode: number | undefined;
50
+ readonly serverCodeName: string | undefined;
51
+ readonly index: string | undefined;
52
+ readonly keys: string[];
53
+ readonly values: Record<string, unknown> | undefined;
54
+ readonly issues: ValidationIssue[];
55
+ readonly expectedVersion: number | undefined;
56
+ readonly actualVersion: number | undefined;
57
+ constructor(message?: string, options?: DataErrorOptions);
58
+ }
59
+ /** No document matched, where one was required. */
60
+ export declare class NotFoundError extends DataError {
61
+ name: string;
62
+ readonly code: "NOT_FOUND";
63
+ constructor(message?: string, options?: DataErrorOptions);
64
+ }
65
+ /** A unique index refused the write: MongoDB's `E11000`. */
66
+ export declare class ConflictError extends DataError {
67
+ name: string;
68
+ readonly code: "CONFLICT";
69
+ constructor(message?: string, options?: DataErrorOptions);
70
+ }
71
+ /** The collection's `$jsonSchema` validator refused the document: code 121. */
72
+ export declare class ValidationError extends DataError {
73
+ name: string;
74
+ readonly code: "VALIDATION";
75
+ constructor(message?: string, options?: DataErrorOptions);
76
+ }
77
+ /**
78
+ * The document changed since it was read: its `version` is no longer the one
79
+ * the update expected, and nothing was written.
80
+ */
81
+ export declare class OptimisticLockError extends DataError {
82
+ name: string;
83
+ readonly code: "OPTIMISTIC_LOCK";
84
+ constructor(message?: string, options?: DataErrorOptions);
85
+ }
86
+ /** A cursor this package did not write, or one for another ordering. */
87
+ export declare class InvalidCursorError extends DataError {
88
+ name: string;
89
+ readonly code: "INVALID_CURSOR";
90
+ constructor(message?: string, options?: DataErrorOptions);
91
+ }
92
+ //# sourceMappingURL=data-error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"data-error.d.ts","sourceRoot":"","sources":["../../src/errors/data-error.ts"],"names":[],"mappings":"AAAA,2DAA2D;AAC3D,MAAM,MAAM,aAAa,GACtB,UAAU,GACV,WAAW,GACX,UAAU,GACV,YAAY,GACZ,iBAAiB,GACjB,gBAAgB,CAAC;AAEpB,6EAA6E;AAC7E,MAAM,WAAW,eAAe;IAC/B,mEAAmE;IACnE,IAAI,EAAE,MAAM,CAAC;IACb,4DAA4D;IAC5D,MAAM,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,4DAA4D;IAC5D,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,0EAA0E;IAC1E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAChC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,0CAA0C;IAC1C,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,oDAAoD;IACpD,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,6DAA6D;IAC7D,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,6DAA6D;IAC7D,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,8EAA8E;IAC9E,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,wDAAwD;IACxD,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;IAC7C,MAAM,CAAC,EAAE,eAAe,EAAE,CAAC;IAC3B,eAAe,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,KAAK,CAAC,EAAE,OAAO,CAAC;CAChB;AAED;;;;;;GAMG;AACH,qBAAa,SAAU,SAAQ,KAAK;IAC1B,IAAI,SAAe;IAC5B,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAc;IAC1C,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IACxC,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IACxC,QAAQ,CAAC,cAAc,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5C,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;IACrD,QAAQ,CAAC,MAAM,EAAE,eAAe,EAAE,CAAC;IACnC,QAAQ,CAAC,eAAe,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7C,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;gBAE/B,OAAO,SAAmB,EAAE,OAAO,GAAE,gBAAqB;CAgBtE;AAED,mDAAmD;AACnD,qBAAa,aAAc,SAAQ,SAAS;IAClC,IAAI,SAAmB;IAChC,SAAkB,IAAI,EAAG,WAAW,CAAU;gBAElC,OAAO,SAAc,EAAE,OAAO,GAAE,gBAAqB;CAGjE;AAED,4DAA4D;AAC5D,qBAAa,aAAc,SAAQ,SAAS;IAClC,IAAI,SAAmB;IAChC,SAAkB,IAAI,EAAG,UAAU,CAAU;gBAEjC,OAAO,SAAkB,EAAE,OAAO,GAAE,gBAAqB;CAGrE;AAED,+EAA+E;AAC/E,qBAAa,eAAgB,SAAQ,SAAS;IACpC,IAAI,SAAqB;IAClC,SAAkB,IAAI,EAAG,YAAY,CAAU;gBAG9C,OAAO,SAA+B,EACtC,OAAO,GAAE,gBAAqB;CAI/B;AAED;;;GAGG;AACH,qBAAa,mBAAoB,SAAQ,SAAS;IACxC,IAAI,SAAyB;IACtC,SAAkB,IAAI,EAAG,iBAAiB,CAAU;gBAExC,OAAO,SAAqB,EAAE,OAAO,GAAE,gBAAqB;CAGxE;AAED,wEAAwE;AACxE,qBAAa,kBAAmB,SAAQ,SAAS;IACvC,IAAI,SAAwB;IACrC,SAAkB,IAAI,EAAG,gBAAgB,CAAU;gBAEvC,OAAO,SAAmB,EAAE,OAAO,GAAE,gBAAqB;CAGtE"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * A MongoDB error as one of this package's, or the error itself when it is
3
+ * none of them.
4
+ *
5
+ * It reads the error's fields rather than its class: a duplicate key arrives
6
+ * as a `MongoServerError` with `keyPattern` from `insertOne`, and as a
7
+ * `MongoBulkWriteError` whose `writeErrors` carry neither `keyPattern` nor
8
+ * `keyValue` from `insertMany` and `bulkWrite`. Both become a `ConflictError`
9
+ * with the same fields. Reading fields also survives two copies of the driver
10
+ * in one tree, where `instanceof` does not.
11
+ */
12
+ export declare function toDataError(error: unknown, context?: {
13
+ collection?: string | undefined;
14
+ }): unknown;
15
+ //# sourceMappingURL=to-data-error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"to-data-error.d.ts","sourceRoot":"","sources":["../../src/errors/to-data-error.ts"],"names":[],"mappings":"AA0HA;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CAC1B,KAAK,EAAE,OAAO,EACd,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAAO,GAC/C,OAAO,CAoDT"}
@@ -0,0 +1,14 @@
1
+ export { type AnyCollectionDefinition, type CollectionConfig, type CollectionDefinition, type DocumentOf, defineCollection, type FieldOf, type IdOf, type NewDocumentOf, stampsOf, type ValidationAction, type ValidationConfig, type ValidationLevel, } from './definition/define-collection';
2
+ export { actors, id, objectId, optimisticLock, STAMP_FIELDS, softDelete, timestamps, } from './definition/fields';
3
+ export { MONGO_JSON_SCHEMA_KEYWORDS, toMongoJsonSchema, } from './definition/json-schema';
4
+ export { ConflictError, DataError, type DataErrorCode, type DataErrorOptions, InvalidCursorError, NotFoundError, OptimisticLockError, ValidationError, type ValidationIssue, } from './errors/data-error';
5
+ export { toDataError } from './errors/to-data-error';
6
+ export { type CursorPayload, decodeCursor, encodeCursor, } from './pagination/cursor';
7
+ export { type CursorPage, cursorLimit, DEFAULT_MAX_PAGE_SIZE, DEFAULT_PAGE_SIZE, type Page, type PageOptions, type PageWindow, pageWindow, toPage, } from './pagination/page';
8
+ export { createRepository } from './repository/create-repository';
9
+ export type { CursorPaginateOptions, FindFirstOptions, FindManyOptions, OrderDirection, PaginateOptions, Patch, ReadOptions, Repository, RepositoryOptions, UpdateOptions, } from './repository/types';
10
+ export { diffIndexes, type IndexDiff, indexMatches, indexNameOf, type NormalizedIndex, normalizeIndex, } from './sync/index-diff';
11
+ export { type SyncOptions, type SyncReport, syncCollection, syncCollections, } from './sync/sync-collection';
12
+ export { hasValidator, type LiveValidation, validationMatches, type WantedValidation, } from './sync/validator-diff';
13
+ export { type TransactionHost, withTransaction, } from './transaction/with-transaction';
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,uBAAuB,EAC5B,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EACzB,KAAK,UAAU,EACf,gBAAgB,EAChB,KAAK,OAAO,EACZ,KAAK,IAAI,EACT,KAAK,aAAa,EAClB,QAAQ,EACR,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,eAAe,GACpB,MAAM,gCAAgC,CAAC;AACxC,OAAO,EACN,MAAM,EACN,EAAE,EACF,QAAQ,EACR,cAAc,EACd,YAAY,EACZ,UAAU,EACV,UAAU,GACV,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACN,0BAA0B,EAC1B,iBAAiB,GACjB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACN,aAAa,EACb,SAAS,EACT,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,kBAAkB,EAClB,aAAa,EACb,mBAAmB,EACnB,eAAe,EACf,KAAK,eAAe,GACpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EACN,KAAK,aAAa,EAClB,YAAY,EACZ,YAAY,GACZ,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACN,KAAK,UAAU,EACf,WAAW,EACX,qBAAqB,EACrB,iBAAiB,EACjB,KAAK,IAAI,EACT,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,UAAU,EACV,MAAM,GACN,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAClE,YAAY,EACX,qBAAqB,EACrB,gBAAgB,EAChB,eAAe,EACf,cAAc,EACd,eAAe,EACf,KAAK,EACL,WAAW,EACX,UAAU,EACV,iBAAiB,EACjB,aAAa,GACb,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACN,WAAW,EACX,KAAK,SAAS,EACd,YAAY,EACZ,WAAW,EACX,KAAK,eAAe,EACpB,cAAc,GACd,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACN,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,cAAc,EACd,eAAe,GACf,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACN,YAAY,EACZ,KAAK,cAAc,EACnB,iBAAiB,EACjB,KAAK,gBAAgB,GACrB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACN,KAAK,eAAe,EACpB,eAAe,GACf,MAAM,gCAAgC,CAAC"}