@nimbus-cqrs/mongodb 2.0.0-beta.0 → 2.0.1-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +226 -1
- package/esm/lib/crud/aggregate.js +1 -1
- package/esm/lib/crud/find.js +1 -1
- package/esm/lib/crud/findOne.js +1 -1
- package/esm/lib/crud/findOneAndDelete.js +1 -1
- package/esm/lib/crud/findOneAndReplace.js +1 -1
- package/esm/lib/crud/findOneAndUpdate.js +1 -1
- package/esm/lib/deployCollection.js +1 -1
- package/esm/lib/handleMongoError.d.ts +1 -1
- package/esm/lib/handleMongoError.d.ts.map +1 -1
- package/esm/lib/handleMongoError.js +1 -1
- package/esm/lib/mongoConnectionManager.d.ts +1 -1
- package/esm/lib/mongoConnectionManager.js +2 -2
- package/esm/lib/mongoJSON.d.ts +1 -1
- package/esm/lib/mongoJSON.js +2 -2
- package/esm/lib/repository.d.ts +2 -2
- package/esm/lib/repository.js +3 -3
- package/package.json +12 -3
package/README.md
CHANGED
|
@@ -5,10 +5,235 @@
|
|
|
5
5
|
|
|
6
6
|
# Nimbus MongoDB
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
A small, opinionated layer on top of the official [`mongodb`](https://www.mongodb.com/docs/drivers/node/current/) Node driver. The package gives you:
|
|
9
|
+
|
|
10
|
+
- a **singleton connection manager** with lazy connect, health check and graceful shutdown,
|
|
11
|
+
- typed **CRUD helpers** that throw Nimbus exceptions and emit OpenTelemetry spans automatically,
|
|
12
|
+
- a generic **`MongoDBRepository`** for typed entities with [Zod](https://zod.dev/) validation and pluggable document/entity mapping,
|
|
13
|
+
- a **collection deployer** that creates / updates collections and synchronizes their indexes from a single declarative definition,
|
|
14
|
+
- `MongoJSON` for round-tripping Mongo-typed values (`ObjectId`, `Date`, …) through plain JSON,
|
|
15
|
+
- `handleMongoError` for translating MongoDB driver errors into Nimbus exceptions.
|
|
9
16
|
|
|
10
17
|
Refer to the [Nimbus main repository](https://github.com/overlap-dev/Nimbus) or the [Nimbus documentation](https://nimbus.overlap.at) for more information about the Nimbus framework.
|
|
11
18
|
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# Deno
|
|
23
|
+
deno add jsr:@nimbus-cqrs/mongodb
|
|
24
|
+
|
|
25
|
+
# NPM
|
|
26
|
+
npm install @nimbus-cqrs/mongodb
|
|
27
|
+
|
|
28
|
+
# Bun
|
|
29
|
+
bun add @nimbus-cqrs/mongodb
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`mongodb` is a peer dependency — install it (or use one of the runtimes that resolves it via `npm:`/`jsr:` specifiers).
|
|
33
|
+
|
|
34
|
+
# Examples
|
|
35
|
+
|
|
36
|
+
For detailed documentation, please refer to the [Nimbus documentation](https://nimbus.overlap.at).
|
|
37
|
+
|
|
38
|
+
The snippets below use a tiny `Todo` entity to walk through the package.
|
|
39
|
+
|
|
40
|
+
## MongoConnectionManager
|
|
41
|
+
|
|
42
|
+
`MongoConnectionManager` is a thin singleton around a single long-lived `MongoClient` that makes sure your app reuses one client, connects lazily on first use and shuts down cleanly.
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import { MongoConnectionManager } from "@nimbus-cqrs/mongodb";
|
|
46
|
+
import { ServerApiVersion } from "mongodb";
|
|
47
|
+
|
|
48
|
+
export const mongoManager = MongoConnectionManager.getInstance(
|
|
49
|
+
process.env.MONGO_URI ?? "",
|
|
50
|
+
{
|
|
51
|
+
appName: "todo-app",
|
|
52
|
+
serverApi: {
|
|
53
|
+
version: ServerApiVersion.v1,
|
|
54
|
+
strict: false,
|
|
55
|
+
deprecationErrors: true,
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// Use it from anywhere in the app.
|
|
61
|
+
const todos = await mongoManager.getCollection("todo-app", "todos");
|
|
62
|
+
|
|
63
|
+
// Health endpoint.
|
|
64
|
+
const health = await mongoManager.healthCheck();
|
|
65
|
+
// { status: 'healthy' } | { status: 'error', details: '...' }
|
|
66
|
+
|
|
67
|
+
// Graceful shutdown.
|
|
68
|
+
process.on("SIGTERM", () => {
|
|
69
|
+
mongoManager.close().catch(console.error);
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`getInstance` is idempotent: subsequent calls return the existing instance and ignore the arguments. Concurrent first-callers share the same in-flight `connect()` promise so only one connection is ever established.
|
|
74
|
+
|
|
75
|
+
## CRUD helpers
|
|
76
|
+
|
|
77
|
+
Every common operation has a typed wrapper: `findOne`, `find`, `insertOne`, `insertMany`, `updateOne`, `updateMany`, `replaceOne`, `deleteOne`, `deleteMany`, `findOneAndUpdate`, `findOneAndReplace`, `findOneAndDelete`, `aggregate`, `bulkWrite` and `countDocuments`. They all share the same idea: take a Zod schema for the result, run the operation against a `Collection`, validate the output, throw a Nimbus exception on failure, and produce an OpenTelemetry span for observability.
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
import { findOne, insertOne } from "@nimbus-cqrs/mongodb";
|
|
81
|
+
import { z } from "zod";
|
|
82
|
+
|
|
83
|
+
const Todo = z.object({
|
|
84
|
+
_id: z.string(),
|
|
85
|
+
title: z.string().min(1),
|
|
86
|
+
status: z.enum(["open", "done"]),
|
|
87
|
+
});
|
|
88
|
+
type Todo = z.infer<typeof Todo>;
|
|
89
|
+
|
|
90
|
+
const collection = await mongoManager.getCollection("todo-app", "todos");
|
|
91
|
+
|
|
92
|
+
await insertOne({
|
|
93
|
+
collection,
|
|
94
|
+
document: { _id: "todo-1", title: "Write the README", status: "open" },
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const todo = await findOne<Todo>({
|
|
98
|
+
collection,
|
|
99
|
+
filter: { _id: "todo-1" },
|
|
100
|
+
mapDocument: (doc) => ({
|
|
101
|
+
_id: doc._id.toString(),
|
|
102
|
+
title: doc.title,
|
|
103
|
+
status: doc.status,
|
|
104
|
+
}),
|
|
105
|
+
outputType: Todo,
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
If the document doesn't exist, `findOne` throws a `NotFoundException` instead of returning `null`. If it exists but doesn't match the schema, it throws a `GenericException` carrying the original Zod error — so a malformed document at runtime is loud, not silent.
|
|
110
|
+
|
|
111
|
+
## MongoDBRepository
|
|
112
|
+
|
|
113
|
+
For most domains you don't want to call the CRUD helpers directly in every handler. `MongoDBRepository` wraps them in a typed, single-collection class with consistent error handling (entity-specific `NotFoundException` codes), centralized document↔entity mapping and a stable API.
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
import { MongoDBRepository } from "@nimbus-cqrs/mongodb";
|
|
117
|
+
import type { Document } from "mongodb";
|
|
118
|
+
import { ObjectId } from "mongodb";
|
|
119
|
+
import { z } from "zod";
|
|
120
|
+
|
|
121
|
+
const Todo = z.object({
|
|
122
|
+
_id: z.string(),
|
|
123
|
+
title: z.string().min(1),
|
|
124
|
+
status: z.enum(["open", "done"]),
|
|
125
|
+
});
|
|
126
|
+
type Todo = z.infer<typeof Todo>;
|
|
127
|
+
|
|
128
|
+
class TodoRepository extends MongoDBRepository<Todo> {
|
|
129
|
+
constructor() {
|
|
130
|
+
super(
|
|
131
|
+
() => mongoManager.getCollection("todo-app", "todos"),
|
|
132
|
+
Todo,
|
|
133
|
+
"Todo"
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
override _mapDocumentToEntity(doc: Document): Todo {
|
|
138
|
+
return Todo.parse({
|
|
139
|
+
_id: doc._id.toString(),
|
|
140
|
+
title: doc.title,
|
|
141
|
+
status: doc.status,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
override _mapEntityToDocument(todo: Todo): Document {
|
|
146
|
+
return {
|
|
147
|
+
_id: new ObjectId(todo._id),
|
|
148
|
+
title: todo.title,
|
|
149
|
+
status: todo.status,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export const todoRepository = new TodoRepository();
|
|
155
|
+
|
|
156
|
+
await todoRepository.insertOne({
|
|
157
|
+
item: { _id: "507f1f77bcf86cd799439011", title: "Ship v2", status: "open" },
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const todo = await todoRepository.findOne({
|
|
161
|
+
filter: { _id: new ObjectId("507f1f77bcf86cd799439011") },
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Misses, missed updates and missed deletes throw a `NotFoundException` with a domain-specific `errorCode` derived from the entity name (e.g. `TODO_NOT_FOUND`), which combines very nicely with [`@nimbus-cqrs/hono`](https://www.npmjs.com/package/@nimbus-cqrs/hono)'s `handleError` to produce consistent `404` responses.
|
|
166
|
+
|
|
167
|
+
## deployMongoCollection
|
|
168
|
+
|
|
169
|
+
`deployMongoCollection` reads a single `MongoCollectionDefinition` (name + driver options + indexes) and reconciles it against the database: it creates the collection if missing, applies `collMod` if it exists, and — when `allowUpdateIndexes` is enabled — adds new indexes and drops the ones no longer in the definition. Use it at startup or in a one-off migration script.
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
import { deployMongoCollection } from "@nimbus-cqrs/mongodb";
|
|
173
|
+
|
|
174
|
+
const todoCollection = {
|
|
175
|
+
name: "todos",
|
|
176
|
+
options: {
|
|
177
|
+
validator: {
|
|
178
|
+
$jsonSchema: {
|
|
179
|
+
bsonType: "object",
|
|
180
|
+
required: ["_id", "title", "status"],
|
|
181
|
+
properties: {
|
|
182
|
+
_id: { bsonType: "objectId" },
|
|
183
|
+
title: { bsonType: "string", minLength: 1 },
|
|
184
|
+
status: { enum: ["open", "done"] },
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
indexes: [{ key: { status: 1 }, name: "status_1" }],
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const client = await mongoManager.getClient();
|
|
193
|
+
|
|
194
|
+
await deployMongoCollection({
|
|
195
|
+
mongoClient: client,
|
|
196
|
+
dbName: "todo-app",
|
|
197
|
+
collectionDefinition: todoCollection,
|
|
198
|
+
allowUpdateIndexes: true,
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## MongoJSON
|
|
203
|
+
|
|
204
|
+
`MongoJSON` is a `parse` / `stringify` pair that preserves Mongo-typed values across JSON boundaries by encoding them with short prefixes (`objectId::`, `date::`, `int::`, `double::`). Handy when filters arrive as query-string parameters or are stored in configuration that has to round-trip through plain JSON.
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
import { MongoJSON } from "@nimbus-cqrs/mongodb";
|
|
208
|
+
|
|
209
|
+
const filter = MongoJSON.parse(`{
|
|
210
|
+
"_id": "objectId::507f1f77bcf86cd799439011",
|
|
211
|
+
"createdAt": { "$gte": "date::2025-01-01T00:00:00Z" },
|
|
212
|
+
"priority": "int::1"
|
|
213
|
+
}`);
|
|
214
|
+
|
|
215
|
+
// filter._id instanceof ObjectId
|
|
216
|
+
// filter.createdAt contains a Date
|
|
217
|
+
// filter.priority === 1 (number)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
`parse` rejects strings containing blacklisted operators (default: `$where`) so untrusted input can't smuggle in JavaScript execution.
|
|
221
|
+
|
|
222
|
+
## handleMongoError
|
|
223
|
+
|
|
224
|
+
`handleMongoError` translates raw driver errors into Nimbus exceptions: schema-validation failures (`121`) and duplicate-key errors (`11000`) become `InvalidInputException` with the offending key/value attached, anything else falls back to `GenericException`. The CRUD helpers call it for you — reach for it directly only when you're using the raw driver inside a handler.
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
import { handleMongoError } from "@nimbus-cqrs/mongodb";
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
await collection.insertOne({ _id: "duplicate", title: "oops" });
|
|
231
|
+
} catch (error) {
|
|
232
|
+
throw handleMongoError(error);
|
|
233
|
+
// -> InvalidInputException with { keyValue: { _id: 'duplicate' } }
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
12
237
|
# License
|
|
13
238
|
|
|
14
239
|
Copyright 2024-present Overlap GmbH & Co KG (https://overlap.at)
|
package/esm/lib/crud/find.js
CHANGED
package/esm/lib/crud/findOne.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GenericException, NotFoundException } from '
|
|
1
|
+
import { GenericException, NotFoundException } from '@nimbus-cqrs/core';
|
|
2
2
|
import { handleMongoError } from '../handleMongoError.js';
|
|
3
3
|
import { withSpan } from '../tracing.js';
|
|
4
4
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GenericException, NotFoundException } from '
|
|
1
|
+
import { GenericException, NotFoundException } from '@nimbus-cqrs/core';
|
|
2
2
|
import { handleMongoError } from '../handleMongoError.js';
|
|
3
3
|
import { withSpan } from '../tracing.js';
|
|
4
4
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GenericException, NotFoundException } from '
|
|
1
|
+
import { GenericException, NotFoundException } from '@nimbus-cqrs/core';
|
|
2
2
|
import { handleMongoError } from '../handleMongoError.js';
|
|
3
3
|
import { withSpan } from '../tracing.js';
|
|
4
4
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GenericException, NotFoundException } from '
|
|
1
|
+
import { GenericException, NotFoundException } from '@nimbus-cqrs/core';
|
|
2
2
|
import { handleMongoError } from '../handleMongoError.js';
|
|
3
3
|
import { withSpan } from '../tracing.js';
|
|
4
4
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handleMongoError.d.ts","sourceRoot":"","sources":["../../src/lib/handleMongoError.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,KAAK,SAAS,EAGjB,MAAM,
|
|
1
|
+
{"version":3,"file":"handleMongoError.d.ts","sourceRoot":"","sources":["../../src/lib/handleMongoError.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,KAAK,SAAS,EAGjB,MAAM,4DAA4D,CAAC;AAEpE;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,GAAI,OAAO,GAAG,KAAG,SAuB7C,CAAC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GenericException, InvalidInputException, } from '
|
|
1
|
+
import { GenericException, InvalidInputException, } from '@nimbus-cqrs/core';
|
|
2
2
|
/**
|
|
3
3
|
* Handles MongoDB errors and converts them
|
|
4
4
|
* to Nimbus exceptions based on the error code.
|
|
@@ -14,7 +14,7 @@ import { type Collection, type Db, MongoClient, type MongoClientOptions } from '
|
|
|
14
14
|
*
|
|
15
15
|
* @example
|
|
16
16
|
* ```ts
|
|
17
|
-
* import { MongoConnectionManager } from '@nimbus/mongodb';
|
|
17
|
+
* import { MongoConnectionManager } from '@nimbus-cqrs/mongodb';
|
|
18
18
|
* import { ServerApiVersion } from 'mongodb';
|
|
19
19
|
*
|
|
20
20
|
* export const mongoManager = MongoConnectionManager.getInstance(
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getLogger } from '
|
|
1
|
+
import { getLogger } from '@nimbus-cqrs/core';
|
|
2
2
|
import { MongoClient, } from 'mongodb';
|
|
3
3
|
/**
|
|
4
4
|
* Singleton wrapper around a single long-lived `MongoClient`.
|
|
@@ -15,7 +15,7 @@ import { MongoClient, } from 'mongodb';
|
|
|
15
15
|
*
|
|
16
16
|
* @example
|
|
17
17
|
* ```ts
|
|
18
|
-
* import { MongoConnectionManager } from '@nimbus/mongodb';
|
|
18
|
+
* import { MongoConnectionManager } from '@nimbus-cqrs/mongodb';
|
|
19
19
|
* import { ServerApiVersion } from 'mongodb';
|
|
20
20
|
*
|
|
21
21
|
* export const mongoManager = MongoConnectionManager.getInstance(
|
package/esm/lib/mongoJSON.d.ts
CHANGED
package/esm/lib/mongoJSON.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { InvalidInputException } from '
|
|
1
|
+
import { InvalidInputException } from '@nimbus-cqrs/core';
|
|
2
2
|
import { ObjectId } from 'mongodb';
|
|
3
3
|
/**
|
|
4
4
|
* Converts a JavaScript object to a JSON string.
|
|
@@ -68,7 +68,7 @@ const mongoJSONParse = (text, operatorBlackList = ['$where']) => {
|
|
|
68
68
|
*
|
|
69
69
|
* @example
|
|
70
70
|
* ```ts
|
|
71
|
-
* import { MongoJSON } from '@nimbus/mongodb';
|
|
71
|
+
* import { MongoJSON } from '@nimbus-cqrs/mongodb';
|
|
72
72
|
*
|
|
73
73
|
* const filterString = MongoJSON.stringify({
|
|
74
74
|
* _id: 'objectId::507f1f77bcf86cd799439011',
|
package/esm/lib/repository.d.ts
CHANGED
|
@@ -27,8 +27,8 @@ export type WithStringId<TSchema> = Omit<TSchema, '_id'> & {
|
|
|
27
27
|
*
|
|
28
28
|
* @example
|
|
29
29
|
* ```ts
|
|
30
|
-
* import { MongoDBRepository } from '@nimbus/mongodb';
|
|
31
|
-
* import { getEnv } from '@nimbus/utils';
|
|
30
|
+
* import { MongoDBRepository } from '@nimbus-cqrs/mongodb';
|
|
31
|
+
* import { getEnv } from '@nimbus-cqrs/utils';
|
|
32
32
|
* import { mongoClient } from './mongoDBClient.ts';
|
|
33
33
|
* import { User } from './user.type.ts';
|
|
34
34
|
* import { USER_COLLECTION } from './user.collection.ts';
|
package/esm/lib/repository.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { NotFoundException } from '
|
|
1
|
+
import { NotFoundException } from '@nimbus-cqrs/core';
|
|
2
2
|
import { toSnakeCase } from '../deps/jsr.io/@std/text/1.0.18/mod.js';
|
|
3
3
|
import { ObjectId } from 'mongodb';
|
|
4
4
|
import { bulkWrite } from './crud/bulkWrite.js';
|
|
@@ -30,8 +30,8 @@ import { replaceOne } from './crud/replaceOne.js';
|
|
|
30
30
|
*
|
|
31
31
|
* @example
|
|
32
32
|
* ```ts
|
|
33
|
-
* import { MongoDBRepository } from '@nimbus/mongodb';
|
|
34
|
-
* import { getEnv } from '@nimbus/utils';
|
|
33
|
+
* import { MongoDBRepository } from '@nimbus-cqrs/mongodb';
|
|
34
|
+
* import { getEnv } from '@nimbus-cqrs/utils';
|
|
35
35
|
* import { mongoClient } from './mongoDBClient.ts';
|
|
36
36
|
* import { User } from './user.type.ts';
|
|
37
37
|
* import { USER_COLLECTION } from './user.collection.ts';
|
package/package.json
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nimbus-cqrs/mongodb",
|
|
3
|
-
"version": "2.0.
|
|
4
|
-
"description": "MongoDB integration for the Nimbus
|
|
3
|
+
"version": "2.0.1-beta.1",
|
|
4
|
+
"description": "Simplify Event-Driven Applications - MongoDB integration for the Nimbus framework.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"nimbus",
|
|
7
|
+
"cqrs",
|
|
8
|
+
"event-sourcing",
|
|
9
|
+
"event-driven",
|
|
10
|
+
"typescript",
|
|
11
|
+
"mongodb",
|
|
12
|
+
"database"
|
|
13
|
+
],
|
|
5
14
|
"author": "Daniel Gördes <d.goerdes@overlap.at> (https://overlap.at)",
|
|
6
15
|
"homepage": "https://nimbus.overlap.at",
|
|
7
16
|
"repository": {
|
|
@@ -26,7 +35,7 @@
|
|
|
26
35
|
"@opentelemetry/api": "^1.9.1",
|
|
27
36
|
"mongodb": "^7.1.1",
|
|
28
37
|
"zod": "^4.3.6",
|
|
29
|
-
"@nimbus-cqrs/core": "^2.0.
|
|
38
|
+
"@nimbus-cqrs/core": "^2.0.1-beta.1"
|
|
30
39
|
},
|
|
31
40
|
"devDependencies": {
|
|
32
41
|
"@types/node": "^22.0.0"
|