@livequery/mongodb 2.0.152 → 2.0.154

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 CHANGED
@@ -4,17 +4,14 @@ Native MongoDB datasource adapter for the `@livequery` ecosystem.
4
4
 
5
5
  This package translates Livequery request shapes into MongoDB native driver operations. It is intended for projects that want to use `@livequery/core` with plain `mongodb` collections, without Mongoose models or schema introspection.
6
6
 
7
- The adapter supports two integration styles:
8
-
9
- - Core style: `new MongoDatasource(config)`, `init(routes)`, then `handle(ctx)`.
10
- - Legacy style: `new MongoDatasource()`, `init(config, routes)`, then direct `query(req, options)`.
7
+ Integration with `@livequery/core`: `new MongoDatasource(config)`, `init(routes)`, then `handle(ctx)`. For imperative use you can also call `query(req, options)` directly.
11
8
 
12
9
  Reads are executed with MongoDB aggregation pipelines through `Collection.aggregate(...).toArray()`. Writes use native collection methods such as `insertOne`, `updateOne`, and `deleteOne`.
13
10
 
14
11
  ## Installation
15
12
 
16
13
  ```sh
17
- bun add @livequery/mongodb mongodb bson rxjs
14
+ bun add @livequery/mongodb mongodb rxjs
18
15
  ```
19
16
 
20
17
  For local development in this workspace, `@livequery/core` is installed as a dev dependency from `file:../core`. Runtime JavaScript does not import `@livequery/core`; the generated declaration files use core types.
@@ -25,7 +22,7 @@ For local development in this workspace, `@livequery/core` is installed as a dev
25
22
  export * from './MongoDatasource.js'
26
23
  export * from './DataChangePayload.js'
27
24
  export * from './MongodbRealtime.js'
28
- export * from './types.js'
25
+ export * from './MongodbCollection.js'
29
26
  ```
30
27
 
31
28
  ## Project Meaning
@@ -39,7 +36,7 @@ Typical request flow with `@livequery/core`:
39
36
  1. A framework adapter creates a `LivequeryContext`.
40
37
  2. `LivequeryRequestParser` reads `ctx.request` and writes `ctx.livequery`.
41
38
  3. `MongoDatasource.handle(ctx)` resolves route options from `ctx.request.method` and `ctx.request.ref`.
42
- 4. `MongoDatasource` converts `ctx.livequery.query` into adapter `req.options`.
39
+ 4. `MongoDatasource` reads `ctx.livequery` (keys, query, body, method) as the adapter request.
43
40
  5. Reads are delegated to `MongoQuery`; writes go directly to the native collection.
44
41
  6. The result is assigned to `ctx.response`.
45
42
 
@@ -50,13 +47,13 @@ Typical request flow with `@livequery/core`:
50
47
  Main adapter class.
51
48
 
52
49
  ```ts
53
- class MongoDatasource extends Subject<WebsocketSyncPayload<LivequeryBaseEntity>>
50
+ class MongoDatasource extends Subject<UpdatedData<LivequeryBaseEntity>>
54
51
  ```
55
52
 
56
53
  Responsibilities:
57
54
 
58
55
  - Store datasource config and route options.
59
- - Support core-style and legacy-style initialization.
56
+ - Initialize from a list of routes.
60
57
  - Resolve connection, database, and collection for each request.
61
58
  - Normalize configured ObjectId fields.
62
59
  - Execute reads, inserts, updates, and deletes.
@@ -79,7 +76,7 @@ const datasource = new MongoDatasource({
79
76
  })
80
77
  ```
81
78
 
82
- If `config` is omitted, call `init(config, routes)` later.
79
+ `config` is required before any request runs. Omit it here only if you assign `datasource.config` before the first `handle` / `query` call.
83
80
 
84
81
  #### `init(routes)`
85
82
 
@@ -103,35 +100,6 @@ await datasource.init([
103
100
 
104
101
  Route lookup uses `METHOD path`, for example `GET /products`. A path-only fallback is also stored for compatibility.
105
102
 
106
- #### `init(config, routes)`
107
-
108
- Legacy-style initialization.
109
-
110
- Parameters:
111
-
112
- - `config: MongoDatasourceConfig`: MongoDB connection configuration.
113
- - `routes: Array<{ method; path; options }>`: legacy route entries. The datasource options are nested under `options`.
114
-
115
- Example:
116
-
117
- ```ts
118
- await datasource.init(
119
- {
120
- connections: { default: client },
121
- databases: ['main'],
122
- },
123
- [
124
- {
125
- method: 'GET',
126
- path: '/products',
127
- options: {
128
- collection: 'products',
129
- },
130
- },
131
- ]
132
- )
133
- ```
134
-
135
103
  #### `handle(ctx)`
136
104
 
137
105
  Core handler entry point.
@@ -164,7 +132,7 @@ Executes a parsed Livequery request against one MongoDB collection.
164
132
 
165
133
  Parameters:
166
134
 
167
- - `req: LivequeryRequest`: adapter request. Core requests use `query`; this adapter normalizes it to `options`. Legacy callers can pass `options` directly.
135
+ - `req: LivequeryRequest`: parsed Livequery request. Reads `req.keys`, `req.query`, `req.method`, and `req.body`.
168
136
  - `options: RouteOptions`: route configuration that tells the adapter which collection, database, connection, and ObjectId fields to use.
169
137
 
170
138
  Supported `req.method` values:
@@ -192,7 +160,7 @@ const response = await datasource.query(
192
160
  ref: 'products',
193
161
  is_collection: true,
194
162
  keys: {},
195
- options: { ':limit': 10 },
163
+ query: { ':limit': 10 },
196
164
  },
197
165
  {
198
166
  collection: 'products',
@@ -217,7 +185,7 @@ Responsibilities:
217
185
 
218
186
  Parameters:
219
187
 
220
- - `req: LivequeryRequest`: normalized adapter request. Reads `req.keys`, `req.options`, and `req.is_collection`.
188
+ - `req: LivequeryRequest`: parsed adapter request. Reads `req.keys`, `req.query`, and `req.is_collection` (`req.query` is normalized to `{}` when absent).
221
189
  - `collection: Collection<T>`: native MongoDB collection.
222
190
 
223
191
  Behavior:
@@ -230,8 +198,8 @@ Known behavior:
230
198
  - `:limit` defaults to `10`.
231
199
  - Minimum `:limit` is `1`.
232
200
  - Maximum `:limit` is `100`.
233
- - Cursor paging is implemented.
234
- - Offset paging with `page` is not implemented yet.
201
+ - Cursor paging (`:after` / `:before` / `:around`) is the default.
202
+ - Offset paging is used when `:page` is provided.
235
203
 
236
204
  Filter examples:
237
205
 
@@ -267,7 +235,7 @@ Builds a cursor from a response item and active sort options.
267
235
  Parameters:
268
236
 
269
237
  - `item: LivequeryBaseEntity`: response item. Must contain `id`.
270
- - `options: QueryOption`: request options. Sort options ending with `:sort` are included in the cursor.
238
+ - `options: Record<string, any>`: the request query (`req.query`). Sort options ending with `:sort` are included in the cursor.
271
239
 
272
240
  Returns:
273
241
 
@@ -386,6 +354,136 @@ An inserted `{ _id: 'post1', userId: 'user1', title: 'Hello' }` emits:
386
354
  }
387
355
  ```
388
356
 
357
+ ### `MongodbCollection`
358
+
359
+ Lightweight imperative CRUD wrapper over a native MongoDB collection, with a Mongoose-`Model`-like surface. It is independent of the Livequery request flow and `LivequeryContext`: reach for it when application or service code needs to read and write documents directly, rather than through `MongoDatasource.handle(ctx)`.
360
+
361
+ It is intentionally not an ODM. There are no schemas, validation, hooks, virtuals, or `populate()`. It only layers a few conveniences over the native driver: `id` / `_id` normalization, a collection-level defaults resolver, automatic timestamps, and flexible single-document filters.
362
+
363
+ ```ts
364
+ class MongodbCollection<T = any>
365
+ ```
366
+
367
+ Responsibilities:
368
+
369
+ - Wrap one collection, resolved lazily from a provided `Db`.
370
+ - Return documents with an enumerable `id: string` and a hidden `_id`.
371
+ - Apply a defaults resolver plus `created_at` / `updated_at` on insert.
372
+ - Bump `updated_at` on every update.
373
+ - Accept a `string` id, an `ObjectId`, or a filter object for single-document operations.
374
+
375
+ #### `defineCollection<T>(config)` and `constructor(db, config)`
376
+
377
+ A collection is described once with `defineCollection`, then bound to a `Db` instance.
378
+
379
+ `defineCollection<T>({ collection, defaults? })` returns a typed `CollectionDef<T>`:
380
+
381
+ - `collection: string`: collection name. The handle is resolved lazily via `db.collection(name)`.
382
+ - `defaults?: (input: Partial<T>) => Partial<T>`: optional default-field resolver, a replacement for Mongoose `@Prop({ default })`. It runs on every `create` / `insertMany` with the input document; the input always overrides the returned defaults.
383
+
384
+ `new MongodbCollection<T>(db, config)`:
385
+
386
+ - `db: Db`: a connected `mongodb` `Db`. It is passed in explicitly (no hidden module singleton), so one class works across databases and connections.
387
+ - `config: CollectionDef<T>`: the definition returned by `defineCollection`. The generic `T` is inferred from it, so the instance is fully typed.
388
+
389
+ Example:
390
+
391
+ ```ts
392
+ import { MongoClient } from 'mongodb'
393
+ import { MongodbCollection, defineCollection } from '@livequery/mongodb'
394
+
395
+ const client = new MongoClient(process.env.MONGO_URL!)
396
+ await client.connect()
397
+ const db = client.db('main')
398
+
399
+ type Order = { video_id: string; amount: number; started: boolean; running: boolean }
400
+ type Video = { title: string }
401
+
402
+ // With a defaults resolver (replaces Mongoose @Prop({ default })):
403
+ const Orders = new MongodbCollection(db, defineCollection<Order>({
404
+ collection: 'orders',
405
+ defaults: () => ({ started: false, running: true }),
406
+ }))
407
+
408
+ // Without defaults:
409
+ const Videos = new MongodbCollection(db, defineCollection<Video>({ collection: 'videos' }))
410
+ ```
411
+
412
+ Define each collection once at composition time and reuse the instance across the app.
413
+
414
+ #### Document shape: `MongoDoc<T>`
415
+
416
+ ```ts
417
+ type MongoDoc<T> = T & { id: string; toJSON(): any }
418
+ ```
419
+
420
+ Returned documents are hydrated:
421
+
422
+ - `id` is an enumerable string (`_id.toString()`), so it appears in `JSON.stringify`, JSON responses, and `{ ...doc }`.
423
+ - `_id` is non-enumerable, so it never leaks into output, yet `doc._id` is still readable as an `ObjectId` internally.
424
+ - `toJSON()` returns the document without `_id` and `__v`.
425
+
426
+ #### Methods
427
+
428
+ | Method | Description |
429
+ | --- | --- |
430
+ | `find(filter?)` | Returns hydrated documents. |
431
+ | `findOne(filter?)` | `filter` may be a `string` id, an `ObjectId`, or a filter object. |
432
+ | `findById(id)` | Shorthand for `findOne` with a `string` id or `ObjectId`. |
433
+ | `create(doc)` | Inserts one document; applies defaults + timestamps; strips any incoming `id` / `_id`. |
434
+ | `insertMany(docs)` | Inserts many documents with the same preparation as `create`. |
435
+ | `updateOne(filter, update, opts?)` | Wraps plain updates in `$set` and bumps `updated_at`; `filter` accepts string id / ObjectId / object. |
436
+ | `updateMany(filter, update, opts?)` | Same update handling for many documents. |
437
+ | `deleteOne(filter)` | Deletes one; `filter` accepts string id / ObjectId / object. |
438
+ | `deleteMany(filter?)` | Deletes many documents. |
439
+ | `countDocuments(filter?)` | Counts matching documents. |
440
+ | `exists(filter?)` | `true` when at least one document matches. |
441
+ | `aggregate(pipeline)` | Runs an aggregation pipeline and returns the array. |
442
+ | `collection` | Getter for the raw native `Collection` (escape hatch). |
443
+
444
+ Behavior:
445
+
446
+ - Filter normalization: a 24-hex `string` becomes `{ _id: ObjectId }`; an `ObjectId` becomes `{ _id }`; any other string or object is used unchanged.
447
+ - Update normalization: an update that already contains a `$`-operator (such as `$inc` or `$push`) is passed through; otherwise it is wrapped in `$set`. `updated_at` is always merged into the `$set` branch.
448
+ - Inserts never persist an incoming `id` or `_id`.
449
+
450
+ Example:
451
+
452
+ ```ts
453
+ // create — applies defaults (started/running) + created_at/updated_at; strips any client id/_id
454
+ const order = await Orders.create({ video_id: 'v1', amount: 50 })
455
+ order.id // '507f1f77bcf86cd799439011'
456
+ order.started // false — from the defaults resolver
457
+ order._id // ObjectId — still readable internally
458
+ JSON.stringify(order) // contains "id", not "_id"
459
+
460
+ // insertMany — same preparation as create, returns hydrated docs
461
+ const [a, b] = await Orders.insertMany([{ video_id: 'v2', amount: 10 }, { video_id: 'v3', amount: 20 }])
462
+
463
+ // reads — single-doc helpers accept a string id, an ObjectId, or a filter object
464
+ await Orders.findOne('507f1f77bcf86cd799439011') // by string id (24-hex → _id)
465
+ await Orders.findById(order._id) // by ObjectId
466
+ await Orders.findOne({ video_id: 'v1' }) // by filter
467
+ await Orders.find({ started: false }) // many
468
+
469
+ // updates — plain bodies are wrapped in $set; operator bodies pass through; updated_at always bumped
470
+ await Orders.updateOne(order.id, { amount: 80 }) // → { $set: { amount: 80, updated_at } }
471
+ await Orders.updateOne(order.id, { $inc: { amount: 5 } }) // → { $inc, $set: { updated_at } }
472
+ await Orders.updateMany({ started: false }, { running: true })
473
+
474
+ // existence / counting
475
+ await Orders.exists(order.id) // boolean
476
+ await Orders.countDocuments({ video_id: 'v1' }) // number
477
+
478
+ // delete
479
+ await Orders.deleteOne(order.id)
480
+ await Orders.deleteMany({ video_id: 'v3' })
481
+
482
+ // escape hatches
483
+ await Orders.aggregate([{ $group: { _id: '$video_id', total: { $sum: '$amount' } } }])
484
+ Orders.collection // raw native Collection
485
+ ```
486
+
389
487
  ### `DataChangePayload<T>`
390
488
 
391
489
  Type-only realtime/change payload contract.
@@ -514,7 +612,9 @@ await datasource.handle(ctx)
514
612
 
515
613
  `LivequeryRequestParser` will set `ctx.livequery.document_id` and `ctx.livequery.keys.id`. The datasource converts `id` to Mongo `_id` for document reads and writes.
516
614
 
517
- ## Legacy Usage Example
615
+ ## Direct Query Example
616
+
617
+ For imperative use, call `query(req, options)` directly without going through `handle(ctx)`. Pass the config to the constructor; `init(routes)` is not required for this path since the collection is given in `options`.
518
618
 
519
619
  ```ts
520
620
  import { MongoClient } from 'mongodb'
@@ -523,33 +623,18 @@ import { MongoDatasource } from '@livequery/mongodb'
523
623
  const client = new MongoClient(process.env.MONGO_URL!)
524
624
  await client.connect()
525
625
 
526
- const datasource = new MongoDatasource()
527
-
528
- await datasource.init(
529
- {
530
- connections: { default: client },
531
- databases: ['main'],
532
- },
533
- [
534
- {
535
- method: 'GET',
536
- path: '/products',
537
- options: {
538
- collection: 'products',
539
- },
540
- },
541
- ]
542
- )
626
+ const datasource = new MongoDatasource({
627
+ connections: { default: client },
628
+ databases: ['main'],
629
+ })
543
630
 
544
631
  const response = await datasource.query(
545
632
  {
546
633
  method: 'get',
547
634
  ref: 'products',
548
635
  is_collection: true,
549
- collection_ref: 'products',
550
- schema_collection_ref: 'products',
551
636
  keys: {},
552
- options: { ':limit': 10 },
637
+ query: { ':limit': 10 },
553
638
  },
554
639
  {
555
640
  collection: 'products',
@@ -1,4 +1,4 @@
1
1
  export declare class Cursor {
2
- static caculate(item: Record<string, any>, options: Record<string, any>): string;
2
+ static caculate(item: Record<string, any>, options: Record<string, any>): string | null;
3
3
  static parse(cursor: string): any;
4
4
  }
@@ -46,6 +46,27 @@ export declare class MongoDatasource extends Subject<UpdatedData<LivequeryBaseEn
46
46
  total: number;
47
47
  };
48
48
  } | {
49
+ has: {
50
+ prev: boolean;
51
+ next: boolean;
52
+ };
53
+ count: {
54
+ prev: number;
55
+ next: number;
56
+ current: number;
57
+ total: number;
58
+ };
59
+ page: {
60
+ current: number;
61
+ total: number;
62
+ };
49
63
  item: any;
64
+ cursor: {
65
+ last: string | null;
66
+ first: string | null;
67
+ };
68
+ summary: any;
69
+ } | {
70
+ item: Record<string, any>;
50
71
  }>;
51
72
  }
@@ -111,8 +111,8 @@ export class MongoDatasource extends Subject {
111
111
  const total = current + count.next + count.prev;
112
112
  const paging = {
113
113
  cursor: {
114
- last: Cursor.caculate(items[items.length - 1], req.query),
115
- first: Cursor.caculate(items[0], req.query)
114
+ last: Cursor.caculate(items[items.length - 1], req.query || {}),
115
+ first: Cursor.caculate(items[0], req.query || {})
116
116
  },
117
117
  has,
118
118
  count: {
@@ -155,17 +155,18 @@ export class MongoDatasource extends Subject {
155
155
  };
156
156
  }
157
157
  async #post(req, collection) {
158
+ const { id: _bodyId, _id: _bodyRawId, ...cleanBody } = req.body || {};
158
159
  const merged = {
159
160
  ...req.keys,
160
- ...req.body
161
+ ...cleanBody
161
162
  };
162
163
  const result = await collection.insertOne(merged);
163
164
  return {
164
- item: {
165
+ item: this.#stringifyOids({
165
166
  ...merged,
166
167
  _id: undefined,
167
168
  id: result.insertedId.toString()
168
- }
169
+ })
169
170
  };
170
171
  }
171
172
  async #put(req, collection) {
@@ -185,11 +186,16 @@ export class MongoDatasource extends Subject {
185
186
  const isPlainBody = req.body && typeof req.body === 'object'
186
187
  && !Object.keys(req.body).some(k => k.startsWith('$'));
187
188
  const id = keys.id ?? req.document_id;
188
- return {
189
+ return this.#stringifyOids({
189
190
  ...keys,
190
191
  ...isPlainBody ? req.body : {},
191
192
  ...id ? { id } : {}
192
- };
193
+ });
194
+ }
195
+ #stringifyOids(item) {
196
+ return Object.entries(item).reduce((p, [k, v]) => {
197
+ return { ...p, [k]: v instanceof ObjectId ? v.toString() : v };
198
+ }, {});
193
199
  }
194
200
  #keys(req) {
195
201
  return Object.entries(req.keys).reduce((p, [k, c]) => {
@@ -212,7 +218,8 @@ export class MongoDatasource extends Subject {
212
218
  #update(body) {
213
219
  if (!body || Object.keys(body).some(key => key.startsWith('$')))
214
220
  return body;
215
- return { $set: body };
221
+ const { id: _id, _id: _rawId, ...cleanBody } = body;
222
+ return { $set: cleanBody };
216
223
  }
217
224
  #convert(obj, fields) {
218
225
  return {
@@ -159,7 +159,7 @@ export class MongoQuery {
159
159
  }
160
160
  }).filter(Boolean);
161
161
  const is_distinc_count = `${v}`.includes('distinc');
162
- const simple = is_distinc_count ? key : (exprs.length == 1 ? fns[0].key : false);
162
+ const simple = is_distinc_count ? key : (exprs.length == 1 && fns[0] ? fns[0].key : false);
163
163
  const pipelines = [
164
164
  ...$match.length > 0 ? [{ $match }] : [],
165
165
  {
@@ -238,34 +238,14 @@ export class MongoQuery {
238
238
  if (k.endsWith(':like'))
239
239
  return p;
240
240
  const [key, expression] = k.split(':');
241
- const map = {
242
- eq: () => ({ $eq: value }),
243
- lt: () => ({ $lt: !isNaN(Number(value)) ? Number(value) : 0 }),
244
- lte: () => ({ $lte: !isNaN(Number(value)) ? Number(value) : 0 }),
245
- gt: () => ({ $gt: !isNaN(Number(value)) ? Number(value) : 0 }),
246
- gte: () => ({ $gte: !isNaN(Number(value)) ? Number(value) : 0 }),
247
- ne: () => {
248
- return { $ne: value };
249
- },
250
- in: () => ({ $in: this.#parse_array(value) }),
251
- nin: () => ({ $nin: this.#parse_array(value) }),
252
- 'eq-number': () => ({ $eq: !isNaN(Number(value)) ? Number(value) : 0 }),
253
- 'neq-number': () => ({ $ne: !isNaN(Number(value)) ? Number(value) : 0 }),
254
- 'eq-boolean': () => ({ $eq: `${value}`.toLowerCase() == 'true' ? true : false }),
255
- 'neq-boolean': () => ({ $ne: `${value}`.toLowerCase() == 'false' ? false : true }),
256
- 'eq-null': () => ({ $eq: null }),
257
- 'neq-null': () => ({ $ne: null }),
258
- 'eq-oid': () => ({ $eq: ObjectId.isValid(value) ? new ObjectId(value) : value }),
259
- 'neq-oid': () => ({ $ne: ObjectId.isValid(value) ? new ObjectId(value) : value }),
260
- };
261
- const fn = map[expression || 'eq'];
262
- if (!fn)
241
+ const clause = this.#operator_clause(expression, value);
242
+ if (!clause)
263
243
  return p;
264
244
  return {
265
245
  ...p,
266
246
  [key]: {
267
247
  ...p[key] || {},
268
- ...fn()
248
+ ...clause
269
249
  }
270
250
  };
271
251
  }, {});
@@ -277,20 +257,75 @@ export class MongoQuery {
277
257
  const andMatch = and ? this.#build_match(and) : {};
278
258
  if (Object.keys(andMatch).length > 0)
279
259
  clauses.push(andMatch);
280
- const orMatch = or ? this.#build_match(or) : {};
281
- if (Object.keys(orMatch).length > 0) {
282
- clauses.push({ $or: Object.entries(orMatch).map(([k, v]) => ({ [k]: v })) });
283
- }
284
- const notMatch = not ? this.#build_match(not) : {};
285
- if (Object.keys(notMatch).length > 0) {
286
- clauses.push({ $nor: Object.entries(notMatch).map(([k, v]) => ({ [k]: v })) });
287
- }
260
+ const orBranches = or ? this.#branches(or) : [];
261
+ if (orBranches.length > 0)
262
+ clauses.push({ $or: orBranches });
263
+ const notBranches = not ? this.#branches(not) : [];
264
+ if (notBranches.length > 0)
265
+ clauses.push({ $nor: notBranches });
288
266
  if (clauses.length === 0)
289
267
  return {};
290
268
  if (clauses.length === 1)
291
269
  return clauses[0];
292
270
  return { $and: clauses };
293
271
  }
272
+ static #operator_clause(expression, value) {
273
+ const map = {
274
+ eq: () => ({ $eq: value }),
275
+ lt: () => ({ $lt: !isNaN(Number(value)) ? Number(value) : 0 }),
276
+ lte: () => ({ $lte: !isNaN(Number(value)) ? Number(value) : 0 }),
277
+ gt: () => ({ $gt: !isNaN(Number(value)) ? Number(value) : 0 }),
278
+ gte: () => ({ $gte: !isNaN(Number(value)) ? Number(value) : 0 }),
279
+ ne: () => ({ $ne: value }),
280
+ in: () => ({ $in: this.#parse_array(value) }),
281
+ nin: () => ({ $nin: this.#parse_array(value) }),
282
+ 'eq-number': () => ({ $eq: !isNaN(Number(value)) ? Number(value) : 0 }),
283
+ 'neq-number': () => ({ $ne: !isNaN(Number(value)) ? Number(value) : 0 }),
284
+ 'eq-boolean': () => ({ $eq: `${value}`.toLowerCase() == 'true' ? true : false }),
285
+ 'neq-boolean': () => ({ $ne: `${value}`.toLowerCase() == 'false' ? false : true }),
286
+ 'eq-null': () => ({ $eq: null }),
287
+ 'neq-null': () => ({ $ne: null }),
288
+ 'eq-oid': () => ({ $eq: ObjectId.isValid(value) ? new ObjectId(value) : value }),
289
+ 'neq-oid': () => ({ $ne: ObjectId.isValid(value) ? new ObjectId(value) : value }),
290
+ };
291
+ const fn = map[expression || 'eq'];
292
+ return fn ? fn() : null;
293
+ }
294
+ static #branches(filters) {
295
+ if (!filters)
296
+ return [];
297
+ const { ':and': and, ':or': or, ':not': not, ...rest } = filters;
298
+ const branches = [];
299
+ for (const [k, value] of Object.entries(rest)) {
300
+ if (k.startsWith('::'))
301
+ continue;
302
+ if (k.endsWith(':like')) {
303
+ const key = k.split(':like')[0];
304
+ branches.push({ [key]: { $regex: this.#escape_regex(`${value}`), $options: 'i' } });
305
+ continue;
306
+ }
307
+ const [key, expression] = k.split(':');
308
+ const clause = this.#operator_clause(expression, value);
309
+ if (clause)
310
+ branches.push({ [key]: clause });
311
+ }
312
+ if (and) {
313
+ const m = this.#build_match(and);
314
+ if (Object.keys(m).length > 0)
315
+ branches.push(m);
316
+ }
317
+ if (or) {
318
+ const m = this.#build_match({ ':or': or });
319
+ if (Object.keys(m).length > 0)
320
+ branches.push(m);
321
+ }
322
+ if (not) {
323
+ const m = this.#build_match({ ':not': not });
324
+ if (Object.keys(m).length > 0)
325
+ branches.push(m);
326
+ }
327
+ return branches;
328
+ }
294
329
  static #build_search_query(req) {
295
330
  const search = req.query[":search"];
296
331
  return search ? [{ $match: { $text: { $search: `${search}` } } }] : [];
@@ -489,12 +524,14 @@ export class MongoQuery {
489
524
  return $sort;
490
525
  }
491
526
  static async query(req, collection) {
492
- if (!req.is_collection) {
527
+ const request = { ...req, keys: req.keys ?? {}, query: req.query ?? {} };
528
+ if (!request.is_collection) {
529
+ const { id, _id: _rawId, ...keysWithoutId } = request.keys;
493
530
  const aggregates = [
494
531
  {
495
532
  $match: {
496
- ...req.keys,
497
- ...req.keys.id ? { id: undefined, _id: this.#objectId('id', req.keys.id) } : {}
533
+ ...keysWithoutId,
534
+ ...id ? { _id: this.#objectId('id', id) } : {}
498
535
  }
499
536
  },
500
537
  ...this.#rename_id(),
@@ -513,19 +550,19 @@ export class MongoQuery {
513
550
  summary: {}
514
551
  };
515
552
  }
516
- const is_cursor_paging = req.query[':after'] || req.query[':before'] || req.query[':around'] || !req.query[':page'];
517
- const $sort = this.#get_sorter(req);
553
+ const is_cursor_paging = request.query[':after'] || request.query[':before'] || request.query[':around'] || !request.query[':page'];
554
+ const $sort = this.#get_sorter(request);
518
555
  const pipelines = [
519
556
  { $sort },
520
- ...this.#build_query_filter(req),
521
- ...this.#build_search_query(req),
557
+ ...this.#build_query_filter(request),
558
+ ...this.#build_search_query(request),
522
559
  ...this.#rename_id(),
523
- ...is_cursor_paging ? this.#build_cursor_paging($sort, req) : this.#build_offset_paging(req)
560
+ ...is_cursor_paging ? this.#build_cursor_paging($sort, request) : this.#build_offset_paging(request)
524
561
  ];
525
562
  const response = await collection.aggregate(pipelines).toArray();
526
563
  return {
527
564
  ...response[0],
528
- limit: this.#get_limit(req)
565
+ limit: this.#get_limit(request)
529
566
  };
530
567
  }
531
568
  }
@@ -0,0 +1,37 @@
1
+ import { ObjectId } from 'mongodb';
2
+ import type { Collection, Db, Filter } from 'mongodb';
3
+ export type MongoDoc<T> = T & {
4
+ id: string;
5
+ toJSON(): any;
6
+ };
7
+ export type DefaultsResolver<T> = (input: Partial<T>) => Partial<T>;
8
+ export declare class CollectionDef<T> {
9
+ readonly collection: string;
10
+ readonly defaults?: DefaultsResolver<T> | undefined;
11
+ readonly _type: T;
12
+ constructor(collection: string, defaults?: DefaultsResolver<T> | undefined);
13
+ }
14
+ export declare function defineCollection<T>(config: {
15
+ collection: string;
16
+ defaults?: DefaultsResolver<T>;
17
+ }): CollectionDef<T>;
18
+ export declare class MongodbCollection<T = any> {
19
+ private readonly db;
20
+ private readonly collectionName;
21
+ private readonly resolveDefaults?;
22
+ constructor(db: Db, config: CollectionDef<T>);
23
+ get collection(): Collection<any>;
24
+ private prepareInsert;
25
+ find(filter?: Filter<any>): Promise<MongoDoc<T>[]>;
26
+ findOne(filter?: string | ObjectId | Filter<any>): Promise<MongoDoc<T> | null>;
27
+ findById(id: string | ObjectId): Promise<MongoDoc<T> | null>;
28
+ create(doc: Partial<T>): Promise<MongoDoc<T>>;
29
+ insertMany(docs: Partial<T>[]): Promise<MongoDoc<T>[]>;
30
+ updateOne(filter: string | ObjectId | Filter<any>, update: any, opts?: any): Promise<import("mongodb").UpdateResult<any>>;
31
+ updateMany(filter: Filter<any>, update: any, opts?: any): Promise<import("mongodb").UpdateResult<any>>;
32
+ deleteOne(filter: string | ObjectId | Filter<any>): Promise<import("mongodb").DeleteResult>;
33
+ deleteMany(filter?: Filter<any>): Promise<import("mongodb").DeleteResult>;
34
+ countDocuments(filter?: Filter<any>): Promise<number>;
35
+ exists(filter?: string | ObjectId | Filter<any>): Promise<boolean>;
36
+ aggregate<R = any>(pipeline: any[]): Promise<R[]>;
37
+ }
@@ -0,0 +1,116 @@
1
+ import { ObjectId } from 'mongodb';
2
+ function hydrate(doc) {
3
+ if (!doc)
4
+ return null;
5
+ const oid = doc._id;
6
+ Object.defineProperty(doc, '_id', { value: oid, enumerable: false, configurable: true, writable: true });
7
+ if (Object.prototype.hasOwnProperty.call(doc, '__v')) {
8
+ Object.defineProperty(doc, '__v', { value: doc.__v, enumerable: false, configurable: true, writable: true });
9
+ }
10
+ Object.defineProperty(doc, 'id', {
11
+ value: oid ? oid.toString() : undefined,
12
+ enumerable: true,
13
+ configurable: true,
14
+ writable: true,
15
+ });
16
+ Object.defineProperty(doc, 'toJSON', {
17
+ value() {
18
+ const { _id: _omitId, __v: _omitV, ...rest } = this;
19
+ return rest;
20
+ },
21
+ enumerable: false,
22
+ configurable: true,
23
+ });
24
+ return doc;
25
+ }
26
+ function withSet(update) {
27
+ const hasOperator = Object.keys(update || {}).some((k) => k.startsWith('$'));
28
+ return hasOperator ? update : { $set: update };
29
+ }
30
+ function withTouch(update) {
31
+ const base = withSet(update);
32
+ return { ...base, $set: { ...(base.$set ?? {}), updated_at: Date.now() } };
33
+ }
34
+ function isHex24(s) {
35
+ return /^[a-f\d]{24}$/i.test(s);
36
+ }
37
+ function toFilter(filter = {}) {
38
+ if (typeof filter === 'string')
39
+ return { _id: isHex24(filter) ? ObjectId.createFromHexString(filter) : filter };
40
+ if (filter instanceof ObjectId)
41
+ return { _id: filter };
42
+ return filter;
43
+ }
44
+ export class CollectionDef {
45
+ collection;
46
+ defaults;
47
+ constructor(collection, defaults) {
48
+ this.collection = collection;
49
+ this.defaults = defaults;
50
+ }
51
+ }
52
+ export function defineCollection(config) {
53
+ return new CollectionDef(config.collection, config.defaults);
54
+ }
55
+ export class MongodbCollection {
56
+ db;
57
+ collectionName;
58
+ resolveDefaults;
59
+ constructor(db, config) {
60
+ this.db = db;
61
+ this.collectionName = config.collection;
62
+ this.resolveDefaults = config.defaults;
63
+ }
64
+ get collection() {
65
+ return this.db.collection(this.collectionName);
66
+ }
67
+ prepareInsert(input) {
68
+ const now = Date.now();
69
+ const { id: _gid, _id: _moid, ...clean } = (input ?? {});
70
+ const defaults = this.resolveDefaults ? this.resolveDefaults((input ?? {})) : {};
71
+ return { created_at: now, updated_at: now, ...defaults, ...clean };
72
+ }
73
+ async find(filter = {}) {
74
+ const docs = await this.collection.find(filter).toArray();
75
+ return docs.map((d) => hydrate(d));
76
+ }
77
+ async findOne(filter = {}) {
78
+ return hydrate(await this.collection.findOne(toFilter(filter)));
79
+ }
80
+ findById(id) {
81
+ return this.findOne(id);
82
+ }
83
+ async create(doc) {
84
+ const record = this.prepareInsert(doc);
85
+ const r = await this.collection.insertOne(record);
86
+ record._id = r.insertedId;
87
+ return hydrate(record);
88
+ }
89
+ async insertMany(docs) {
90
+ const records = docs.map((d) => this.prepareInsert(d));
91
+ const r = await this.collection.insertMany(records);
92
+ records.forEach((d, i) => (d._id = r.insertedIds[i]));
93
+ return records.map((d) => hydrate(d));
94
+ }
95
+ updateOne(filter, update, opts) {
96
+ return this.collection.updateOne(toFilter(filter), withTouch(update), opts);
97
+ }
98
+ updateMany(filter, update, opts) {
99
+ return this.collection.updateMany(filter, withTouch(update), opts);
100
+ }
101
+ deleteOne(filter) {
102
+ return this.collection.deleteOne(toFilter(filter));
103
+ }
104
+ deleteMany(filter = {}) {
105
+ return this.collection.deleteMany(filter);
106
+ }
107
+ countDocuments(filter = {}) {
108
+ return this.collection.countDocuments(filter);
109
+ }
110
+ exists(filter = {}) {
111
+ return this.collection.findOne(toFilter(filter), { projection: { _id: 1 } }).then((d) => !!d);
112
+ }
113
+ aggregate(pipeline) {
114
+ return this.collection.aggregate(pipeline).toArray();
115
+ }
116
+ }
@@ -1,3 +1,4 @@
1
1
  export * from './MongoDatasource.js';
2
2
  export * from './DataChangePayload.js';
3
3
  export * from './MongodbRealtime.js';
4
+ export * from './MongodbCollection.js';
@@ -1,3 +1,4 @@
1
1
  export * from './MongoDatasource.js';
2
2
  export * from './DataChangePayload.js';
3
3
  export * from './MongodbRealtime.js';
4
+ export * from './MongodbCollection.js';
@@ -1 +1 @@
1
- {"root":["../src/cursor.ts","../src/datachangepayload.ts","../src/mongodatasource.ts","../src/mongoquery.ts","../src/mongodbrealtime.ts","../src/smartcache.ts","../src/index.ts"],"version":"5.9.3"}
1
+ {"root":["../src/cursor.ts","../src/datachangepayload.ts","../src/mongodatasource.ts","../src/mongoquery.ts","../src/mongodbcollection.ts","../src/mongodbrealtime.ts","../src/smartcache.ts","../src/index.ts"],"version":"6.0.3"}
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "repository": {
7
7
  "url": "git@github.com:livequery/mongodb.git"
8
8
  },
9
- "version": "2.0.152",
9
+ "version": "2.0.154",
10
10
  "description": "MongoDB datasource mapping for @livequery ecosystem",
11
11
  "main": "./build/src/index.js",
12
12
  "types": "./build/src/index.d.ts",
@@ -30,8 +30,8 @@
30
30
  },
31
31
  "scripts": {
32
32
  "test": "bun test",
33
- "build": "rm -rf build; tsc -b .",
34
- "deploy": "rm -rf build && yarn build; git add .; git commit -m \"Update\"; git push origin master; npm publish --access public"
33
+ "build": "rm -rf build && tsc -b .",
34
+ "deploy": "rm -rf build && yarn build && git add . && git commit -m \"Update\" && git push origin main && npm publish --access public"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "mongodb": "^6.20.0"