@livequery/mongodb 2.0.151 → 2.0.153

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
@@ -25,6 +25,7 @@ For local development in this workspace, `@livequery/core` is installed as a dev
25
25
  export * from './MongoDatasource.js'
26
26
  export * from './DataChangePayload.js'
27
27
  export * from './MongodbRealtime.js'
28
+ export * from './MongodbCollection.js'
28
29
  export * from './types.js'
29
30
  ```
30
31
 
@@ -386,6 +387,100 @@ An inserted `{ _id: 'post1', userId: 'user1', title: 'Hello' }` emits:
386
387
  }
387
388
  ```
388
389
 
390
+ ### `MongodbCollection`
391
+
392
+ 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)`.
393
+
394
+ 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.
395
+
396
+ ```ts
397
+ class MongodbCollection<T = any>
398
+ ```
399
+
400
+ Responsibilities:
401
+
402
+ - Wrap one collection, resolved lazily from a provided `Db`.
403
+ - Return documents with an enumerable `id: string` and a hidden `_id`.
404
+ - Apply a defaults resolver plus `created_at` / `updated_at` on insert.
405
+ - Bump `updated_at` on every update.
406
+ - Accept a `string` id, an `ObjectId`, or a filter object for single-document operations.
407
+
408
+ #### `constructor(db, collectionName, resolveDefaults?)`
409
+
410
+ Parameters:
411
+
412
+ - `db: Db`: a connected `mongodb` `Db`. It is passed in explicitly (no hidden module singleton), so one class works across databases and connections.
413
+ - `collectionName: string`: collection name. The handle is resolved lazily via `db.collection(name)`.
414
+ - `resolveDefaults?: (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.
415
+
416
+ Example:
417
+
418
+ ```ts
419
+ import { MongoClient } from 'mongodb'
420
+ import { MongodbCollection } from '@livequery/mongodb'
421
+
422
+ const client = new MongoClient(process.env.MONGO_URL!)
423
+ await client.connect()
424
+ const db = client.db('main')
425
+
426
+ type Order = { video_id: string; amount: number; started: boolean; running: boolean }
427
+
428
+ const Orders = new MongodbCollection<Order>(db, 'orders', () => ({ started: false, running: true }))
429
+ const Videos = new MongodbCollection<Video>(db, 'videos') // no defaults
430
+ ```
431
+
432
+ #### Document shape: `MongoDoc<T>`
433
+
434
+ ```ts
435
+ type MongoDoc<T> = T & { id: string; toJSON(): any }
436
+ ```
437
+
438
+ Returned documents are hydrated:
439
+
440
+ - `id` is an enumerable string (`_id.toString()`), so it appears in `JSON.stringify`, JSON responses, and `{ ...doc }`.
441
+ - `_id` is non-enumerable, so it never leaks into output, yet `doc._id` is still readable as an `ObjectId` internally.
442
+ - `toJSON()` returns the document without `_id` and `__v`.
443
+
444
+ #### Methods
445
+
446
+ | Method | Description |
447
+ | --- | --- |
448
+ | `find(filter?)` | Returns hydrated documents. |
449
+ | `findOne(filter?)` | `filter` may be a `string` id, an `ObjectId`, or a filter object. |
450
+ | `findById(id)` | Shorthand for `findOne` with a `string` id or `ObjectId`. |
451
+ | `create(doc)` | Inserts one document; applies defaults + timestamps; strips any incoming `id` / `_id`. |
452
+ | `insertMany(docs)` | Inserts many documents with the same preparation as `create`. |
453
+ | `updateOne(filter, update, opts?)` | Wraps plain updates in `$set` and bumps `updated_at`; `filter` accepts string id / ObjectId / object. |
454
+ | `updateMany(filter, update, opts?)` | Same update handling for many documents. |
455
+ | `deleteOne(filter)` | Deletes one; `filter` accepts string id / ObjectId / object. |
456
+ | `deleteMany(filter?)` | Deletes many documents. |
457
+ | `countDocuments(filter?)` | Counts matching documents. |
458
+ | `exists(filter?)` | `true` when at least one document matches. |
459
+ | `aggregate(pipeline)` | Runs an aggregation pipeline and returns the array. |
460
+ | `collection` | Getter for the raw native `Collection` (escape hatch). |
461
+
462
+ Behavior:
463
+
464
+ - Filter normalization: a 24-hex `string` becomes `{ _id: ObjectId }`; an `ObjectId` becomes `{ _id }`; any other string or object is used unchanged.
465
+ - 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.
466
+ - Inserts never persist an incoming `id` or `_id`.
467
+
468
+ Example:
469
+
470
+ ```ts
471
+ const order = await Orders.create({ video_id: 'v1', amount: 50 })
472
+ order.id // '507f1f77bcf86cd799439011'
473
+ order._id // ObjectId — still readable internally
474
+ JSON.stringify(order) // contains "id", not "_id"
475
+
476
+ await Orders.findOne('507f1f77bcf86cd799439011') // by string id
477
+ await Orders.findById(order._id) // by ObjectId
478
+ await Orders.findOne({ video_id: 'v1' }) // by filter
479
+
480
+ await Orders.updateOne(order.id, { amount: 80 }) // wrapped in $set, bumps updated_at
481
+ await Orders.updateOne(order.id, { $inc: { amount: 5 } }) // operator preserved, still bumps updated_at
482
+ ```
483
+
389
484
  ### `DataChangePayload<T>`
390
485
 
391
486
  Type-only realtime/change payload contract.
@@ -155,9 +155,10 @@ 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 {
@@ -212,7 +213,8 @@ export class MongoDatasource extends Subject {
212
213
  #update(body) {
213
214
  if (!body || Object.keys(body).some(key => key.startsWith('$')))
214
215
  return body;
215
- return { $set: body };
216
+ const { id: _id, _id: _rawId, ...cleanBody } = body;
217
+ return { $set: cleanBody };
216
218
  }
217
219
  #convert(obj, fields) {
218
220
  return {
@@ -490,11 +490,12 @@ export class MongoQuery {
490
490
  }
491
491
  static async query(req, collection) {
492
492
  if (!req.is_collection) {
493
+ const { id, _id: _rawId, ...keysWithoutId } = req.keys;
493
494
  const aggregates = [
494
495
  {
495
496
  $match: {
496
- ...req.keys,
497
- ...req.keys.id ? { id: undefined, _id: this.#objectId('id', req.keys.id) } : {}
497
+ ...keysWithoutId,
498
+ ...id ? { _id: this.#objectId('id', id) } : {}
498
499
  }
499
500
  },
500
501
  ...this.#rename_id(),
@@ -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>;
11
+ readonly _type: T;
12
+ constructor(collection: string, defaults?: DefaultsResolver<T>);
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,113 @@
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
+ Object.defineProperty(doc, 'id', {
8
+ value: oid ? oid.toString() : undefined,
9
+ enumerable: true,
10
+ configurable: true,
11
+ writable: true,
12
+ });
13
+ Object.defineProperty(doc, 'toJSON', {
14
+ value() {
15
+ const { _id: _omitId, __v: _omitV, ...rest } = this;
16
+ return rest;
17
+ },
18
+ enumerable: false,
19
+ configurable: true,
20
+ });
21
+ return doc;
22
+ }
23
+ function withSet(update) {
24
+ const hasOperator = Object.keys(update || {}).some((k) => k.startsWith('$'));
25
+ return hasOperator ? update : { $set: update };
26
+ }
27
+ function withTouch(update) {
28
+ const base = withSet(update);
29
+ return { ...base, $set: { ...(base.$set ?? {}), updated_at: Date.now() } };
30
+ }
31
+ function isHex24(s) {
32
+ return /^[a-f\d]{24}$/i.test(s);
33
+ }
34
+ function toFilter(filter = {}) {
35
+ if (typeof filter === 'string')
36
+ return { _id: isHex24(filter) ? ObjectId.createFromHexString(filter) : filter };
37
+ if (filter instanceof ObjectId)
38
+ return { _id: filter };
39
+ return filter;
40
+ }
41
+ export class CollectionDef {
42
+ collection;
43
+ defaults;
44
+ constructor(collection, defaults) {
45
+ this.collection = collection;
46
+ this.defaults = defaults;
47
+ }
48
+ }
49
+ export function defineCollection(config) {
50
+ return new CollectionDef(config.collection, config.defaults);
51
+ }
52
+ export class MongodbCollection {
53
+ db;
54
+ collectionName;
55
+ resolveDefaults;
56
+ constructor(db, config) {
57
+ this.db = db;
58
+ this.collectionName = config.collection;
59
+ this.resolveDefaults = config.defaults;
60
+ }
61
+ get collection() {
62
+ return this.db.collection(this.collectionName);
63
+ }
64
+ prepareInsert(input) {
65
+ const now = Date.now();
66
+ const { id: _gid, _id: _moid, ...clean } = (input ?? {});
67
+ const defaults = this.resolveDefaults ? this.resolveDefaults((input ?? {})) : {};
68
+ return { created_at: now, updated_at: now, ...defaults, ...clean };
69
+ }
70
+ async find(filter = {}) {
71
+ const docs = await this.collection.find(filter).toArray();
72
+ return docs.map((d) => hydrate(d));
73
+ }
74
+ async findOne(filter = {}) {
75
+ return hydrate(await this.collection.findOne(toFilter(filter)));
76
+ }
77
+ findById(id) {
78
+ return this.findOne(id);
79
+ }
80
+ async create(doc) {
81
+ const record = this.prepareInsert(doc);
82
+ const r = await this.collection.insertOne(record);
83
+ record._id = r.insertedId;
84
+ return hydrate(record);
85
+ }
86
+ async insertMany(docs) {
87
+ const records = docs.map((d) => this.prepareInsert(d));
88
+ const r = await this.collection.insertMany(records);
89
+ records.forEach((d, i) => (d._id = r.insertedIds[i]));
90
+ return records.map((d) => hydrate(d));
91
+ }
92
+ updateOne(filter, update, opts) {
93
+ return this.collection.updateOne(toFilter(filter), withTouch(update), opts);
94
+ }
95
+ updateMany(filter, update, opts) {
96
+ return this.collection.updateMany(filter, withTouch(update), opts);
97
+ }
98
+ deleteOne(filter) {
99
+ return this.collection.deleteOne(toFilter(filter));
100
+ }
101
+ deleteMany(filter = {}) {
102
+ return this.collection.deleteMany(filter);
103
+ }
104
+ countDocuments(filter = {}) {
105
+ return this.collection.countDocuments(filter);
106
+ }
107
+ exists(filter = {}) {
108
+ return this.collection.findOne(toFilter(filter), { projection: { _id: 1 } }).then((d) => !!d);
109
+ }
110
+ aggregate(pipeline) {
111
+ return this.collection.aggregate(pipeline).toArray();
112
+ }
113
+ }
@@ -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":"5.9.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.151",
9
+ "version": "2.0.153",
10
10
  "description": "MongoDB datasource mapping for @livequery ecosystem",
11
11
  "main": "./build/src/index.js",
12
12
  "types": "./build/src/index.d.ts",
@@ -22,7 +22,7 @@
22
22
  }
23
23
  },
24
24
  "devDependencies": {
25
- "@livequery/core": "^2.0.151",
25
+ "@livequery/core": "^2.0.152",
26
26
  "@types/node": "^18.11.9",
27
27
  "mongodb": "^6.20.0",
28
28
  "rxjs": "*",