@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 +36 -0
- package/README.md +535 -0
- package/dist/index.cjs +219 -0
- package/dist/index.d.cts +88 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +88 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +215 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +87 -0
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;
|
package/dist/index.d.cts
ADDED
|
@@ -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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"}
|
package/dist/index.d.mts
ADDED
|
@@ -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":";;;KAGY,WAAA;AAAA,KAEA,OAAA;EAAA,SACD,IAAA,EAAM,WAAW;EAAA,SACjB,OAAA;EAAA,SACA,KAAA;AAAA;AAAA,cAKE,SAAA,GAAa,KAAA,cAAiB,OAO1C;;;KClBW,EAAA;EAAA,SAAmB,EAAA;EAAA,SAAmB,KAAA,EAAO,CAAC;AAAA;AAAA,KAC9C,GAAA;EAAA,SAAoB,EAAA;EAAA,SAAoB,KAAA,EAAO,CAAC;AAAA;AAAA,KAChD,MAAA,QAAc,OAAA,IAAW,EAAA,CAAG,CAAA,IAAK,GAAA,CAAI,CAAA;AAAA,cAEpC,EAAA,MAAS,KAAA,EAAO,CAAA,KAAI,EAAA,CAAG,CAAA;AAAA,cACvB,GAAA,MAAU,KAAA,EAAO,CAAA,KAAI,GAAA,CAAI,CAAA;AAAA,cACzB,IAAA,SAAc,CAAA,EAAG,MAAA,CAAO,CAAA,EAAG,CAAA,MAAK,CAAA,IAAK,EAAA,CAAG,CAAA;AAAA,cACxC,KAAA,SAAe,CAAA,EAAG,MAAA,CAAO,CAAA,EAAG,CAAA,MAAK,CAAA,IAAK,GAAA,CAAI,CAAA;;;KCT3C,SAAA;EAAA,SAAuB,OAAA;EAAkB,KAAA,CAAM,IAAA;AAAA;AAAA,KAC/C,KAAA,WAAgB,SAAA,IAAa,CAAC;;;KCK9B,UAAA,oCAA8C,SAAS;AAAA,KAEvD,WAAA,WAAsB,UAAA,IAAc,CAAA,sBAC5C,QAAA,GACA,CAAA,2BAEE,CAAA,6BAEE,CAAA,SAAU,SAAA,GACR,KAAA,CAAM,CAAA;;;KCXJ,GAAA,gBAAmB,SAAA,aAAsB,UAAA,IAAc,KAAA,CAAM,MAAA;EAAA,SAC9D,GAAA,EAAK,WAAA,CAAY,EAAA;AAAA;AAAA,KAGhB,aAAA,gBAA6B,SAAA,aAAsB,UAAA;EAAA,SACpD,IAAA;EAAA,SACA,MAAA,EAAQ,MAAA;EAAA,SACR,EAAA,EAAI,EAAA;EAAA,SACJ,OAAA,WAAkB,QAAA;EAAA,SAClB,KAAA,GAAQ,GAAA,CAAI,MAAA,EAAQ,EAAA;AAAA;AAAA,cAGlB,gBAAA,kBACI,SAAA,aACJ,UAAA,eACX,MAAA;EACA,IAAA;EACA,MAAA,EAAQ,MAAA;EACR,EAAA,GAAK,EAAA;EACL,OAAA,GAAU,QAAA;AAAA,MACR,aAAA,CAAc,MAAA,EAAQ,EAAA;;;KCZd,SAAA,GAAY,gBAAgB;AAAA,KAC5B,QAAA;EAAA,SAAsB,IAAA,EAAM,SAAA;EAAA,SAAoB,OAAA,GAAU,oBAAoB;AAAA;AAAA,cAE7E,KAAA,GAAS,IAAA,EAAM,SAAA,EAAW,OAAA,GAAU,oBAAA,KAAuB,QAAA;AAAA,cAK3D,WAAA,GACX,UAAA,EAAY,aAAA,CAAc,SAAA,EAAW,UAAA,GACrC,QAAA,EAAU,EAAA,KACT,OAAA,CAAQ,MAAA;AAAA,cAkCE,sBAAA,GACX,UAAA,EAAY,aAAA,CAAc,SAAA,EAAW,UAAA;;;KC9C3B,UAAA,gBAA0B,SAAA,aAAsB,UAAA;EAC1D,QAAA,CAAS,EAAA,EAAI,WAAA,CAAY,EAAA,IAAM,OAAA,CAAQ,MAAA,CAAO,GAAA,CAAI,MAAA,EAAQ,EAAA;EAC1D,OAAA,CAAQ,MAAA,EAAQ,MAAA,CAAO,GAAA,CAAI,MAAA,EAAQ,EAAA,KAAO,OAAA,CAAQ,MAAA,CAAO,GAAA,CAAI,MAAA,EAAQ,EAAA;EACrE,IAAA,CAAK,MAAA,GAAS,MAAA,CAAO,GAAA,CAAI,MAAA,EAAQ,EAAA,KAAO,OAAA,CAAQ,MAAA,CAAO,GAAA,CAAI,MAAA,EAAQ,EAAA;EACnE,MAAA,CAAO,IAAA,EAAM,KAAA,CAAM,MAAA,IAAU,OAAA,CAAQ,MAAA,CAAO,GAAA,CAAI,MAAA,EAAQ,EAAA;EACxD,UAAA,CAAW,IAAA,EAAM,KAAA,CAAM,MAAA,MAAY,OAAA,CAAQ,MAAA,CAAO,GAAA,CAAI,MAAA,EAAQ,EAAA;EAC9D,UAAA,CACE,EAAA,EAAI,WAAA,CAAY,EAAA,GAChB,KAAA,EAAO,OAAA,CAAQ,KAAA,CAAM,MAAA,KACpB,OAAA,CAAQ,MAAA,CAAO,GAAA,CAAI,MAAA,EAAQ,EAAA;EAC9B,SAAA,CACE,MAAA,EAAQ,MAAA,CAAO,GAAA,CAAI,MAAA,EAAQ,EAAA,IAC3B,KAAA,EAAO,OAAA,CAAQ,KAAA,CAAM,MAAA,KACpB,OAAA,CAAQ,MAAA,CAAO,GAAA,CAAI,MAAA,EAAQ,EAAA;EAC9B,UAAA,CACE,MAAA,EAAQ,MAAA,CAAO,GAAA,CAAI,MAAA,EAAQ,EAAA,IAC3B,KAAA,EAAO,OAAA,CAAQ,KAAA,CAAM,MAAA,KACpB,OAAA,CAAQ,MAAA;IAAS,aAAA;EAAA;EACpB,UAAA,CAAW,EAAA,EAAI,WAAA,CAAY,EAAA,IAAM,OAAA,CAAQ,MAAA,CAAO,GAAA,CAAI,MAAA,EAAQ,EAAA;EAC5D,SAAA,CAAU,MAAA,EAAQ,MAAA,CAAO,GAAA,CAAI,MAAA,EAAQ,EAAA,KAAO,OAAA,CAAQ,MAAA,CAAO,GAAA,CAAI,MAAA,EAAQ,EAAA;EACvE,UAAA,CAAW,MAAA,EAAQ,MAAA,CAAO,GAAA,CAAI,MAAA,EAAQ,EAAA,KAAO,OAAA,CAAQ,MAAA;IAAS,YAAA;EAAA;EAC9D,SAAA,aAAsB,SAAA,EACpB,QAAA,EAAU,QAAA,IACV,YAAA,EAAc,GAAA,GACb,OAAA,CAAQ,MAAA,CAAO,KAAA,CAAM,GAAA;AAAA;AAAA,cASb,gBAAA,kBAAmC,SAAA,aAAsB,UAAA,EACpE,UAAA,EAAY,aAAA,CAAc,MAAA,EAAQ,EAAA,GAClC,QAAA,EAAU,EAAA,KACT,UAAA,CAAW,MAAA,EAAQ,EAAA"}
|
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
|
+
}
|