@wenu/mongo 0.2.4

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/CHANGELOG.md ADDED
@@ -0,0 +1,36 @@
1
+ # Changelog
2
+
3
+ ## [0.2.4](https://github.com/johnnyhuirilef/toolkit/compare/mongo-v0.2.3...mongo-v0.2.4) (2026-06-25)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **zod-mongo:** declare zod peer dep as required in peerDependenciesMeta ([2ed070b](https://github.com/johnnyhuirilef/toolkit/commit/2ed070b14ed8a984ca1c58ee0b6ce4dd4ebed2d4))
9
+
10
+ ## [0.2.3](https://github.com/johnnyhuirilef/toolkit/compare/mongo-v0.2.2...mongo-v0.2.3) (2026-06-25)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **zod-mongo:** broaden zod peer dependency to support zod v5 ([6b6a3c6](https://github.com/johnnyhuirilef/toolkit/commit/6b6a3c6e4596d71331b9f7440d25167d21d3e53f))
16
+
17
+ ## [0.2.2](https://github.com/johnnyhuirilef/toolkit/compare/mongo-v0.2.1...mongo-v0.2.2) (2026-06-25)
18
+
19
+
20
+ ### Bug Fixes
21
+
22
+ * **zod-mongo:** add funding field to package metadata ([ff4e95f](https://github.com/johnnyhuirilef/toolkit/commit/ff4e95fd1dea3bd0b48a523fe2d8185fb56af092))
23
+
24
+ ## [0.2.1](https://github.com/johnnyhuirilef/toolkit/compare/mongo-v0.2.0...mongo-v0.2.1) (2026-06-25)
25
+
26
+
27
+ ### Bug Fixes
28
+
29
+ * **zod-mongo:** add missing npm keywords for discoverability ([f52dc3d](https://github.com/johnnyhuirilef/toolkit/commit/f52dc3da7243ce4077347348d076f6477084220c))
30
+
31
+ ## [0.2.0](https://github.com/johnnyhuirilef/toolkit/compare/mongo-v0.1.0...mongo-v0.2.0) (2026-06-25)
32
+
33
+
34
+ ### Features
35
+
36
+ * add @wenu/mongo and @wenu/nest-mongo packages ([#17](https://github.com/johnnyhuirilef/toolkit/issues/17)) ([8635018](https://github.com/johnnyhuirilef/toolkit/commit/8635018605ab53468feb0257173e39126cecdcb2))
package/README.md ADDED
@@ -0,0 +1,535 @@
1
+ # @wenu/mongo
2
+
3
+ Declarative, immutable, type-safe MongoDB repository layer with Zod validation. Zero throws. Dual
4
+ ESM/CJS. MongoDB 5/6/7 compatible. Zod 3 and 4 compatible.
5
+
6
+ ## Table of Contents
7
+
8
+ - [Features](#features)
9
+ - [Installation](#installation)
10
+ - [Quick Start](#quick-start)
11
+ - [Core Concepts](#core-concepts)
12
+ - [Defining a Collection](#defining-a-collection)
13
+ - [Creating a Repository](#creating-a-repository)
14
+ - [Result Type](#result-type)
15
+ - [CRUD Operations](#crud-operations)
16
+ - [Error Handling](#error-handling)
17
+ - [ID Strategies](#id-strategies)
18
+ - [Index Management](#index-management)
19
+ - [Aggregation](#aggregation)
20
+ - [Compatibility](#compatibility)
21
+ - [API Reference](#api-reference)
22
+
23
+ ---
24
+
25
+ ## Features
26
+
27
+ - **Zero throws** — every method returns `Result<T, DbError>`, never throws
28
+ - **Full type inference** — document shape, `_id` type, and filter types flow from a single
29
+ `defineCollection()` call
30
+ - **Pluggable ID strategies** — `objectid` (default), `uuid`, `string`, or any Zod schema for custom
31
+ types
32
+ - **Zod validation on write** — inserts and updates are validated before touching the database
33
+ - **Immutable collection definitions** — `defineCollection()` returns a frozen, reusable descriptor
34
+ - **Index management** — declare indexes alongside the schema, sync or generate migrate-mongo
35
+ scripts
36
+ - **Typed aggregation** — `aggregate()` accepts an output schema and returns `Result<Infer<Out>[]>`
37
+ - **Dual ESM/CJS** — works in Node ESM projects and CommonJS consumers alike
38
+
39
+ ---
40
+
41
+ ## Installation
42
+
43
+ ```bash
44
+ # npm
45
+ npm install @wenu/mongo
46
+
47
+ # pnpm
48
+ pnpm add @wenu/mongo
49
+
50
+ # yarn
51
+ yarn add @wenu/mongo
52
+ ```
53
+
54
+ ### Peer dependencies
55
+
56
+ ```bash
57
+ # MongoDB driver (choose one range)
58
+ npm install mongodb@^5 # or ^6 or ^7
59
+
60
+ # Zod (v3 or v4)
61
+ npm install zod@^3
62
+ ```
63
+
64
+ **Requirements:** Node `>=18.0.0`
65
+
66
+ ---
67
+
68
+ ## Quick Start
69
+
70
+ ```typescript
71
+ import * as z from 'zod';
72
+ import { MongoClient } from 'mongodb';
73
+ import { defineCollection, createRepository } from '@wenu/mongo';
74
+
75
+ // 1. Define the schema and collection
76
+ const UserCollection = defineCollection({
77
+ name: 'users',
78
+ schema: z.object({
79
+ name: z.string().min(1),
80
+ email: z.string().email(),
81
+ createdAt: z.date(),
82
+ }),
83
+ });
84
+
85
+ // 2. Connect and create the repository
86
+ const client = await MongoClient.connect('mongodb://localhost:27017');
87
+ const db = client.db('myapp');
88
+
89
+ const users = createRepository(UserCollection, db);
90
+
91
+ // 3. Use it — no throws, ever
92
+ const result = await users.insert({
93
+ name: 'Alice',
94
+ email: 'alice@example.com',
95
+ createdAt: new Date(),
96
+ });
97
+
98
+ if (result.ok) {
99
+ console.log(result.value._id); // ObjectId (inferred from id: 'objectid')
100
+ } else {
101
+ console.error(result.error.kind, result.error.message);
102
+ }
103
+ ```
104
+
105
+ ---
106
+
107
+ ## Core Concepts
108
+
109
+ ### Defining a Collection
110
+
111
+ `defineCollection()` is the single source of truth for a collection's shape, ID strategy, and
112
+ indexes. It returns a frozen `CollectionDef` object you pass to `createRepository()` and index
113
+ utilities.
114
+
115
+ ```typescript
116
+ import * as z from 'zod';
117
+ import { defineCollection, index } from '@wenu/mongo';
118
+
119
+ const ProductCollection = defineCollection({
120
+ name: 'products',
121
+ schema: z.object({
122
+ sku: z.string(),
123
+ name: z.string(),
124
+ price: z.number().positive(),
125
+ tags: z.array(z.string()).default([]),
126
+ }),
127
+ id: 'uuid', // optional — defaults to 'objectid'
128
+ indexes: [index({ sku: 1 }, { unique: true }), index({ tags: 1 })],
129
+ });
130
+ ```
131
+
132
+ The `Doc<Schema, Id>` type resolves to the schema output merged with the inferred `_id` type:
133
+
134
+ ```typescript
135
+ import type { Doc } from '@wenu/mongo';
136
+
137
+ type Product = Doc<(typeof ProductCollection)['schema'], (typeof ProductCollection)['id']>;
138
+ // { _id: string; sku: string; name: string; price: number; tags: string[] }
139
+ ```
140
+
141
+ ### Creating a Repository
142
+
143
+ Pass the collection definition and a `Db` instance. The repository is a plain object — no classes,
144
+ no state beyond the driver handle.
145
+
146
+ ```typescript
147
+ import { createRepository } from '@wenu/mongo';
148
+
149
+ const products = createRepository(ProductCollection, db);
150
+ ```
151
+
152
+ All 12 methods return `Promise<Result<T, DbError>>`.
153
+
154
+ ### Result Type
155
+
156
+ The `Result` type is a discriminated union — it never throws and forces you to handle both paths:
157
+
158
+ ```typescript
159
+ type Ok<T> = { readonly ok: true; readonly value: T };
160
+ type Err<E> = { readonly ok: false; readonly error: E };
161
+ type Result<T, E = DbError> = Ok<T> | Err<E>;
162
+ ```
163
+
164
+ Use the `ok` discriminant or the `isOk` / `isErr` type guards:
165
+
166
+ ```typescript
167
+ import { isOk, isErr } from '@wenu/mongo';
168
+
169
+ const result = await users.findById(id);
170
+
171
+ // Discriminant
172
+ if (result.ok) {
173
+ // result.value is Doc<...> | null
174
+ }
175
+
176
+ // Type guards
177
+ if (isOk(result)) {
178
+ console.log(result.value);
179
+ }
180
+ if (isErr(result)) {
181
+ console.error(result.error);
182
+ }
183
+ ```
184
+
185
+ ---
186
+
187
+ ## CRUD Operations
188
+
189
+ All examples use the `users` repository from the Quick Start.
190
+
191
+ ### Find
192
+
193
+ ```typescript
194
+ // By ID
195
+ const byId = await users.findById(someObjectId);
196
+
197
+ // By filter — returns first match or null
198
+ const byEmail = await users.findOne({ email: 'alice@example.com' });
199
+
200
+ // All matching documents
201
+ const all = await users.find({ name: /alice/i });
202
+
203
+ // All documents (no filter)
204
+ const everyone = await users.find();
205
+ ```
206
+
207
+ ### Insert
208
+
209
+ ```typescript
210
+ // Single document — validated before insert
211
+ const inserted = await users.insert({
212
+ name: 'Bob',
213
+ email: 'bob@example.com',
214
+ createdAt: new Date(),
215
+ });
216
+
217
+ // Multiple documents — validates all first; any failure returns an error before any DB write
218
+ const many = await users.insertMany([
219
+ { name: 'Carol', email: 'carol@example.com', createdAt: new Date() },
220
+ { name: 'Dave', email: 'dave@example.com', createdAt: new Date() },
221
+ ]);
222
+ ```
223
+
224
+ ### Update
225
+
226
+ ```typescript
227
+ // By ID — returns updated document or null if not found
228
+ const updated = await users.updateById(someObjectId, { name: 'Alice Smith' });
229
+
230
+ // By filter — returns updated document or null
231
+ const updatedOne = await users.updateOne({ email: 'alice@example.com' }, { name: 'Alice Smith' });
232
+
233
+ // Bulk update — returns { modifiedCount }
234
+ const bulk = await users.updateMany(
235
+ { createdAt: { $lt: new Date('2024-01-01') } },
236
+ { name: 'archived' },
237
+ );
238
+ if (bulk.ok) {
239
+ console.log(`Updated ${bulk.value.modifiedCount} documents`);
240
+ }
241
+ ```
242
+
243
+ ### Delete
244
+
245
+ ```typescript
246
+ // By ID — returns deleted document or null
247
+ const deleted = await users.deleteById(someObjectId);
248
+
249
+ // By filter — returns deleted document or null
250
+ const deletedOne = await users.deleteOne({ email: 'bob@example.com' });
251
+
252
+ // Bulk delete — returns { deletedCount }
253
+ const bulk = await users.deleteMany({ createdAt: { $lt: cutoff } });
254
+ if (bulk.ok) {
255
+ console.log(`Deleted ${bulk.value.deletedCount} documents`);
256
+ }
257
+ ```
258
+
259
+ ---
260
+
261
+ ## Error Handling
262
+
263
+ `DbError` carries a discriminated `kind` field so you can branch on error type without inspecting
264
+ message strings:
265
+
266
+ ```typescript
267
+ import type { DbError, DbErrorKind } from '@wenu/mongo';
268
+
269
+ type DbErrorKind =
270
+ | 'validation' // Zod parse failed (insert/update input) — no DB call was made
271
+ | 'duplicate-key' // MongoDB error code 11000 (unique index violation)
272
+ | 'not-found' // reserved for future use
273
+ | 'connection' // reserved for future use
274
+ | 'unknown'; // any other driver error
275
+ ```
276
+
277
+ ```typescript
278
+ const result = await products.insert({ sku: 'existing-sku', name: 'X', price: 10, tags: [] });
279
+
280
+ if (!result.ok) {
281
+ switch (result.error.kind) {
282
+ case 'duplicate-key':
283
+ throw new ConflictError('SKU already exists');
284
+ case 'validation':
285
+ throw new BadRequestError(result.error.message);
286
+ default:
287
+ throw new InternalError(result.error.message);
288
+ }
289
+ }
290
+ ```
291
+
292
+ You can also convert any caught value into a `DbError` using `toDbError`:
293
+
294
+ ```typescript
295
+ import { toDbError } from '@wenu/mongo';
296
+
297
+ const dbError = toDbError(caughtError);
298
+ ```
299
+
300
+ ---
301
+
302
+ ## ID Strategies
303
+
304
+ Set `id` in `defineCollection()`. The `_id` type on every document is inferred automatically.
305
+
306
+ ### `objectid` (default)
307
+
308
+ Generated by the library using `new ObjectId()`. No `_id` required in input data.
309
+
310
+ ```typescript
311
+ const Collection = defineCollection({ name: 'items', schema, id: 'objectid' });
312
+ // Doc._id: ObjectId
313
+ ```
314
+
315
+ ### `uuid`
316
+
317
+ Generated using `crypto.randomUUID()`. No `_id` required in input data.
318
+
319
+ ```typescript
320
+ const Collection = defineCollection({ name: 'items', schema, id: 'uuid' });
321
+ // Doc._id: string (UUID v4)
322
+ ```
323
+
324
+ ### `string`
325
+
326
+ Caller-supplied string. The schema must include `_id: z.string()`.
327
+
328
+ ```typescript
329
+ const Collection = defineCollection({
330
+ name: 'items',
331
+ schema: z.object({ _id: z.string(), name: z.string() }),
332
+ id: 'string',
333
+ });
334
+ // Doc._id: string (from data)
335
+ ```
336
+
337
+ ### Custom Zod schema
338
+
339
+ Pass any Zod schema as the `id` value. The `_id` type is inferred from the schema's output. The
340
+ caller is responsible for including `_id` in the input data.
341
+
342
+ ```typescript
343
+ import * as z from 'zod';
344
+
345
+ const OrderId = z.string().brand<'OrderId'>();
346
+
347
+ const OrderCollection = defineCollection({
348
+ name: 'orders',
349
+ schema: z.object({ _id: OrderId, total: z.number() }),
350
+ id: OrderId,
351
+ });
352
+ // Doc._id: string & Brand<'OrderId'>
353
+ ```
354
+
355
+ ---
356
+
357
+ ## Index Management
358
+
359
+ Declare indexes inside `defineCollection()` using the `index()` helper, then either sync them at
360
+ startup or generate a migrate-mongo migration file.
361
+
362
+ ### Declaring indexes
363
+
364
+ ```typescript
365
+ import { defineCollection, index } from '@wenu/mongo';
366
+
367
+ const ArticleCollection = defineCollection({
368
+ name: 'articles',
369
+ schema: z.object({
370
+ slug: z.string(),
371
+ authorId: z.string(),
372
+ publishedAt: z.date().optional(),
373
+ tags: z.array(z.string()).default([]),
374
+ }),
375
+ indexes: [
376
+ index({ slug: 1 }, { unique: true }),
377
+ index({ authorId: 1, publishedAt: -1 }),
378
+ index({ tags: 1 }),
379
+ ],
380
+ });
381
+ ```
382
+
383
+ ### Syncing at startup
384
+
385
+ `syncIndexes()` calls `createIndexes()` on the underlying collection and returns `Result<void>`.
386
+ Safe to call on every startup — MongoDB skips indexes that already exist.
387
+
388
+ ```typescript
389
+ import { syncIndexes } from '@wenu/mongo';
390
+
391
+ const result = await syncIndexes(ArticleCollection, db);
392
+ if (!result.ok) {
393
+ console.error('Index sync failed:', result.error.message);
394
+ }
395
+ ```
396
+
397
+ ### Generating a migrate-mongo script
398
+
399
+ ```typescript
400
+ import { generateIndexMigration } from '@wenu/mongo';
401
+ import fs from 'node:fs';
402
+
403
+ const migration = generateIndexMigration(ArticleCollection);
404
+ fs.writeFileSync('migrations/20240101-articles-indexes.js', migration);
405
+ ```
406
+
407
+ The generated file exports `up(db)` / `down(db)` compatible with
408
+ [migrate-mongo](https://github.com/seppevs/migrate-mongo).
409
+
410
+ ---
411
+
412
+ ## Aggregation
413
+
414
+ Pass the pipeline and an output schema. Results are parsed with the provided schema and returned as
415
+ `Result<Infer<Out>[]>`.
416
+
417
+ ```typescript
418
+ import * as z from 'zod';
419
+
420
+ const SummarySchema = z.object({
421
+ authorId: z.string(),
422
+ articleCount: z.number(),
423
+ latestPublishedAt: z.date().nullable(),
424
+ });
425
+
426
+ const result = await articles.aggregate(
427
+ [
428
+ { $match: { publishedAt: { $exists: true } } },
429
+ {
430
+ $group: {
431
+ _id: '$authorId',
432
+ articleCount: { $sum: 1 },
433
+ latestPublishedAt: { $max: '$publishedAt' },
434
+ },
435
+ },
436
+ { $project: { authorId: '$_id', articleCount: 1, latestPublishedAt: 1, _id: 0 } },
437
+ ],
438
+ SummarySchema,
439
+ );
440
+
441
+ if (result.ok) {
442
+ for (const summary of result.value) {
443
+ console.log(summary.authorId, summary.articleCount);
444
+ }
445
+ }
446
+ ```
447
+
448
+ ---
449
+
450
+ ## Compatibility
451
+
452
+ | Feature | Supported |
453
+ | ---------------- | ------------------------------------------- |
454
+ | Zod 3 | Yes |
455
+ | Zod 4 | Yes (same `ZodCompat` API surface) |
456
+ | MongoDB driver 5 | Yes (shim normalizes `ModifyResult.value`) |
457
+ | MongoDB driver 6 | Yes (direct return from `findOneAndUpdate`) |
458
+ | MongoDB driver 7 | Yes |
459
+ | Node.js | `>=18.0.0` |
460
+ | ESM | Yes |
461
+ | CJS | Yes |
462
+
463
+ ---
464
+
465
+ ## API Reference
466
+
467
+ ### `defineCollection(config)`
468
+
469
+ Creates an immutable `CollectionDef` descriptor.
470
+
471
+ | Parameter | Type | Default | Description |
472
+ | ---------------- | ------------ | ------------ | ----------------------------------- |
473
+ | `config.name` | `string` | — | MongoDB collection name |
474
+ | `config.schema` | `ZodCompat` | — | Zod schema for the document body |
475
+ | `config.id` | `IdStrategy` | `'objectid'` | ID generation or inference strategy |
476
+ | `config.indexes` | `IndexDef[]` | `[]` | Index definitions |
477
+
478
+ ### `createRepository(collection, db)`
479
+
480
+ Returns a `Repository<Schema, Id>` bound to the collection definition and database.
481
+
482
+ ### `Repository<Schema, Id>` methods
483
+
484
+ | Method | Returns |
485
+ | ----------------------------------- | -------------------------------------------- |
486
+ | `findById(id)` | `Promise<Result<Doc \| null>>` |
487
+ | `findOne(filter)` | `Promise<Result<Doc \| null>>` |
488
+ | `find(filter?)` | `Promise<Result<Doc[]>>` |
489
+ | `insert(data)` | `Promise<Result<Doc>>` |
490
+ | `insertMany(data)` | `Promise<Result<Doc[]>>` |
491
+ | `updateById(id, patch)` | `Promise<Result<Doc \| null>>` |
492
+ | `updateOne(filter, patch)` | `Promise<Result<Doc \| null>>` |
493
+ | `updateMany(filter, patch)` | `Promise<Result<{ modifiedCount: number }>>` |
494
+ | `deleteById(id)` | `Promise<Result<Doc \| null>>` |
495
+ | `deleteOne(filter)` | `Promise<Result<Doc \| null>>` |
496
+ | `deleteMany(filter?)` | `Promise<Result<{ deletedCount: number }>>` |
497
+ | `aggregate(pipeline, outputSchema)` | `Promise<Result<Infer<Out>[]>>` |
498
+
499
+ ### `index(spec, options?)`
500
+
501
+ Creates an `IndexDef`. `spec` follows the MongoDB index key format (`{ field: 1 }`, `{ field: -1 }`,
502
+ etc.).
503
+
504
+ ### `syncIndexes(collection, db)`
505
+
506
+ Syncs all declared indexes to MongoDB. Returns `Promise<Result<void>>`. Idempotent.
507
+
508
+ ### `generateIndexMigration(collection)`
509
+
510
+ Returns a migrate-mongo compatible JS migration string (`up` / `down`) for the collection's indexes.
511
+
512
+ ### Result helpers
513
+
514
+ | Export | Signature | Description |
515
+ | ----------- | ---------------------------------------- | --------------------------------- |
516
+ | `ok` | `<T>(value: T) => Ok<T>` | Construct a success result |
517
+ | `err` | `<E>(error: E) => Err<E>` | Construct an error result |
518
+ | `isOk` | `<T, E>(r: Result<T, E>) => r is Ok<T>` | Type guard for success |
519
+ | `isErr` | `<T, E>(r: Result<T, E>) => r is Err<E>` | Type guard for error |
520
+ | `toDbError` | `(e: unknown) => DbError` | Map any thrown value to `DbError` |
521
+
522
+ ### ID Strategies at a glance
523
+
524
+ | Strategy | `_id` type | Auto-generated |
525
+ | ---------------------- | --------------- | --------------------------- |
526
+ | `'objectid'` (default) | `ObjectId` | Yes — `new ObjectId()` |
527
+ | `'uuid'` | `string` | Yes — `crypto.randomUUID()` |
528
+ | `'string'` | `string` | No — embed `_id` in data |
529
+ | Any Zod schema | `Infer<Schema>` | No — embed `_id` in data |
530
+
531
+ ---
532
+
533
+ ## License
534
+
535
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,219 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let radashi = require("radashi");
3
+ let zod = require("zod");
4
+ let node_crypto = require("node:crypto");
5
+ let mongodb = require("mongodb");
6
+ //#region src/errors.ts
7
+ const MONGO_DUPLICATE_KEY_CODE = 11e3;
8
+ const toDbError = (error) => {
9
+ if (error instanceof zod.ZodError) return {
10
+ kind: "validation",
11
+ message: (0, radashi.getErrorMessage)(error),
12
+ cause: error
13
+ };
14
+ if ((0, radashi.isError)(error) && "code" in error && error.code === MONGO_DUPLICATE_KEY_CODE) return {
15
+ kind: "duplicate-key",
16
+ message: (0, radashi.getErrorMessage)(error),
17
+ cause: error
18
+ };
19
+ if ((0, radashi.isError)(error)) return {
20
+ kind: "unknown",
21
+ message: (0, radashi.getErrorMessage)(error),
22
+ cause: error
23
+ };
24
+ return {
25
+ kind: "unknown",
26
+ message: String(error)
27
+ };
28
+ };
29
+ //#endregion
30
+ //#region src/result.ts
31
+ const ok = (value) => ({
32
+ ok: true,
33
+ value
34
+ });
35
+ const err = (error) => ({
36
+ ok: false,
37
+ error
38
+ });
39
+ const isOk = (r) => r.ok;
40
+ const isErr = (r) => !r.ok;
41
+ //#endregion
42
+ //#region src/indexes.ts
43
+ const index = (spec, options) => ({
44
+ spec,
45
+ options
46
+ });
47
+ const syncIndexes = async (collection, database) => {
48
+ if ((0, radashi.isEmpty)(collection.indexes)) return ok(void 0);
49
+ const [error] = await (0, radashi.tryit)(() => database.collection(collection.name).createIndexes(collection.indexes.map((indexEntry) => ({
50
+ key: indexEntry.spec,
51
+ ...indexEntry.options
52
+ }))))();
53
+ return (0, radashi.isNullish)(error) ? ok(void 0) : err(toDbError(error));
54
+ };
55
+ const emptyMigration = () => radashi.dedent`
56
+ 'use strict'
57
+
58
+ module.exports = {
59
+ async up(_db) {},
60
+ async down(_db) {},
61
+ }
62
+ `;
63
+ const indexMigration = (name, specs, names) => radashi.dedent`
64
+ 'use strict'
65
+
66
+ module.exports = {
67
+ async up(db) {
68
+ await db.collection('${name}').createIndexes(${specs})
69
+ },
70
+ async down(db) {
71
+ await db.collection('${name}').dropIndexes(${names})
72
+ },
73
+ }
74
+ `;
75
+ const generateIndexMigration = (collection) => {
76
+ if ((0, radashi.isEmpty)(collection.indexes)) return emptyMigration();
77
+ const specs = JSON.stringify(collection.indexes.map((indexEntry) => ({
78
+ key: indexEntry.spec,
79
+ ...indexEntry.options
80
+ })), null, 2);
81
+ const names = JSON.stringify(collection.indexes.map((indexEntry) => {
82
+ if (indexEntry.options?.name) return indexEntry.options.name;
83
+ return Object.keys(indexEntry.spec).map((key) => `${key}_${String(indexEntry.spec[key])}`).join("_");
84
+ }));
85
+ return indexMigration(collection.name, specs, names);
86
+ };
87
+ //#endregion
88
+ //#region src/collection.ts
89
+ const defineCollection = (config) => Object.freeze({
90
+ name: config.name,
91
+ schema: config.schema,
92
+ id: config.id ?? "objectid",
93
+ indexes: Object.freeze(config.indexes ?? [])
94
+ });
95
+ //#endregion
96
+ //#region src/compat/driver.ts
97
+ const MONGO_MAJOR = (() => {
98
+ const version = require("mongodb/package.json").version;
99
+ const match = /^(\d+)/.exec(version);
100
+ return match ? Number(match[1]) : 6;
101
+ })();
102
+ const extractResult = (raw, isV5) => {
103
+ if (isV5) return raw?.value ?? null;
104
+ return raw ?? null;
105
+ };
106
+ const findOneAndModify = async (collection, filter, op) => {
107
+ const isV5 = MONGO_MAJOR <= 5;
108
+ if (op.kind === "delete") return extractResult(await collection.findOneAndDelete(filter, op.options ?? {}), isV5);
109
+ return extractResult(await collection.findOneAndUpdate(filter, op.update, {
110
+ returnDocument: "after",
111
+ ...op.options
112
+ }), isV5);
113
+ };
114
+ //#endregion
115
+ //#region src/id.ts
116
+ const generateId = (strategy) => {
117
+ if (strategy === "objectid") return new mongodb.ObjectId();
118
+ return (0, node_crypto.randomUUID)();
119
+ };
120
+ //#endregion
121
+ //#region src/repository.ts
122
+ const runSafe = async (operation) => {
123
+ const [error, value] = await (0, radashi.tryit)(operation)();
124
+ return (0, radashi.isNullish)(error) ? ok(value) : err(toDbError(error));
125
+ };
126
+ const createRepository = (collection, database) => {
127
+ const coll = database.collection(collection.name);
128
+ const schema = collection.schema;
129
+ const idStrategy = collection.id;
130
+ const parseSchema = (data) => {
131
+ try {
132
+ return ok(schema.parse(data));
133
+ } catch (error) {
134
+ return err(toDbError(error));
135
+ }
136
+ };
137
+ const parsePartialSchema = (data) => {
138
+ try {
139
+ return ok(("partial" in schema && typeof schema.partial === "function" ? schema.partial() : schema).parse(data));
140
+ } catch (error) {
141
+ return err(toDbError(error));
142
+ }
143
+ };
144
+ const buildDoc = (validated) => {
145
+ if (idStrategy === "objectid" || idStrategy === "uuid") {
146
+ const id = generateId(idStrategy);
147
+ return {
148
+ ...validated,
149
+ _id: id
150
+ };
151
+ }
152
+ return { ...validated };
153
+ };
154
+ return {
155
+ findById: (id) => runSafe(() => coll.findOne({ _id: id }).then((found) => (0, radashi.isNullish)(found) ? null : found)),
156
+ findOne: (filter) => runSafe(() => coll.findOne(filter).then((found) => (0, radashi.isNullish)(found) ? null : found)),
157
+ find: (filter) => runSafe(() => {
158
+ return coll.find(filter ?? {}).toArray().then((records) => records);
159
+ }),
160
+ insert: async (data) => {
161
+ const parsed = parseSchema(data);
162
+ if (!parsed.ok) return parsed;
163
+ return runSafe(async () => {
164
+ const record = buildDoc(parsed.value);
165
+ await coll.insertOne(record);
166
+ return record;
167
+ });
168
+ },
169
+ insertMany: async (data) => {
170
+ const parsedItems = [];
171
+ for (const item of data) {
172
+ const result = parseSchema(item);
173
+ if (!result.ok) return result;
174
+ parsedItems.push(result.value);
175
+ }
176
+ return runSafe(async () => {
177
+ const records = parsedItems.map((item) => buildDoc(item));
178
+ await coll.insertMany(records);
179
+ return records;
180
+ });
181
+ },
182
+ updateById: async (id, patch) => {
183
+ const parsed = parsePartialSchema(patch);
184
+ if (!parsed.ok) return parsed;
185
+ return runSafe(() => findOneAndModify(coll, { _id: id }, {
186
+ kind: "update",
187
+ update: { $set: (0, radashi.shake)(parsed.value) }
188
+ }).then((found) => (0, radashi.isNullish)(found) ? null : found));
189
+ },
190
+ updateOne: async (filter, patch) => {
191
+ const parsed = parsePartialSchema(patch);
192
+ if (!parsed.ok) return parsed;
193
+ return runSafe(() => findOneAndModify(coll, filter, {
194
+ kind: "update",
195
+ update: { $set: (0, radashi.shake)(parsed.value) }
196
+ }).then((found) => (0, radashi.isNullish)(found) ? null : found));
197
+ },
198
+ updateMany: async (filter, patch) => {
199
+ const parsed = parsePartialSchema(patch);
200
+ if (!parsed.ok) return parsed;
201
+ return runSafe(() => coll.updateMany(filter, { $set: (0, radashi.shake)(parsed.value) }).then((result) => ({ modifiedCount: result.modifiedCount })));
202
+ },
203
+ deleteById: (id) => runSafe(() => findOneAndModify(coll, { _id: id }, { kind: "delete" }).then((found) => (0, radashi.isNullish)(found) ? null : found)),
204
+ deleteOne: (filter) => runSafe(() => findOneAndModify(coll, filter, { kind: "delete" }).then((found) => (0, radashi.isNullish)(found) ? null : found)),
205
+ deleteMany: (filter) => runSafe(() => coll.deleteMany(filter).then((result) => ({ deletedCount: result.deletedCount }))),
206
+ aggregate: (pipeline, outputSchema) => runSafe(() => coll.aggregate(pipeline).toArray().then((records) => records.map((r) => outputSchema.parse(r))))
207
+ };
208
+ };
209
+ //#endregion
210
+ exports.createRepository = createRepository;
211
+ exports.defineCollection = defineCollection;
212
+ exports.err = err;
213
+ exports.generateIndexMigration = generateIndexMigration;
214
+ exports.index = index;
215
+ exports.isErr = isErr;
216
+ exports.isOk = isOk;
217
+ exports.ok = ok;
218
+ exports.syncIndexes = syncIndexes;
219
+ exports.toDbError = toDbError;
@@ -0,0 +1,88 @@
1
+ import { CreateIndexesOptions, Db, Document, Filter, IndexDescription, ObjectId } from "mongodb";
2
+
3
+ //#region src/errors.d.ts
4
+ type DbErrorKind = 'validation' | 'not-found' | 'duplicate-key' | 'connection' | 'unknown';
5
+ type DbError = {
6
+ readonly kind: DbErrorKind;
7
+ readonly message: string;
8
+ readonly cause?: unknown;
9
+ };
10
+ declare const toDbError: (error: unknown) => DbError;
11
+ //#endregion
12
+ //#region src/result.d.ts
13
+ type Ok<T> = {
14
+ readonly ok: true;
15
+ readonly value: T;
16
+ };
17
+ type Err<E> = {
18
+ readonly ok: false;
19
+ readonly error: E;
20
+ };
21
+ type Result<T, E = DbError> = Ok<T> | Err<E>;
22
+ declare const ok: <T>(value: T) => Ok<T>;
23
+ declare const err: <E>(error: E) => Err<E>;
24
+ declare const isOk: <T, E>(r: Result<T, E>) => r is Ok<T>;
25
+ declare const isErr: <T, E>(r: Result<T, E>) => r is Err<E>;
26
+ //#endregion
27
+ //#region src/compat/zod.d.ts
28
+ type ZodCompat = {
29
+ readonly _output: unknown;
30
+ parse(data: unknown): unknown;
31
+ };
32
+ type Infer<T extends ZodCompat> = T['_output'];
33
+ //#endregion
34
+ //#region src/id.d.ts
35
+ type IdStrategy = 'objectid' | 'uuid' | 'string' | ZodCompat;
36
+ type InferIdType<T extends IdStrategy> = T extends 'objectid' ? ObjectId : T extends 'uuid' ? string : T extends 'string' ? string : T extends ZodCompat ? Infer<T> : never;
37
+ //#endregion
38
+ //#region src/collection.d.ts
39
+ type Doc<Schema extends ZodCompat, Id extends IdStrategy> = Infer<Schema> & {
40
+ readonly _id: InferIdType<Id>;
41
+ };
42
+ type CollectionDef<Schema extends ZodCompat, Id extends IdStrategy> = {
43
+ readonly name: string;
44
+ readonly schema: Schema;
45
+ readonly id: Id;
46
+ readonly indexes: readonly IndexDef[];
47
+ readonly __doc?: Doc<Schema, Id>;
48
+ };
49
+ declare const defineCollection: <Schema extends ZodCompat, Id extends IdStrategy = "objectid">(config: {
50
+ name: string;
51
+ schema: Schema;
52
+ id?: Id;
53
+ indexes?: IndexDef[];
54
+ }) => CollectionDef<Schema, Id>;
55
+ //#endregion
56
+ //#region src/indexes.d.ts
57
+ type IndexSpec = IndexDescription['key'];
58
+ type IndexDef = {
59
+ readonly spec: IndexSpec;
60
+ readonly options?: CreateIndexesOptions;
61
+ };
62
+ declare const index: (spec: IndexSpec, options?: CreateIndexesOptions) => IndexDef;
63
+ declare const syncIndexes: (collection: CollectionDef<ZodCompat, IdStrategy>, database: Db) => Promise<Result<void>>;
64
+ declare const generateIndexMigration: (collection: CollectionDef<ZodCompat, IdStrategy>) => string;
65
+ //#endregion
66
+ //#region src/repository.d.ts
67
+ type Repository<Schema extends ZodCompat, Id extends IdStrategy> = {
68
+ findById(id: InferIdType<Id>): Promise<Result<Doc<Schema, Id> | null>>;
69
+ findOne(filter: Filter<Doc<Schema, Id>>): Promise<Result<Doc<Schema, Id> | null>>;
70
+ find(filter?: Filter<Doc<Schema, Id>>): Promise<Result<Doc<Schema, Id>[]>>;
71
+ insert(data: Infer<Schema>): Promise<Result<Doc<Schema, Id>>>;
72
+ insertMany(data: Infer<Schema>[]): Promise<Result<Doc<Schema, Id>[]>>;
73
+ updateById(id: InferIdType<Id>, patch: Partial<Infer<Schema>>): Promise<Result<Doc<Schema, Id> | null>>;
74
+ updateOne(filter: Filter<Doc<Schema, Id>>, patch: Partial<Infer<Schema>>): Promise<Result<Doc<Schema, Id> | null>>;
75
+ updateMany(filter: Filter<Doc<Schema, Id>>, patch: Partial<Infer<Schema>>): Promise<Result<{
76
+ modifiedCount: number;
77
+ }>>;
78
+ deleteById(id: InferIdType<Id>): Promise<Result<Doc<Schema, Id> | null>>;
79
+ deleteOne(filter: Filter<Doc<Schema, Id>>): Promise<Result<Doc<Schema, Id> | null>>;
80
+ deleteMany(filter: Filter<Doc<Schema, Id>>): Promise<Result<{
81
+ deletedCount: number;
82
+ }>>;
83
+ aggregate<Out extends ZodCompat>(pipeline: Document[], outputSchema: Out): Promise<Result<Infer<Out>[]>>;
84
+ };
85
+ declare const createRepository: <Schema extends ZodCompat, Id extends IdStrategy>(collection: CollectionDef<Schema, Id>, database: Db) => Repository<Schema, Id>;
86
+ //#endregion
87
+ export { type CollectionDef, type DbError, type DbErrorKind, type Doc, type Err, type IdStrategy, type IndexDef, type Infer, type InferIdType, type Ok, type Repository, type Result, type ZodCompat, createRepository, defineCollection, err, generateIndexMigration, index, isErr, isOk, ok, syncIndexes, toDbError };
88
+ //# sourceMappingURL=index.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../src/errors.ts","../src/result.ts","../src/compat/zod.ts","../src/id.ts","../src/collection.ts","../src/indexes.ts","../src/repository.ts"],"mappings":"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"}
@@ -0,0 +1,88 @@
1
+ import { CreateIndexesOptions, Db, Document, Filter, IndexDescription, ObjectId } from "mongodb";
2
+
3
+ //#region src/errors.d.ts
4
+ type DbErrorKind = 'validation' | 'not-found' | 'duplicate-key' | 'connection' | 'unknown';
5
+ type DbError = {
6
+ readonly kind: DbErrorKind;
7
+ readonly message: string;
8
+ readonly cause?: unknown;
9
+ };
10
+ declare const toDbError: (error: unknown) => DbError;
11
+ //#endregion
12
+ //#region src/result.d.ts
13
+ type Ok<T> = {
14
+ readonly ok: true;
15
+ readonly value: T;
16
+ };
17
+ type Err<E> = {
18
+ readonly ok: false;
19
+ readonly error: E;
20
+ };
21
+ type Result<T, E = DbError> = Ok<T> | Err<E>;
22
+ declare const ok: <T>(value: T) => Ok<T>;
23
+ declare const err: <E>(error: E) => Err<E>;
24
+ declare const isOk: <T, E>(r: Result<T, E>) => r is Ok<T>;
25
+ declare const isErr: <T, E>(r: Result<T, E>) => r is Err<E>;
26
+ //#endregion
27
+ //#region src/compat/zod.d.ts
28
+ type ZodCompat = {
29
+ readonly _output: unknown;
30
+ parse(data: unknown): unknown;
31
+ };
32
+ type Infer<T extends ZodCompat> = T['_output'];
33
+ //#endregion
34
+ //#region src/id.d.ts
35
+ type IdStrategy = 'objectid' | 'uuid' | 'string' | ZodCompat;
36
+ type InferIdType<T extends IdStrategy> = T extends 'objectid' ? ObjectId : T extends 'uuid' ? string : T extends 'string' ? string : T extends ZodCompat ? Infer<T> : never;
37
+ //#endregion
38
+ //#region src/collection.d.ts
39
+ type Doc<Schema extends ZodCompat, Id extends IdStrategy> = Infer<Schema> & {
40
+ readonly _id: InferIdType<Id>;
41
+ };
42
+ type CollectionDef<Schema extends ZodCompat, Id extends IdStrategy> = {
43
+ readonly name: string;
44
+ readonly schema: Schema;
45
+ readonly id: Id;
46
+ readonly indexes: readonly IndexDef[];
47
+ readonly __doc?: Doc<Schema, Id>;
48
+ };
49
+ declare const defineCollection: <Schema extends ZodCompat, Id extends IdStrategy = "objectid">(config: {
50
+ name: string;
51
+ schema: Schema;
52
+ id?: Id;
53
+ indexes?: IndexDef[];
54
+ }) => CollectionDef<Schema, Id>;
55
+ //#endregion
56
+ //#region src/indexes.d.ts
57
+ type IndexSpec = IndexDescription['key'];
58
+ type IndexDef = {
59
+ readonly spec: IndexSpec;
60
+ readonly options?: CreateIndexesOptions;
61
+ };
62
+ declare const index: (spec: IndexSpec, options?: CreateIndexesOptions) => IndexDef;
63
+ declare const syncIndexes: (collection: CollectionDef<ZodCompat, IdStrategy>, database: Db) => Promise<Result<void>>;
64
+ declare const generateIndexMigration: (collection: CollectionDef<ZodCompat, IdStrategy>) => string;
65
+ //#endregion
66
+ //#region src/repository.d.ts
67
+ type Repository<Schema extends ZodCompat, Id extends IdStrategy> = {
68
+ findById(id: InferIdType<Id>): Promise<Result<Doc<Schema, Id> | null>>;
69
+ findOne(filter: Filter<Doc<Schema, Id>>): Promise<Result<Doc<Schema, Id> | null>>;
70
+ find(filter?: Filter<Doc<Schema, Id>>): Promise<Result<Doc<Schema, Id>[]>>;
71
+ insert(data: Infer<Schema>): Promise<Result<Doc<Schema, Id>>>;
72
+ insertMany(data: Infer<Schema>[]): Promise<Result<Doc<Schema, Id>[]>>;
73
+ updateById(id: InferIdType<Id>, patch: Partial<Infer<Schema>>): Promise<Result<Doc<Schema, Id> | null>>;
74
+ updateOne(filter: Filter<Doc<Schema, Id>>, patch: Partial<Infer<Schema>>): Promise<Result<Doc<Schema, Id> | null>>;
75
+ updateMany(filter: Filter<Doc<Schema, Id>>, patch: Partial<Infer<Schema>>): Promise<Result<{
76
+ modifiedCount: number;
77
+ }>>;
78
+ deleteById(id: InferIdType<Id>): Promise<Result<Doc<Schema, Id> | null>>;
79
+ deleteOne(filter: Filter<Doc<Schema, Id>>): Promise<Result<Doc<Schema, Id> | null>>;
80
+ deleteMany(filter: Filter<Doc<Schema, Id>>): Promise<Result<{
81
+ deletedCount: number;
82
+ }>>;
83
+ aggregate<Out extends ZodCompat>(pipeline: Document[], outputSchema: Out): Promise<Result<Infer<Out>[]>>;
84
+ };
85
+ declare const createRepository: <Schema extends ZodCompat, Id extends IdStrategy>(collection: CollectionDef<Schema, Id>, database: Db) => Repository<Schema, Id>;
86
+ //#endregion
87
+ export { type CollectionDef, type DbError, type DbErrorKind, type Doc, type Err, type IdStrategy, type IndexDef, type Infer, type InferIdType, type Ok, type Repository, type Result, type ZodCompat, createRepository, defineCollection, err, generateIndexMigration, index, isErr, isOk, ok, syncIndexes, toDbError };
88
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/errors.ts","../src/result.ts","../src/compat/zod.ts","../src/id.ts","../src/collection.ts","../src/indexes.ts","../src/repository.ts"],"mappings":"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"}
package/dist/index.mjs ADDED
@@ -0,0 +1,215 @@
1
+ import { createRequire } from "node:module";
2
+ import { dedent, getErrorMessage, isEmpty, isError, isNullish, shake, tryit } from "radashi";
3
+ import { ZodError } from "zod";
4
+ import { randomUUID } from "node:crypto";
5
+ import { ObjectId } from "mongodb";
6
+ //#region \0rolldown/runtime.js
7
+ var __require = /* #__PURE__ */ (() => createRequire(import.meta.url))();
8
+ //#endregion
9
+ //#region src/errors.ts
10
+ const MONGO_DUPLICATE_KEY_CODE = 11e3;
11
+ const toDbError = (error) => {
12
+ if (error instanceof ZodError) return {
13
+ kind: "validation",
14
+ message: getErrorMessage(error),
15
+ cause: error
16
+ };
17
+ if (isError(error) && "code" in error && error.code === MONGO_DUPLICATE_KEY_CODE) return {
18
+ kind: "duplicate-key",
19
+ message: getErrorMessage(error),
20
+ cause: error
21
+ };
22
+ if (isError(error)) return {
23
+ kind: "unknown",
24
+ message: getErrorMessage(error),
25
+ cause: error
26
+ };
27
+ return {
28
+ kind: "unknown",
29
+ message: String(error)
30
+ };
31
+ };
32
+ //#endregion
33
+ //#region src/result.ts
34
+ const ok = (value) => ({
35
+ ok: true,
36
+ value
37
+ });
38
+ const err = (error) => ({
39
+ ok: false,
40
+ error
41
+ });
42
+ const isOk = (r) => r.ok;
43
+ const isErr = (r) => !r.ok;
44
+ //#endregion
45
+ //#region src/indexes.ts
46
+ const index = (spec, options) => ({
47
+ spec,
48
+ options
49
+ });
50
+ const syncIndexes = async (collection, database) => {
51
+ if (isEmpty(collection.indexes)) return ok(void 0);
52
+ const [error] = await tryit(() => database.collection(collection.name).createIndexes(collection.indexes.map((indexEntry) => ({
53
+ key: indexEntry.spec,
54
+ ...indexEntry.options
55
+ }))))();
56
+ return isNullish(error) ? ok(void 0) : err(toDbError(error));
57
+ };
58
+ const emptyMigration = () => dedent`
59
+ 'use strict'
60
+
61
+ module.exports = {
62
+ async up(_db) {},
63
+ async down(_db) {},
64
+ }
65
+ `;
66
+ const indexMigration = (name, specs, names) => dedent`
67
+ 'use strict'
68
+
69
+ module.exports = {
70
+ async up(db) {
71
+ await db.collection('${name}').createIndexes(${specs})
72
+ },
73
+ async down(db) {
74
+ await db.collection('${name}').dropIndexes(${names})
75
+ },
76
+ }
77
+ `;
78
+ const generateIndexMigration = (collection) => {
79
+ if (isEmpty(collection.indexes)) return emptyMigration();
80
+ const specs = JSON.stringify(collection.indexes.map((indexEntry) => ({
81
+ key: indexEntry.spec,
82
+ ...indexEntry.options
83
+ })), null, 2);
84
+ const names = JSON.stringify(collection.indexes.map((indexEntry) => {
85
+ if (indexEntry.options?.name) return indexEntry.options.name;
86
+ return Object.keys(indexEntry.spec).map((key) => `${key}_${String(indexEntry.spec[key])}`).join("_");
87
+ }));
88
+ return indexMigration(collection.name, specs, names);
89
+ };
90
+ //#endregion
91
+ //#region src/collection.ts
92
+ const defineCollection = (config) => Object.freeze({
93
+ name: config.name,
94
+ schema: config.schema,
95
+ id: config.id ?? "objectid",
96
+ indexes: Object.freeze(config.indexes ?? [])
97
+ });
98
+ //#endregion
99
+ //#region src/compat/driver.ts
100
+ const MONGO_MAJOR = (() => {
101
+ const version = __require("mongodb/package.json").version;
102
+ const match = /^(\d+)/.exec(version);
103
+ return match ? Number(match[1]) : 6;
104
+ })();
105
+ const extractResult = (raw, isV5) => {
106
+ if (isV5) return raw?.value ?? null;
107
+ return raw ?? null;
108
+ };
109
+ const findOneAndModify = async (collection, filter, op) => {
110
+ const isV5 = MONGO_MAJOR <= 5;
111
+ if (op.kind === "delete") return extractResult(await collection.findOneAndDelete(filter, op.options ?? {}), isV5);
112
+ return extractResult(await collection.findOneAndUpdate(filter, op.update, {
113
+ returnDocument: "after",
114
+ ...op.options
115
+ }), isV5);
116
+ };
117
+ //#endregion
118
+ //#region src/id.ts
119
+ const generateId = (strategy) => {
120
+ if (strategy === "objectid") return new ObjectId();
121
+ return randomUUID();
122
+ };
123
+ //#endregion
124
+ //#region src/repository.ts
125
+ const runSafe = async (operation) => {
126
+ const [error, value] = await tryit(operation)();
127
+ return isNullish(error) ? ok(value) : err(toDbError(error));
128
+ };
129
+ const createRepository = (collection, database) => {
130
+ const coll = database.collection(collection.name);
131
+ const schema = collection.schema;
132
+ const idStrategy = collection.id;
133
+ const parseSchema = (data) => {
134
+ try {
135
+ return ok(schema.parse(data));
136
+ } catch (error) {
137
+ return err(toDbError(error));
138
+ }
139
+ };
140
+ const parsePartialSchema = (data) => {
141
+ try {
142
+ return ok(("partial" in schema && typeof schema.partial === "function" ? schema.partial() : schema).parse(data));
143
+ } catch (error) {
144
+ return err(toDbError(error));
145
+ }
146
+ };
147
+ const buildDoc = (validated) => {
148
+ if (idStrategy === "objectid" || idStrategy === "uuid") {
149
+ const id = generateId(idStrategy);
150
+ return {
151
+ ...validated,
152
+ _id: id
153
+ };
154
+ }
155
+ return { ...validated };
156
+ };
157
+ return {
158
+ findById: (id) => runSafe(() => coll.findOne({ _id: id }).then((found) => isNullish(found) ? null : found)),
159
+ findOne: (filter) => runSafe(() => coll.findOne(filter).then((found) => isNullish(found) ? null : found)),
160
+ find: (filter) => runSafe(() => {
161
+ return coll.find(filter ?? {}).toArray().then((records) => records);
162
+ }),
163
+ insert: async (data) => {
164
+ const parsed = parseSchema(data);
165
+ if (!parsed.ok) return parsed;
166
+ return runSafe(async () => {
167
+ const record = buildDoc(parsed.value);
168
+ await coll.insertOne(record);
169
+ return record;
170
+ });
171
+ },
172
+ insertMany: async (data) => {
173
+ const parsedItems = [];
174
+ for (const item of data) {
175
+ const result = parseSchema(item);
176
+ if (!result.ok) return result;
177
+ parsedItems.push(result.value);
178
+ }
179
+ return runSafe(async () => {
180
+ const records = parsedItems.map((item) => buildDoc(item));
181
+ await coll.insertMany(records);
182
+ return records;
183
+ });
184
+ },
185
+ updateById: async (id, patch) => {
186
+ const parsed = parsePartialSchema(patch);
187
+ if (!parsed.ok) return parsed;
188
+ return runSafe(() => findOneAndModify(coll, { _id: id }, {
189
+ kind: "update",
190
+ update: { $set: shake(parsed.value) }
191
+ }).then((found) => isNullish(found) ? null : found));
192
+ },
193
+ updateOne: async (filter, patch) => {
194
+ const parsed = parsePartialSchema(patch);
195
+ if (!parsed.ok) return parsed;
196
+ return runSafe(() => findOneAndModify(coll, filter, {
197
+ kind: "update",
198
+ update: { $set: shake(parsed.value) }
199
+ }).then((found) => isNullish(found) ? null : found));
200
+ },
201
+ updateMany: async (filter, patch) => {
202
+ const parsed = parsePartialSchema(patch);
203
+ if (!parsed.ok) return parsed;
204
+ return runSafe(() => coll.updateMany(filter, { $set: shake(parsed.value) }).then((result) => ({ modifiedCount: result.modifiedCount })));
205
+ },
206
+ deleteById: (id) => runSafe(() => findOneAndModify(coll, { _id: id }, { kind: "delete" }).then((found) => isNullish(found) ? null : found)),
207
+ deleteOne: (filter) => runSafe(() => findOneAndModify(coll, filter, { kind: "delete" }).then((found) => isNullish(found) ? null : found)),
208
+ deleteMany: (filter) => runSafe(() => coll.deleteMany(filter).then((result) => ({ deletedCount: result.deletedCount }))),
209
+ aggregate: (pipeline, outputSchema) => runSafe(() => coll.aggregate(pipeline).toArray().then((records) => records.map((r) => outputSchema.parse(r))))
210
+ };
211
+ };
212
+ //#endregion
213
+ export { createRepository, defineCollection, err, generateIndexMigration, index, isErr, isOk, ok, syncIndexes, toDbError };
214
+
215
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":["result"],"sources":["../src/errors.ts","../src/result.ts","../src/indexes.ts","../src/collection.ts","../src/compat/driver.ts","../src/id.ts","../src/repository.ts"],"sourcesContent":["import { isError, getErrorMessage } from 'radashi';\nimport { ZodError } from 'zod';\n\nexport type DbErrorKind = 'validation' | 'not-found' | 'duplicate-key' | 'connection' | 'unknown';\n\nexport type DbError = {\n readonly kind: DbErrorKind;\n readonly message: string;\n readonly cause?: unknown;\n};\n\nconst MONGO_DUPLICATE_KEY_CODE = 11_000;\n\nexport const toDbError = (error: unknown): DbError => {\n if (error instanceof ZodError)\n return { kind: 'validation', message: getErrorMessage(error), cause: error };\n if (isError(error) && 'code' in error && error.code === MONGO_DUPLICATE_KEY_CODE)\n return { kind: 'duplicate-key', message: getErrorMessage(error), cause: error };\n if (isError(error)) return { kind: 'unknown', message: getErrorMessage(error), cause: error };\n return { kind: 'unknown', message: String(error) };\n};\n","import type { DbError } from './errors.js';\n\nexport type Ok<T> = { readonly ok: true; readonly value: T };\nexport type Err<E> = { readonly ok: false; readonly error: E };\nexport type Result<T, E = DbError> = Ok<T> | Err<E>;\n\nexport const ok = <T>(value: T): Ok<T> => ({ ok: true, value });\nexport const err = <E>(error: E): Err<E> => ({ ok: false, error });\nexport const isOk = <T, E>(r: Result<T, E>): r is Ok<T> => r.ok;\nexport const isErr = <T, E>(r: Result<T, E>): r is Err<E> => !r.ok;\n","import type { CreateIndexesOptions, Db, IndexDescription } from 'mongodb';\nimport { dedent, isEmpty, isNullish, tryit } from 'radashi';\n\nimport type { CollectionDef } from './collection.js';\nimport type { ZodCompat } from './compat/zod.js';\nimport { toDbError } from './errors.js';\nimport type { IdStrategy } from './id.js';\nimport { err, ok } from './result.js';\nimport type { Result } from './result.js';\n\n// ponytail: IndexDescription.key is { [key: string]: IndexDirection } | Map — not the broader IndexSpecification.\n// We alias it to IndexSpec so callers use the correct type that createIndexes() accepts.\nexport type IndexSpec = IndexDescription['key'];\nexport type IndexDef = { readonly spec: IndexSpec; readonly options?: CreateIndexesOptions };\n\nexport const index = (spec: IndexSpec, options?: CreateIndexesOptions): IndexDef => ({\n spec,\n options,\n});\n\nexport const syncIndexes = async (\n collection: CollectionDef<ZodCompat, IdStrategy>,\n database: Db,\n): Promise<Result<void>> => {\n if (isEmpty(collection.indexes)) return ok(void 0);\n const [error] = await tryit(() =>\n database\n .collection(collection.name)\n .createIndexes(\n collection.indexes.map((indexEntry) => ({ key: indexEntry.spec, ...indexEntry.options })),\n ),\n )();\n return isNullish(error) ? ok(void 0) : err(toDbError(error));\n};\n\nconst emptyMigration = () => dedent`\n 'use strict'\n\n module.exports = {\n async up(_db) {},\n async down(_db) {},\n }\n`;\n\nconst indexMigration = (name: string, specs: string, names: string) => dedent`\n 'use strict'\n\n module.exports = {\n async up(db) {\n await db.collection('${name}').createIndexes(${specs})\n },\n async down(db) {\n await db.collection('${name}').dropIndexes(${names})\n },\n }\n`;\n\nexport const generateIndexMigration = (\n collection: CollectionDef<ZodCompat, IdStrategy>,\n): string => {\n if (isEmpty(collection.indexes)) return emptyMigration();\n\n const specs = JSON.stringify(\n collection.indexes.map((indexEntry) => ({ key: indexEntry.spec, ...indexEntry.options })),\n null,\n 2,\n );\n // ponytail: respect options.name if provided — MongoDB stores the index under that name,\n // so dropIndexes must use the same name or the rollback is a silent no-op\n const names = JSON.stringify(\n collection.indexes.map((indexEntry) => {\n if (indexEntry.options?.name) return indexEntry.options.name;\n const keys = Object.keys(indexEntry.spec as object);\n return keys\n .map((key) => `${key}_${String((indexEntry.spec as Record<string, number | string>)[key])}`)\n .join('_');\n }),\n );\n\n return indexMigration(collection.name, specs, names);\n};\n","import type { ZodCompat, Infer } from './compat/zod.js';\nimport type { IdStrategy, InferIdType } from './id.js';\nimport type { IndexDef } from './indexes.js';\n\nexport type Doc<Schema extends ZodCompat, Id extends IdStrategy> = Infer<Schema> & {\n readonly _id: InferIdType<Id>;\n};\n\nexport type CollectionDef<Schema extends ZodCompat, Id extends IdStrategy> = {\n readonly name: string;\n readonly schema: Schema;\n readonly id: Id;\n readonly indexes: readonly IndexDef[];\n readonly __doc?: Doc<Schema, Id>;\n};\n\nexport const defineCollection = <\n Schema extends ZodCompat,\n Id extends IdStrategy = 'objectid',\n>(config: {\n name: string;\n schema: Schema;\n id?: Id;\n indexes?: IndexDef[];\n}): CollectionDef<Schema, Id> =>\n Object.freeze({\n name: config.name,\n schema: config.schema,\n id: (config.id ?? 'objectid') as Id,\n indexes: Object.freeze(config.indexes ?? []),\n }) as CollectionDef<Schema, Id>;\n","import type {\n Collection,\n Filter,\n UpdateFilter,\n FindOneAndUpdateOptions,\n FindOneAndDeleteOptions,\n ModifyResult,\n WithId,\n} from 'mongodb';\n\n// ponytail: version detection at load time — avoids per-call overhead\nconst MONGO_MAJOR = (() => {\n // eslint-disable-next-line @typescript-eslint/no-require-imports, unicorn/prefer-module\n const version: string = (require('mongodb/package.json') as { version: string }).version;\n const match = /^(\\d+)/.exec(version);\n return match ? Number(match[1]) : 6;\n})();\n\ntype FindOneAndModifyOp<T> =\n | { kind: 'update'; update: UpdateFilter<T>; options?: FindOneAndUpdateOptions }\n | { kind: 'delete'; options?: FindOneAndDeleteOptions };\n\n// ponytail: v5 returns ModifyResult<T> ({ value: WithId<T> | null, ... }); v6/7 returns WithId<T> | null directly.\n// ModifyResult<T> and WithId<T> don't overlap, so we go through unknown for the v5 branch only.\nconst extractResult = <T>(raw: unknown, isV5: boolean): WithId<T> | null => {\n if (isV5) {\n const result = raw as ModifyResult<T> | null | undefined;\n return result?.value ?? null;\n }\n return (raw as WithId<T> | null) ?? null;\n};\n\nexport const findOneAndModify = async <T extends object>(\n collection: Collection<T>,\n filter: Filter<T>,\n op: FindOneAndModifyOp<T>,\n): Promise<WithId<T> | null> => {\n const isV5 = MONGO_MAJOR <= 5;\n if (op.kind === 'delete') {\n const raw = await collection.findOneAndDelete(filter, op.options ?? {});\n return extractResult<T>(raw, isV5);\n }\n const raw = await collection.findOneAndUpdate(filter, op.update, {\n returnDocument: 'after',\n ...op.options,\n });\n return extractResult<T>(raw, isV5);\n};\n","import { randomUUID } from 'node:crypto';\n\nimport { ObjectId } from 'mongodb';\n\nimport type { Infer, ZodCompat } from './compat/zod.js';\n\nexport type IdStrategy = 'objectid' | 'uuid' | 'string' | ZodCompat;\n\nexport type InferIdType<T extends IdStrategy> = T extends 'objectid'\n ? ObjectId\n : T extends 'uuid'\n ? string\n : T extends 'string'\n ? string\n : T extends ZodCompat\n ? Infer<T>\n : never;\n\n// generateId handles only auto-generated strategies.\n// 'string' and custom ZodCompat require caller-supplied values.\nexport const generateId = (strategy: 'objectid' | 'uuid'): ObjectId | string => {\n if (strategy === 'objectid') return new ObjectId();\n return randomUUID();\n};\n","import type { Db, Document, Filter, OptionalUnlessRequiredId, UpdateFilter } from 'mongodb';\nimport { isNullish, shake, tryit } from 'radashi';\n\nimport type { CollectionDef, Doc } from './collection.js';\nimport { findOneAndModify } from './compat/driver.js';\nimport type { Infer, ZodCompat } from './compat/zod.js';\nimport { toDbError } from './errors.js';\nimport { generateId } from './id.js';\nimport type { IdStrategy, InferIdType } from './id.js';\nimport { err, ok } from './result.js';\nimport type { Result } from './result.js';\n\nexport type Repository<Schema extends ZodCompat, Id extends IdStrategy> = {\n findById(id: InferIdType<Id>): Promise<Result<Doc<Schema, Id> | null>>;\n findOne(filter: Filter<Doc<Schema, Id>>): Promise<Result<Doc<Schema, Id> | null>>;\n find(filter?: Filter<Doc<Schema, Id>>): Promise<Result<Doc<Schema, Id>[]>>;\n insert(data: Infer<Schema>): Promise<Result<Doc<Schema, Id>>>;\n insertMany(data: Infer<Schema>[]): Promise<Result<Doc<Schema, Id>[]>>;\n updateById(\n id: InferIdType<Id>,\n patch: Partial<Infer<Schema>>,\n ): Promise<Result<Doc<Schema, Id> | null>>;\n updateOne(\n filter: Filter<Doc<Schema, Id>>,\n patch: Partial<Infer<Schema>>,\n ): Promise<Result<Doc<Schema, Id> | null>>;\n updateMany(\n filter: Filter<Doc<Schema, Id>>,\n patch: Partial<Infer<Schema>>,\n ): Promise<Result<{ modifiedCount: number }>>;\n deleteById(id: InferIdType<Id>): Promise<Result<Doc<Schema, Id> | null>>;\n deleteOne(filter: Filter<Doc<Schema, Id>>): Promise<Result<Doc<Schema, Id> | null>>;\n deleteMany(filter: Filter<Doc<Schema, Id>>): Promise<Result<{ deletedCount: number }>>;\n aggregate<Out extends ZodCompat>(\n pipeline: Document[],\n outputSchema: Out,\n ): Promise<Result<Infer<Out>[]>>;\n};\n\n// ponytail: wraps driver promises — tryit returns [error, value] tuple, we map to our Result type\nconst runSafe = async <T>(operation: () => Promise<T>): Promise<Result<T>> => {\n const [error, value] = await tryit(operation)();\n return isNullish(error) ? ok(value as T) : err(toDbError(error));\n};\n\nexport const createRepository = <Schema extends ZodCompat, Id extends IdStrategy>(\n collection: CollectionDef<Schema, Id>,\n database: Db,\n): Repository<Schema, Id> => {\n type TDoc = Doc<Schema, Id>;\n // ponytail: Doc<Schema, Id> has a typed _id: InferIdType<Id> which conflicts with MongoDB's\n // internal WithId<T> wrapper. We narrow via explicit cast at each read site rather than widening\n // the collection type. The cast is safe: the driver returns the shape we inserted.\n const coll = database.collection<TDoc>(collection.name);\n const schema = collection.schema;\n const idStrategy = collection.id;\n\n const parseSchema = (data: unknown): Result<Infer<Schema>> => {\n try {\n return ok(schema.parse(data) as Infer<Schema>);\n } catch (error) {\n return err(toDbError(error));\n }\n };\n\n const parsePartialSchema = (data: unknown): Result<Partial<Infer<Schema>>> => {\n try {\n // ponytail: ZodCompat only guarantees parse() at the type level; partial() exists at runtime\n // for all Zod schemas. The defensive check avoids a hard dependency on Zod internals.\n const partial =\n 'partial' in schema && typeof (schema as { partial?: unknown }).partial === 'function'\n ? (schema as { partial: () => ZodCompat }).partial()\n : schema;\n return ok(partial.parse(data) as Partial<Infer<Schema>>);\n } catch (error) {\n return err(toDbError(error));\n }\n };\n\n const buildDoc = (validated: Infer<Schema>): TDoc => {\n if (idStrategy === 'objectid' || idStrategy === 'uuid') {\n const id = generateId(idStrategy);\n // ponytail: Infer<Schema> is structurally unknown at this level; spreading requires a cast to object.\n // The result is Doc<Schema, Id> by construction (_id + validated fields).\n return { ...(validated as object), _id: id } as TDoc;\n }\n // ponytail: for 'string'/'custom' strategies the caller embeds _id in data —\n // validated already satisfies TDoc structurally; single cast via object is sufficient.\n return { ...(validated as object) } as TDoc;\n };\n\n return {\n findById: (id) =>\n runSafe(() =>\n coll\n .findOne({ _id: id } as Filter<TDoc>)\n .then((found) => (isNullish(found) ? null : found) as TDoc | null),\n ),\n\n findOne: (filter) =>\n runSafe(() =>\n coll.findOne(filter).then((found) => (isNullish(found) ? null : found) as TDoc | null),\n ),\n\n find: (filter) =>\n runSafe(() => {\n // ponytail: Collection.find() is not Array.find() — unicorn cannot distinguish them.\n // eslint-disable-next-line unicorn/no-array-callback-reference\n const cursor = coll.find(filter ?? {});\n return cursor.toArray().then((records) => records as TDoc[]);\n }),\n\n insert: async (data) => {\n const parsed = parseSchema(data);\n if (!parsed.ok) return parsed as Result<TDoc>;\n return runSafe(async () => {\n const record = buildDoc(parsed.value);\n await coll.insertOne(record as OptionalUnlessRequiredId<TDoc>);\n return record;\n });\n },\n\n insertMany: async (data) => {\n const parsedItems: Infer<Schema>[] = [];\n for (const item of data) {\n const result = parseSchema(item);\n if (!result.ok) return result as Result<TDoc[]>;\n parsedItems.push(result.value);\n }\n return runSafe(async () => {\n const records = parsedItems.map((item) => buildDoc(item));\n await coll.insertMany(records as OptionalUnlessRequiredId<TDoc>[]);\n return records;\n });\n },\n\n updateById: async (id, patch) => {\n const parsed = parsePartialSchema(patch);\n if (!parsed.ok) return parsed as Result<TDoc | null>;\n return runSafe(() =>\n findOneAndModify(coll, { _id: id } as Filter<TDoc>, {\n kind: 'update',\n update: { $set: shake(parsed.value) } as UpdateFilter<TDoc>,\n }).then((found) => (isNullish(found) ? null : found) as TDoc | null),\n );\n },\n\n updateOne: async (filter, patch) => {\n const parsed = parsePartialSchema(patch);\n if (!parsed.ok) return parsed as Result<TDoc | null>;\n return runSafe(() =>\n findOneAndModify(coll, filter, {\n kind: 'update',\n update: { $set: shake(parsed.value) } as UpdateFilter<TDoc>,\n }).then((found) => (isNullish(found) ? null : found) as TDoc | null),\n );\n },\n\n updateMany: async (filter, patch) => {\n const parsed = parsePartialSchema(patch);\n if (!parsed.ok) return parsed as Result<{ modifiedCount: number }>;\n return runSafe(() =>\n coll\n .updateMany(filter, { $set: shake(parsed.value) } as UpdateFilter<TDoc>)\n .then((result) => ({ modifiedCount: result.modifiedCount })),\n );\n },\n\n deleteById: (id) =>\n runSafe(() =>\n findOneAndModify(coll, { _id: id } as Filter<TDoc>, { kind: 'delete' }).then(\n (found) => (isNullish(found) ? null : found) as TDoc | null,\n ),\n ),\n\n deleteOne: (filter) =>\n runSafe(() =>\n findOneAndModify(coll, filter, { kind: 'delete' }).then(\n (found) => (isNullish(found) ? null : found) as TDoc | null,\n ),\n ),\n\n deleteMany: (filter) =>\n runSafe(() =>\n coll.deleteMany(filter).then((result) => ({ deletedCount: result.deletedCount })),\n ),\n\n aggregate: <Out extends ZodCompat>(pipeline: Document[], outputSchema: Out) =>\n runSafe(() =>\n coll\n .aggregate(pipeline)\n .toArray()\n .then((records) => records.map((r) => outputSchema.parse(r) as Infer<Out>)),\n ),\n };\n};\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,87 @@
1
+ {
2
+ "name": "@wenu/mongo",
3
+ "version": "0.2.4",
4
+ "description": "Declarative, immutable, type-safe MongoDB repository layer with Zod validation. Zero throws, dual ESM/CJS, pluggable ID strategies.",
5
+ "main": "./dist/index.cjs",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.mts",
8
+ "exports": {
9
+ "./package.json": "./package.json",
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.mts",
13
+ "default": "./dist/index.mjs"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "dist/*.cjs",
23
+ "dist/*.mjs",
24
+ "dist/*.d.cts",
25
+ "dist/*.d.mts",
26
+ "dist/*.map",
27
+ "README.md",
28
+ "LICENSE",
29
+ "CHANGELOG.md"
30
+ ],
31
+ "type": "commonjs",
32
+ "sideEffects": false,
33
+ "keywords": [
34
+ "mongodb",
35
+ "zod",
36
+ "validation",
37
+ "repository",
38
+ "typescript",
39
+ "schema",
40
+ "nosql",
41
+ "database",
42
+ "immutable",
43
+ "type-safe"
44
+ ],
45
+ "author": "Johnny J. Huirilef",
46
+ "license": "MIT",
47
+ "homepage": "https://github.com/johnnyhuirilef/toolkit/tree/main/packages/zod-mongo#readme",
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "git+https://github.com/johnnyhuirilef/toolkit.git",
51
+ "directory": "packages/zod-mongo"
52
+ },
53
+ "bugs": {
54
+ "url": "https://github.com/johnnyhuirilef/toolkit/issues"
55
+ },
56
+ "engines": {
57
+ "node": ">=18.0.0"
58
+ },
59
+ "publishConfig": {
60
+ "access": "public",
61
+ "registry": "https://registry.npmjs.org/"
62
+ },
63
+ "peerDependencies": {
64
+ "mongodb": ">=6.0.0",
65
+ "zod": ">=3.0.0"
66
+ },
67
+ "peerDependenciesMeta": {
68
+ "zod": {
69
+ "optional": false
70
+ }
71
+ },
72
+ "funding": {
73
+ "type": "github",
74
+ "url": "https://github.com/sponsors/johnnyhuirilef"
75
+ },
76
+ "devDependencies": {
77
+ "@nx/vite": "^22.0.0",
78
+ "@testcontainers/mongodb": "^11.10.0",
79
+ "jsonc-eslint-parser": "^2.0.0",
80
+ "tsdown": "^0.22.3",
81
+ "tsx": "^4.0.0",
82
+ "vitest": "^3.0.0"
83
+ },
84
+ "dependencies": {
85
+ "radashi": "^12.1.0"
86
+ }
87
+ }