@ebarahona/loopback-connector-mongodb 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +526 -0
- package/dist/connector/coercion.d.ts +30 -0
- package/dist/connector/coercion.js +75 -0
- package/dist/connector/coercion.js.map +1 -0
- package/dist/connector/errors.d.ts +13 -0
- package/dist/connector/errors.js +20 -0
- package/dist/connector/errors.js.map +1 -0
- package/dist/connector/index.d.ts +6 -0
- package/dist/connector/index.js +24 -0
- package/dist/connector/index.js.map +1 -0
- package/dist/connector/mongo.connector.d.ts +171 -0
- package/dist/connector/mongo.connector.js +567 -0
- package/dist/connector/mongo.connector.js.map +1 -0
- package/dist/connector/property-mapping.d.ts +64 -0
- package/dist/connector/property-mapping.js +105 -0
- package/dist/connector/property-mapping.js.map +1 -0
- package/dist/connector/query-builder.d.ts +42 -0
- package/dist/connector/query-builder.js +204 -0
- package/dist/connector/query-builder.js.map +1 -0
- package/dist/datasource/index.d.ts +3 -0
- package/dist/datasource/index.js +10 -0
- package/dist/datasource/index.js.map +1 -0
- package/dist/datasource/mongo.datasource.d.ts +17 -0
- package/dist/datasource/mongo.datasource.factory.d.ts +30 -0
- package/dist/datasource/mongo.datasource.factory.js +44 -0
- package/dist/datasource/mongo.datasource.factory.js.map +1 -0
- package/dist/datasource/mongo.datasource.js +40 -0
- package/dist/datasource/mongo.datasource.js.map +1 -0
- package/dist/datasource/mongo.datasource.provider.d.ts +17 -0
- package/dist/datasource/mongo.datasource.provider.js +42 -0
- package/dist/datasource/mongo.datasource.provider.js.map +1 -0
- package/dist/helpers/config-validator.d.ts +34 -0
- package/dist/helpers/config-validator.js +79 -0
- package/dist/helpers/config-validator.js.map +1 -0
- package/dist/helpers/connection-manager.d.ts +78 -0
- package/dist/helpers/connection-manager.js +212 -0
- package/dist/helpers/connection-manager.js.map +1 -0
- package/dist/helpers/index.d.ts +5 -0
- package/dist/helpers/index.js +15 -0
- package/dist/helpers/index.js.map +1 -0
- package/dist/helpers/topology.d.ts +23 -0
- package/dist/helpers/topology.js +27 -0
- package/dist/helpers/topology.js.map +1 -0
- package/dist/helpers/url-builder.d.ts +7 -0
- package/dist/helpers/url-builder.js +30 -0
- package/dist/helpers/url-builder.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/dist/keys.d.ts +38 -0
- package/dist/keys.js +38 -0
- package/dist/keys.js.map +1 -0
- package/dist/mongo.component.d.ts +59 -0
- package/dist/mongo.component.js +138 -0
- package/dist/mongo.component.js.map +1 -0
- package/dist/providers/index.d.ts +0 -0
- package/dist/providers/index.js +4 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.js +7 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/mongo.service.d.ts +61 -0
- package/dist/services/mongo.service.impl.d.ts +58 -0
- package/dist/services/mongo.service.impl.js +211 -0
- package/dist/services/mongo.service.impl.js.map +1 -0
- package/dist/services/mongo.service.js +3 -0
- package/dist/services/mongo.service.js.map +1 -0
- package/dist/types.d.ts +85 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +109 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Ed Barahona
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
# @ebarahona/loopback-connector-mongodb
|
|
2
|
+
|
|
3
|
+
Full-featured MongoDB connector for LoopBack 4, built on the native MongoDB Node.js driver 7.x. Provides CRUD via the juggler connector interface, plus advanced operations through an injectable MongoService.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @ebarahona/loopback-connector-mongodb
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Why
|
|
10
|
+
|
|
11
|
+
This connector is built for a specific architectural goal: combined with [`@ebarahona/loopback-transport-core`](https://github.com/ebarahona/loopback-transport-core), it gives LoopBack 4 apps the same `ExecutionContext`-driven, decorator-based message-handler architecture that NestJS provides, on the LB4 foundation, with LB4's DI, lifecycle, and component model.
|
|
12
|
+
|
|
13
|
+
Two halves of one design:
|
|
14
|
+
|
|
15
|
+
- **[`@ebarahona/loopback-transport-core`](https://github.com/ebarahona/loopback-transport-core)**: a transport-agnostic `ExecutionContext` (one API across HTTP, RPC, and event transports), `@messageHandler` / `@eventHandler` / `@payload` / `@transportCtx` decorators, abstract `ServerBase` / `ClientProxy` for transport adapters. Same programming model as NestJS microservices, composed with LB4's container.
|
|
16
|
+
- **`@ebarahona/loopback-connector-mongodb`** (this package): modern MongoDB driver 7.x connector with a shared `MongoConnectionManager`, multi-tenant `MongoDataSourceFactory`, injectable `MongoService`, and full TypeScript types. Anything a `@messageHandler` method needs from MongoDB is one `@inject(MongoBindings.…)` away.
|
|
17
|
+
|
|
18
|
+
> **`ExecutionContext` is unified across transports.**
|
|
19
|
+
|
|
20
|
+
The two plugins compose orthogonally through LB4's DI, with no glue layer required. A handler is a regular controller method that happens to be decorated with `@messageHandler`; it injects MongoDB the same way any controller would.
|
|
21
|
+
|
|
22
|
+
<details>
|
|
23
|
+
<summary><b>Show example: NestJS-style handler backed by MongoService</b></summary>
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import {inject} from '@loopback/core';
|
|
27
|
+
import {
|
|
28
|
+
messageHandler,
|
|
29
|
+
eventHandler,
|
|
30
|
+
payload,
|
|
31
|
+
} from '@ebarahona/loopback-transport-core';
|
|
32
|
+
import {
|
|
33
|
+
MongoBindings,
|
|
34
|
+
MongoService,
|
|
35
|
+
} from '@ebarahona/loopback-connector-mongodb';
|
|
36
|
+
|
|
37
|
+
export class OrderController {
|
|
38
|
+
constructor(@inject(MongoBindings.SERVICE) private mongo: MongoService) {}
|
|
39
|
+
|
|
40
|
+
@messageHandler('order.get')
|
|
41
|
+
async getOrder(@payload() data: {id: string}) {
|
|
42
|
+
const [order] = await this.mongo.aggregate('orders', [
|
|
43
|
+
{$match: {_id: data.id}},
|
|
44
|
+
{
|
|
45
|
+
$lookup: {
|
|
46
|
+
from: 'line_items',
|
|
47
|
+
localField: '_id',
|
|
48
|
+
foreignField: 'order_id',
|
|
49
|
+
as: 'items',
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
]);
|
|
53
|
+
return order;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@eventHandler('order.placed')
|
|
57
|
+
async onPlaced(@payload() event: {id: string; total: number}) {
|
|
58
|
+
await this.mongo.getCollection('orders').insertOne(event);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
</details>
|
|
64
|
+
|
|
65
|
+
Beyond the transport-core pairing, the official `loopback-connector-mongodb` is stuck on MongoDB driver 5.x with callback-based internals and JavaScript source; it does not support aggregation pipelines, Change Streams, Time Series Collections, `$jsonSchema` validation, GridFS, tailable cursors, or bulk operations. This package is a ground-up TypeScript implementation on driver 7.x that exposes every native driver feature the official connector cannot.
|
|
66
|
+
|
|
67
|
+
| | Official connector | This package |
|
|
68
|
+
| --------------------------------------------------- | ------------------ | ------------ |
|
|
69
|
+
| CRUD (repositories) | Yes | Yes |
|
|
70
|
+
| MongoDB driver | 5.x | 7.x |
|
|
71
|
+
| TypeScript | No | Yes |
|
|
72
|
+
| Aggregation pipelines | No | Yes |
|
|
73
|
+
| Change Streams | No | Yes |
|
|
74
|
+
| Time Series Collections | No | Yes |
|
|
75
|
+
| $jsonSchema validation | No | Yes |
|
|
76
|
+
| GridFS | No | Yes |
|
|
77
|
+
| Transactions | Partial | Yes |
|
|
78
|
+
| Bulk operations | No | Yes |
|
|
79
|
+
| Tailable cursors | No | Yes |
|
|
80
|
+
| Pairs with transport-core for NestJS-style handlers | No | Yes |
|
|
81
|
+
|
|
82
|
+
## What This Provides
|
|
83
|
+
|
|
84
|
+
| Layer | Purpose |
|
|
85
|
+
| ------------------ | ---------------------------------------------------------------------------------------------------------- |
|
|
86
|
+
| **Connector** | Juggler-compatible CRUD (models, repositories, datasources) |
|
|
87
|
+
| **MongoService** | Aggregation, Change Streams, Time Series, GridFS, transactions, bulk ops, tailable cursors, indexes, admin |
|
|
88
|
+
| **MongoComponent** | LB4 Component with singleton MongoClient, lifecycle management |
|
|
89
|
+
|
|
90
|
+
## Integration Paths
|
|
91
|
+
|
|
92
|
+
This package supports two integration modes:
|
|
93
|
+
|
|
94
|
+
**Component path (recommended):** Use `MongoComponent`. It binds a shared `MongoConnectionManager`, the `MongoService`, and a `MongoDataSource` (a juggler `DataSource` wired to the shared manager) so repositories and `MongoService` share one connection pool. The lifecycle observer owns connect/disconnect.
|
|
95
|
+
|
|
96
|
+
**Standalone juggler path:** Use `initialize()` via a plain juggler `DataSource`. The connector creates and owns its own connection manager. `MongoService` is not available in this mode.
|
|
97
|
+
|
|
98
|
+
## Quick Start
|
|
99
|
+
|
|
100
|
+
### Using the Component (recommended)
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
import {Application} from '@loopback/core';
|
|
104
|
+
import {juggler} from '@loopback/repository';
|
|
105
|
+
import {
|
|
106
|
+
MongoComponent,
|
|
107
|
+
MongoBindings,
|
|
108
|
+
MongoService,
|
|
109
|
+
} from '@ebarahona/loopback-connector-mongodb';
|
|
110
|
+
|
|
111
|
+
const app = new Application();
|
|
112
|
+
app.bind(MongoBindings.CONFIG).to({
|
|
113
|
+
url: 'mongodb://localhost:27017',
|
|
114
|
+
database: 'myapp',
|
|
115
|
+
});
|
|
116
|
+
app.component(MongoComponent);
|
|
117
|
+
await app.start();
|
|
118
|
+
|
|
119
|
+
// Shared DataSource for repositories
|
|
120
|
+
const ds = await app.get<juggler.DataSource>(MongoBindings.DATASOURCE);
|
|
121
|
+
|
|
122
|
+
// Same connection pool, advanced operations
|
|
123
|
+
const mongo = await app.get<MongoService>(MongoBindings.SERVICE);
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The repositories built against `MongoBindings.DATASOURCE` and code that injects `MongoBindings.SERVICE` share the same `MongoConnectionManager`, so there is exactly one pool, one lifecycle, and one topology state.
|
|
127
|
+
|
|
128
|
+
### Using the Connector with DataSource (standalone)
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
import {juggler} from '@loopback/repository';
|
|
132
|
+
|
|
133
|
+
const ds = new juggler.DataSource({
|
|
134
|
+
connector: require('@ebarahona/loopback-connector-mongodb'),
|
|
135
|
+
url: 'mongodb://localhost:27017/myapp',
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## MongoService
|
|
140
|
+
|
|
141
|
+
Inject `MongoBindings.SERVICE` to access advanced operations:
|
|
142
|
+
|
|
143
|
+
<details>
|
|
144
|
+
<summary><b>Show example: aggregation, change streams, time series, GridFS, transactions</b></summary>
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
import {inject} from '@loopback/core';
|
|
148
|
+
import {
|
|
149
|
+
MongoBindings,
|
|
150
|
+
MongoService,
|
|
151
|
+
} from '@ebarahona/loopback-connector-mongodb';
|
|
152
|
+
|
|
153
|
+
class AnalyticsService {
|
|
154
|
+
constructor(@inject(MongoBindings.SERVICE) private mongo: MongoService) {}
|
|
155
|
+
|
|
156
|
+
// Aggregation pipeline
|
|
157
|
+
async getDailyMetrics(): Promise<DailyMetric[]> {
|
|
158
|
+
return this.mongo.aggregate('ts_ad_insights', [
|
|
159
|
+
{$match: {timestamp: {$gte: startDate}}},
|
|
160
|
+
{$group: {_id: '$date', totalSpend: {$sum: '$spend'}}},
|
|
161
|
+
{$sort: {_id: 1}},
|
|
162
|
+
]);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Change Streams (requires replica set)
|
|
166
|
+
watchInserts(): ChangeStream {
|
|
167
|
+
return this.mongo.watchCollection('orders', [
|
|
168
|
+
{$match: {operationType: 'insert'}},
|
|
169
|
+
]);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Time Series collection
|
|
173
|
+
async setupMetrics(): Promise<void> {
|
|
174
|
+
await this.mongo.createTimeSeriesCollection('ts_metrics', {
|
|
175
|
+
timeField: 'timestamp',
|
|
176
|
+
metaField: 'source',
|
|
177
|
+
granularity: 'minutes',
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// GridFS
|
|
182
|
+
getFileBucket(): GridFSBucket {
|
|
183
|
+
return this.mongo.getGridFSBucket('uploads');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Transactions
|
|
187
|
+
async transferFunds(from: string, to: string, amount: number): Promise<void> {
|
|
188
|
+
await this.mongo.withTransaction(async session => {
|
|
189
|
+
const accounts = this.mongo.getCollection('accounts');
|
|
190
|
+
await accounts.updateOne(
|
|
191
|
+
{_id: from},
|
|
192
|
+
{$inc: {balance: -amount}},
|
|
193
|
+
{session},
|
|
194
|
+
);
|
|
195
|
+
await accounts.updateOne({_id: to}, {$inc: {balance: amount}}, {session});
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
</details>
|
|
202
|
+
|
|
203
|
+
## MongoService API
|
|
204
|
+
|
|
205
|
+
### Core Access
|
|
206
|
+
|
|
207
|
+
- `getClient()` -- native MongoClient
|
|
208
|
+
- `getDb(name?)` -- database instance
|
|
209
|
+
- `getCollection<T>(name, db?)` -- typed collection
|
|
210
|
+
|
|
211
|
+
### Aggregation
|
|
212
|
+
|
|
213
|
+
- `aggregate<T>(collection, pipeline, options?)` -- execute pipeline, return array
|
|
214
|
+
- `aggregateCursor<T>(collection, pipeline, options?)` -- return cursor for streaming
|
|
215
|
+
|
|
216
|
+
### Change Streams
|
|
217
|
+
|
|
218
|
+
- `watchCollection<T>(collection, pipeline?, options?)` -- collection-level
|
|
219
|
+
- `watchDatabase(pipeline?, options?)` -- database-level
|
|
220
|
+
- `watchClient(pipeline?, options?)` -- client-level (all databases)
|
|
221
|
+
|
|
222
|
+
Requires replica set or sharded cluster. Throws on standalone with a clear error.
|
|
223
|
+
|
|
224
|
+
### Time Series
|
|
225
|
+
|
|
226
|
+
- `createTimeSeriesCollection(name, timeseriesOptions, validatorSchema?, options?)` -- create with optional $jsonSchema
|
|
227
|
+
|
|
228
|
+
### GridFS
|
|
229
|
+
|
|
230
|
+
- `getGridFSBucket(bucketName?, options?)` -- file upload/download
|
|
231
|
+
|
|
232
|
+
### Bulk Operations
|
|
233
|
+
|
|
234
|
+
- `bulkWrite<T>(collection, operations, options?)` -- mixed insert/update/delete
|
|
235
|
+
|
|
236
|
+
### Transactions
|
|
237
|
+
|
|
238
|
+
- `withSession<T>(fn)` -- session scope
|
|
239
|
+
- `withTransaction<T>(fn, options?)` -- ACID transaction with auto-retry
|
|
240
|
+
|
|
241
|
+
### Tailable Cursors
|
|
242
|
+
|
|
243
|
+
- `tailableCursor<T>(collection, filter?, options?)` -- continuous reads on capped collections
|
|
244
|
+
|
|
245
|
+
### Index Management
|
|
246
|
+
|
|
247
|
+
- `createIndex(collection, indexSpec, options?)`
|
|
248
|
+
- `createIndexes(collection, indexes, options?)`
|
|
249
|
+
- `listIndexes(collection)`
|
|
250
|
+
- `dropIndex(collection, indexName)`
|
|
251
|
+
|
|
252
|
+
### Admin
|
|
253
|
+
|
|
254
|
+
- `admin()` -- native Admin instance
|
|
255
|
+
- `listDatabases()`
|
|
256
|
+
- `listCollections(db?, filter?)`
|
|
257
|
+
- `dbStats(db?)`
|
|
258
|
+
- `command(command, db?)`
|
|
259
|
+
|
|
260
|
+
### Topology
|
|
261
|
+
|
|
262
|
+
- `isReplicaSet()` -- detect topology
|
|
263
|
+
- `getTopologyType()` -- 'Single', 'ReplicaSetWithPrimary', 'Sharded', etc.
|
|
264
|
+
|
|
265
|
+
## Connector CRUD
|
|
266
|
+
|
|
267
|
+
The connector implements the juggler interface for standard repository operations:
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
// Standard LoopBack 4 repository usage
|
|
271
|
+
const orders = await this.orderRepo.find({where: {status: 'active'}});
|
|
272
|
+
const order = await this.orderRepo.create({name: 'New', total: 99});
|
|
273
|
+
await this.orderRepo.updateAll({status: 'shipped'}, {where: {id: orderId}});
|
|
274
|
+
const count = await this.orderRepo.count({status: 'pending'});
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Supports: `create`, `find`, `all`, `updateAll`, `deleteAll`, `count`, `replaceById`, `updateOrCreate`, `findOrCreate`, `exists`, `execute`, `beginTransaction`, `commit`, `rollback`.
|
|
278
|
+
|
|
279
|
+
## Reaching the native driver
|
|
280
|
+
|
|
281
|
+
This package exposes the common MongoDB surface via typed helpers, and the rest of the driver is one method call away through documented escape hatches. The goal is that users never need to leave LoopBack 4's DI surface to access any MongoDB capability -- driver options pass through `clientOptions`, and the raw `MongoClient`, `Db`, and `Collection<T>` are reachable from `MongoService`. This is the same architectural pattern MongoDB's own libraries use; for example, the PHP library exposes `$vectorSearch` as just an aggregation pipeline stage rather than a separate API.
|
|
282
|
+
|
|
283
|
+
Authoritative reference: [MongoDB Node.js Driver docs](https://www.mongodb.com/docs/drivers/node/current/) (driver 7.x). The examples below link to the specific driver-doc pages where each feature is documented in depth.
|
|
284
|
+
|
|
285
|
+
### Driver-level logging
|
|
286
|
+
|
|
287
|
+
The driver's structured logger is configured through `clientOptions`, environment variables, or a custom destination. Full reference: [Logging](https://www.mongodb.com/docs/drivers/node/current/monitoring-and-logging/logging/).
|
|
288
|
+
|
|
289
|
+
**Via `clientOptions` (typed):**
|
|
290
|
+
|
|
291
|
+
<details>
|
|
292
|
+
<summary><b>Show config: clientOptions logging</b></summary>
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
app.bind(MongoBindings.CONFIG).to({
|
|
296
|
+
url: 'mongodb://localhost:27017',
|
|
297
|
+
database: 'myapp',
|
|
298
|
+
clientOptions: {
|
|
299
|
+
mongodbLogComponentSeverities: {default: 'info', command: 'off'},
|
|
300
|
+
mongodbLogPath: 'stdout',
|
|
301
|
+
mongodbLogMaxDocumentLength: 500,
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
</details>
|
|
307
|
+
|
|
308
|
+
**Via environment variables (zero config code):**
|
|
309
|
+
|
|
310
|
+
<details>
|
|
311
|
+
<summary><b>Show CLI: log via env vars</b></summary>
|
|
312
|
+
|
|
313
|
+
```bash
|
|
314
|
+
MONGODB_LOG_COMMAND=debug MONGODB_LOG_PATH=stderr node app.js
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
</details>
|
|
318
|
+
|
|
319
|
+
Available variables: `MONGODB_LOG_ALL`, `MONGODB_LOG_COMMAND`, `MONGODB_LOG_TOPOLOGY`, `MONGODB_LOG_SERVER_SELECTION`, `MONGODB_LOG_CONNECTION`, `MONGODB_LOG_CLIENT`, `MONGODB_LOG_PATH`, `MONGODB_LOG_MAX_DOCUMENT_LENGTH`.
|
|
320
|
+
|
|
321
|
+
**Via custom log destination:**
|
|
322
|
+
|
|
323
|
+
<details>
|
|
324
|
+
<summary><b>Show example: custom log destination</b></summary>
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
app.bind(MongoBindings.CONFIG).to({
|
|
328
|
+
url: '...',
|
|
329
|
+
database: '...',
|
|
330
|
+
clientOptions: {
|
|
331
|
+
mongodbLogPath: {
|
|
332
|
+
async write(log) {
|
|
333
|
+
// ship to your structured logger here
|
|
334
|
+
myLogger.info(log);
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
</details>
|
|
342
|
+
|
|
343
|
+
Command logging is performance-heavy; use `mongodbLogMaxDocumentLength` to cap document size in logs and avoid sensitive data leaking through query payloads.
|
|
344
|
+
|
|
345
|
+
### Atlas Vector Search
|
|
346
|
+
|
|
347
|
+
Vector search is server-side and works via the existing `MongoService.aggregate()` -- no special method needed. Pipeline-stage reference: [`$vectorSearch`](https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-stage/).
|
|
348
|
+
|
|
349
|
+
<details>
|
|
350
|
+
<summary><b>Show example: $vectorSearch aggregation pipeline</b></summary>
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
const results = await mongo.aggregate('embeddings', [
|
|
354
|
+
{
|
|
355
|
+
$vectorSearch: {
|
|
356
|
+
index: 'plot_embedding_index',
|
|
357
|
+
path: 'plot_embedding',
|
|
358
|
+
queryVector: [
|
|
359
|
+
/* your embedding */
|
|
360
|
+
],
|
|
361
|
+
numCandidates: 150,
|
|
362
|
+
limit: 5,
|
|
363
|
+
filter: {genre: 'action'},
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
$project: {
|
|
368
|
+
_id: 0,
|
|
369
|
+
title: 1,
|
|
370
|
+
score: {$meta: 'vectorSearchScore'},
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
]);
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
</details>
|
|
377
|
+
|
|
378
|
+
Vector search requires MongoDB Atlas (cloud) or Enterprise 8.0+ with the Atlas Search local emulator. Self-hosted Community Edition does not support vector search.
|
|
379
|
+
|
|
380
|
+
### Atlas Search index management
|
|
381
|
+
|
|
382
|
+
Search-index methods aren't yet first-class on `MongoService` (planned). For now, use the driver via `getCollection()`. Driver-doc reference: [Atlas Search Indexes](https://www.mongodb.com/docs/drivers/node/current/atlas-search/).
|
|
383
|
+
|
|
384
|
+
<details>
|
|
385
|
+
<summary><b>Show example: create and list Atlas Search index</b></summary>
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
const coll = mongo.getCollection('embeddings');
|
|
389
|
+
|
|
390
|
+
await coll.createSearchIndex({
|
|
391
|
+
name: 'plot_embedding_index',
|
|
392
|
+
type: 'vectorSearch',
|
|
393
|
+
definition: {
|
|
394
|
+
fields: [
|
|
395
|
+
{
|
|
396
|
+
type: 'vector',
|
|
397
|
+
path: 'plot_embedding',
|
|
398
|
+
numDimensions: 1536,
|
|
399
|
+
similarity: 'cosine',
|
|
400
|
+
},
|
|
401
|
+
],
|
|
402
|
+
},
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
// Wait for index to be queryable (sync is async on Atlas):
|
|
406
|
+
const indexes = await coll.listSearchIndexes().toArray();
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
</details>
|
|
410
|
+
|
|
411
|
+
Typed `createSearchIndex` / `listSearchIndexes` / `updateSearchIndex` / `dropSearchIndex` helpers on `MongoService` are coming in a future release.
|
|
412
|
+
|
|
413
|
+
### Raw client / db / collection access
|
|
414
|
+
|
|
415
|
+
<details>
|
|
416
|
+
<summary><b>Show example: native MongoClient, Db, and Collection access</b></summary>
|
|
417
|
+
|
|
418
|
+
```typescript
|
|
419
|
+
import {inject} from '@loopback/core';
|
|
420
|
+
import {
|
|
421
|
+
MongoBindings,
|
|
422
|
+
MongoService,
|
|
423
|
+
} from '@ebarahona/loopback-connector-mongodb';
|
|
424
|
+
|
|
425
|
+
class CustomService {
|
|
426
|
+
constructor(@inject(MongoBindings.SERVICE) private mongo: MongoService) {}
|
|
427
|
+
|
|
428
|
+
async runRawCommand() {
|
|
429
|
+
const client = this.mongo.getClient(); // native MongoClient
|
|
430
|
+
const db = this.mongo.getDb(); // native Db (default database)
|
|
431
|
+
const coll = this.mongo.getCollection<MyDoc>('items'); // typed Collection<MyDoc>
|
|
432
|
+
|
|
433
|
+
return db.command({serverStatus: 1});
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
</details>
|
|
439
|
+
|
|
440
|
+
`MongoService.getCollection<T>(name)` retains TypeScript type-safety through the driver's `Collection<T>` shape. See the driver's [Fundamentals](https://www.mongodb.com/docs/drivers/node/current/get-started/) and [CRUD Operations](https://www.mongodb.com/docs/drivers/node/current/crud/) for the full native API.
|
|
441
|
+
|
|
442
|
+
### Arbitrary database commands
|
|
443
|
+
|
|
444
|
+
For any MongoDB command not covered by a first-class helper (server admin, diagnostics, replica-set management, free-form database commands), use `MongoService.command()`. It's a thin wrapper over the driver's [`db.runCommand()`](https://www.mongodb.com/docs/drivers/node/current/run-command/) and accepts any command document the server supports.
|
|
445
|
+
|
|
446
|
+
<details>
|
|
447
|
+
<summary><b>Show example: arbitrary database commands</b></summary>
|
|
448
|
+
|
|
449
|
+
```typescript
|
|
450
|
+
// Server diagnostics
|
|
451
|
+
const status = await mongo.command({serverStatus: 1});
|
|
452
|
+
const stats = await mongo.command({dbStats: 1});
|
|
453
|
+
|
|
454
|
+
// Replica set introspection
|
|
455
|
+
const rsStatus = await mongo.command({replSetGetStatus: 1});
|
|
456
|
+
|
|
457
|
+
// Server-side scripting / admin
|
|
458
|
+
const hello = await mongo.command({hello: 1});
|
|
459
|
+
const buildInfo = await mongo.command({buildInfo: 1});
|
|
460
|
+
|
|
461
|
+
// Target a specific database
|
|
462
|
+
const adminPing = await mongo.command({ping: 1}, 'admin');
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
</details>
|
|
466
|
+
|
|
467
|
+
Use `mongo.getDb().command(...)` directly if you need to pass driver-level `RunCommandOptions` (read preference, session, etc.). The same applies to `mongo.admin().command(...)` for commands that must run against the admin database.
|
|
468
|
+
|
|
469
|
+
Avoid `db.command()` for operations that have a first-class helper (`findOne`, `aggregate`, `createIndex`, etc.). Those wrappers return typed results, handle cursor management, and integrate with the connector's session/transaction support.
|
|
470
|
+
|
|
471
|
+
## Known limitations
|
|
472
|
+
|
|
473
|
+
These APIs are marked `@experimental` for the first release. They work but
|
|
474
|
+
have documented edge cases pending follow-up work.
|
|
475
|
+
|
|
476
|
+
### `MongoConnector.execute()`
|
|
477
|
+
|
|
478
|
+
A raw-driver escape hatch. The `SAFE_COMMANDS` allowlist gates _which_
|
|
479
|
+
methods may be called; argument shape is not validated. Calling
|
|
480
|
+
`execute('deleteMany', {})` with an empty filter will delete the entire
|
|
481
|
+
collection. Prefer the typed helpers on `MongoService` or the connector's
|
|
482
|
+
CRUD methods. Treat `execute()` as a stopgap until the operation you need
|
|
483
|
+
has a first-class wrapper.
|
|
484
|
+
|
|
485
|
+
### `MongoConnector.findOrCreate()`
|
|
486
|
+
|
|
487
|
+
On a duplicate-key conflict (MongoDB error 11000), the follow-up lookup
|
|
488
|
+
re-runs `find(filter)` with the caller's original filter. If the unique
|
|
489
|
+
index that caused the conflict covers a field _not_ in `filter`, the
|
|
490
|
+
returned document may be unrelated to the duplicate. Use `updateOrCreate`
|
|
491
|
+
(upsert) or raw `replaceOne` with explicit unique-key filters for stricter
|
|
492
|
+
semantics. A future release will narrow the lookup to the conflicting
|
|
493
|
+
key automatically.
|
|
494
|
+
|
|
495
|
+
### `Decimal128` precision
|
|
496
|
+
|
|
497
|
+
When a `Decimal128` value is mapped back to a JavaScript number through
|
|
498
|
+
the connector's property mapper, it passes through `parseFloat`. Values
|
|
499
|
+
outside JavaScript's safe Number range (`Number.MAX_SAFE_INTEGER` is
|
|
500
|
+
roughly `2^53`) lose precision. A `decimalAsString` configuration option
|
|
501
|
+
that preserves the full value as a string is planned. For now,
|
|
502
|
+
applications handling financial values should read raw documents via
|
|
503
|
+
`MongoService.getCollection<T>(name)` and work with `Decimal128`
|
|
504
|
+
instances directly.
|
|
505
|
+
|
|
506
|
+
## Topology
|
|
507
|
+
|
|
508
|
+
The connector and service detect topology automatically after connection:
|
|
509
|
+
|
|
510
|
+
- **Standalone**: All operations except Change Streams
|
|
511
|
+
- **Replica Set**: All operations including Change Streams
|
|
512
|
+
- **Sharded**: All operations including Change Streams
|
|
513
|
+
|
|
514
|
+
Change Stream methods throw a descriptive error on standalone instances.
|
|
515
|
+
|
|
516
|
+
## Requirements
|
|
517
|
+
|
|
518
|
+
- Node.js >= 20.19.0
|
|
519
|
+
- MongoDB 5.0+
|
|
520
|
+
- LoopBack 4 application
|
|
521
|
+
|
|
522
|
+
Peer dependencies: `@loopback/core` (>=7.0.0 <8.0.0), `@loopback/repository` (>=8.0.0 <9.0.0). Runtime dependencies: `mongodb` 7.x, `debug`.
|
|
523
|
+
|
|
524
|
+
## License
|
|
525
|
+
|
|
526
|
+
MIT
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coerce a value to ObjectId if it matches the 24-char hex pattern.
|
|
3
|
+
* Returns the original value if it's not a valid ObjectId string.
|
|
4
|
+
*/
|
|
5
|
+
export declare function toObjectId(value: unknown): unknown;
|
|
6
|
+
/**
|
|
7
|
+
* Check if a value is a valid ObjectId hex string.
|
|
8
|
+
*/
|
|
9
|
+
export declare function isObjectIdString(value: unknown): value is string;
|
|
10
|
+
/**
|
|
11
|
+
* Coerce a value to Decimal128 if it's a number or numeric string.
|
|
12
|
+
*/
|
|
13
|
+
export declare function toDecimal128(value: unknown): unknown;
|
|
14
|
+
/**
|
|
15
|
+
* Convert a Binary value to a Buffer.
|
|
16
|
+
*/
|
|
17
|
+
export declare function binaryToBuffer(value: unknown): unknown;
|
|
18
|
+
/**
|
|
19
|
+
* Coerce ID values for a model based on property definitions.
|
|
20
|
+
*
|
|
21
|
+
* @param idValue - The ID value to coerce
|
|
22
|
+
* @param idProp - The property definition for the ID field
|
|
23
|
+
* @param strict - If true, only coerce when explicitly marked as ObjectId
|
|
24
|
+
*/
|
|
25
|
+
export declare function coerceId(idValue: unknown, idProp?: {
|
|
26
|
+
mongodb?: {
|
|
27
|
+
dataType?: string;
|
|
28
|
+
};
|
|
29
|
+
type?: unknown;
|
|
30
|
+
}, strict?: boolean): unknown;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toObjectId = toObjectId;
|
|
4
|
+
exports.isObjectIdString = isObjectIdString;
|
|
5
|
+
exports.toDecimal128 = toDecimal128;
|
|
6
|
+
exports.binaryToBuffer = binaryToBuffer;
|
|
7
|
+
exports.coerceId = coerceId;
|
|
8
|
+
const mongodb_1 = require("mongodb");
|
|
9
|
+
const OBJECT_ID_REGEX = /^[0-9a-fA-F]{24}$/;
|
|
10
|
+
/**
|
|
11
|
+
* Coerce a value to ObjectId if it matches the 24-char hex pattern.
|
|
12
|
+
* Returns the original value if it's not a valid ObjectId string.
|
|
13
|
+
*/
|
|
14
|
+
function toObjectId(value) {
|
|
15
|
+
if (value instanceof mongodb_1.ObjectId)
|
|
16
|
+
return value;
|
|
17
|
+
if (typeof value === 'string' && OBJECT_ID_REGEX.test(value)) {
|
|
18
|
+
return new mongodb_1.ObjectId(value);
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Check if a value is a valid ObjectId hex string.
|
|
24
|
+
*/
|
|
25
|
+
function isObjectIdString(value) {
|
|
26
|
+
return typeof value === 'string' && OBJECT_ID_REGEX.test(value);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Coerce a value to Decimal128 if it's a number or numeric string.
|
|
30
|
+
*/
|
|
31
|
+
function toDecimal128(value) {
|
|
32
|
+
if (value instanceof mongodb_1.Decimal128)
|
|
33
|
+
return value;
|
|
34
|
+
if (typeof value === 'number')
|
|
35
|
+
return mongodb_1.Decimal128.fromString(String(value));
|
|
36
|
+
if (typeof value === 'string') {
|
|
37
|
+
try {
|
|
38
|
+
return mongodb_1.Decimal128.fromString(value);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Convert a Binary value to a Buffer.
|
|
48
|
+
*/
|
|
49
|
+
function binaryToBuffer(value) {
|
|
50
|
+
if (value instanceof mongodb_1.Binary) {
|
|
51
|
+
return value.buffer;
|
|
52
|
+
}
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Coerce ID values for a model based on property definitions.
|
|
57
|
+
*
|
|
58
|
+
* @param idValue - The ID value to coerce
|
|
59
|
+
* @param idProp - The property definition for the ID field
|
|
60
|
+
* @param strict - If true, only coerce when explicitly marked as ObjectId
|
|
61
|
+
*/
|
|
62
|
+
function coerceId(idValue, idProp, strict = false) {
|
|
63
|
+
if (idValue === null || idValue === undefined)
|
|
64
|
+
return idValue;
|
|
65
|
+
// Explicitly marked as ObjectId
|
|
66
|
+
if (idProp?.mongodb?.dataType === 'ObjectId') {
|
|
67
|
+
return toObjectId(idValue);
|
|
68
|
+
}
|
|
69
|
+
// Strict mode: only coerce if explicitly marked
|
|
70
|
+
if (strict)
|
|
71
|
+
return idValue;
|
|
72
|
+
// Lenient mode: auto-coerce 24-char hex strings
|
|
73
|
+
return toObjectId(idValue);
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=coercion.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"coercion.js","sourceRoot":"","sources":["../../src/connector/coercion.ts"],"names":[],"mappings":";;AAQA,gCAMC;AAKD,4CAEC;AAKD,oCAWC;AAKD,wCAKC;AASD,4BAiBC;AAzED,qCAAqD;AAErD,MAAM,eAAe,GAAG,mBAAmB,CAAC;AAE5C;;;GAGG;AACH,SAAgB,UAAU,CAAC,KAAc;IACvC,IAAI,KAAK,YAAY,kBAAQ;QAAE,OAAO,KAAK,CAAC;IAC5C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAC7D,OAAO,IAAI,kBAAQ,CAAC,KAAK,CAAC,CAAC;IAC7B,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;GAEG;AACH,SAAgB,gBAAgB,CAAC,KAAc;IAC7C,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAClE,CAAC;AAED;;GAEG;AACH,SAAgB,YAAY,CAAC,KAAc;IACzC,IAAI,KAAK,YAAY,oBAAU;QAAE,OAAO,KAAK,CAAC;IAC9C,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,oBAAU,CAAC,UAAU,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IAC3E,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,IAAI,CAAC;YACH,OAAO,oBAAU,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QACtC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;GAEG;AACH,SAAgB,cAAc,CAAC,KAAc;IAC3C,IAAI,KAAK,YAAY,gBAAM,EAAE,CAAC;QAC5B,OAAO,KAAK,CAAC,MAAM,CAAC;IACtB,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,QAAQ,CACtB,OAAgB,EAChB,MAAwD,EACxD,MAAM,GAAG,KAAK;IAEd,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,OAAO,CAAC;IAE9D,gCAAgC;IAChC,IAAI,MAAM,EAAE,OAAO,EAAE,QAAQ,KAAK,UAAU,EAAE,CAAC;QAC7C,OAAO,UAAU,CAAC,OAAO,CAAC,CAAC;IAC7B,CAAC;IAED,gDAAgD;IAChD,IAAI,MAAM;QAAE,OAAO,OAAO,CAAC;IAE3B,gDAAgD;IAChD,OAAO,UAAU,CAAC,OAAO,CAAC,CAAC;AAC7B,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error thrown for connector-level failures that aren't config or
|
|
3
|
+
* driver errors (allowlist denial, unknown command, missing document
|
|
4
|
+
* after update, invalid query operator operand). Always wraps the
|
|
5
|
+
* underlying issue in a typed name consumers can match via
|
|
6
|
+
* `instanceof` or `error.name`.
|
|
7
|
+
*
|
|
8
|
+
* @public
|
|
9
|
+
*/
|
|
10
|
+
export declare class MongoConnectorError extends Error {
|
|
11
|
+
readonly name = "MongoConnectorError";
|
|
12
|
+
constructor(message: string);
|
|
13
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MongoConnectorError = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Error thrown for connector-level failures that aren't config or
|
|
6
|
+
* driver errors (allowlist denial, unknown command, missing document
|
|
7
|
+
* after update, invalid query operator operand). Always wraps the
|
|
8
|
+
* underlying issue in a typed name consumers can match via
|
|
9
|
+
* `instanceof` or `error.name`.
|
|
10
|
+
*
|
|
11
|
+
* @public
|
|
12
|
+
*/
|
|
13
|
+
class MongoConnectorError extends Error {
|
|
14
|
+
constructor(message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = 'MongoConnectorError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
exports.MongoConnectorError = MongoConnectorError;
|
|
20
|
+
//# sourceMappingURL=errors.js.map
|