@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.
Files changed (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +526 -0
  3. package/dist/connector/coercion.d.ts +30 -0
  4. package/dist/connector/coercion.js +75 -0
  5. package/dist/connector/coercion.js.map +1 -0
  6. package/dist/connector/errors.d.ts +13 -0
  7. package/dist/connector/errors.js +20 -0
  8. package/dist/connector/errors.js.map +1 -0
  9. package/dist/connector/index.d.ts +6 -0
  10. package/dist/connector/index.js +24 -0
  11. package/dist/connector/index.js.map +1 -0
  12. package/dist/connector/mongo.connector.d.ts +171 -0
  13. package/dist/connector/mongo.connector.js +567 -0
  14. package/dist/connector/mongo.connector.js.map +1 -0
  15. package/dist/connector/property-mapping.d.ts +64 -0
  16. package/dist/connector/property-mapping.js +105 -0
  17. package/dist/connector/property-mapping.js.map +1 -0
  18. package/dist/connector/query-builder.d.ts +42 -0
  19. package/dist/connector/query-builder.js +204 -0
  20. package/dist/connector/query-builder.js.map +1 -0
  21. package/dist/datasource/index.d.ts +3 -0
  22. package/dist/datasource/index.js +10 -0
  23. package/dist/datasource/index.js.map +1 -0
  24. package/dist/datasource/mongo.datasource.d.ts +17 -0
  25. package/dist/datasource/mongo.datasource.factory.d.ts +30 -0
  26. package/dist/datasource/mongo.datasource.factory.js +44 -0
  27. package/dist/datasource/mongo.datasource.factory.js.map +1 -0
  28. package/dist/datasource/mongo.datasource.js +40 -0
  29. package/dist/datasource/mongo.datasource.js.map +1 -0
  30. package/dist/datasource/mongo.datasource.provider.d.ts +17 -0
  31. package/dist/datasource/mongo.datasource.provider.js +42 -0
  32. package/dist/datasource/mongo.datasource.provider.js.map +1 -0
  33. package/dist/helpers/config-validator.d.ts +34 -0
  34. package/dist/helpers/config-validator.js +79 -0
  35. package/dist/helpers/config-validator.js.map +1 -0
  36. package/dist/helpers/connection-manager.d.ts +78 -0
  37. package/dist/helpers/connection-manager.js +212 -0
  38. package/dist/helpers/connection-manager.js.map +1 -0
  39. package/dist/helpers/index.d.ts +5 -0
  40. package/dist/helpers/index.js +15 -0
  41. package/dist/helpers/index.js.map +1 -0
  42. package/dist/helpers/topology.d.ts +23 -0
  43. package/dist/helpers/topology.js +27 -0
  44. package/dist/helpers/topology.js.map +1 -0
  45. package/dist/helpers/url-builder.d.ts +7 -0
  46. package/dist/helpers/url-builder.js +30 -0
  47. package/dist/helpers/url-builder.js.map +1 -0
  48. package/dist/index.d.ts +15 -0
  49. package/dist/index.js +37 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/keys.d.ts +38 -0
  52. package/dist/keys.js +38 -0
  53. package/dist/keys.js.map +1 -0
  54. package/dist/mongo.component.d.ts +59 -0
  55. package/dist/mongo.component.js +138 -0
  56. package/dist/mongo.component.js.map +1 -0
  57. package/dist/providers/index.d.ts +0 -0
  58. package/dist/providers/index.js +4 -0
  59. package/dist/providers/index.js.map +1 -0
  60. package/dist/services/index.d.ts +2 -0
  61. package/dist/services/index.js +7 -0
  62. package/dist/services/index.js.map +1 -0
  63. package/dist/services/mongo.service.d.ts +61 -0
  64. package/dist/services/mongo.service.impl.d.ts +58 -0
  65. package/dist/services/mongo.service.impl.js +211 -0
  66. package/dist/services/mongo.service.impl.js.map +1 -0
  67. package/dist/services/mongo.service.js +3 -0
  68. package/dist/services/mongo.service.js.map +1 -0
  69. package/dist/types.d.ts +85 -0
  70. package/dist/types.js +3 -0
  71. package/dist/types.js.map +1 -0
  72. 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