@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 +151 -66
- package/build/src/Cursor.d.ts +1 -1
- package/build/src/MongoDatasource.d.ts +21 -0
- package/build/src/MongoDatasource.js +15 -8
- package/build/src/MongoQuery.js +78 -41
- package/build/src/MongodbCollection.d.ts +37 -0
- package/build/src/MongodbCollection.js +116 -0
- package/build/src/index.d.ts +1 -0
- package/build/src/index.js +1 -0
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
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
|
-
|
|
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
|
|
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 './
|
|
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`
|
|
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<
|
|
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
|
-
-
|
|
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
|
-
|
|
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`:
|
|
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
|
-
|
|
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`:
|
|
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
|
|
234
|
-
- Offset paging
|
|
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:
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
637
|
+
query: { ':limit': 10 },
|
|
553
638
|
},
|
|
554
639
|
{
|
|
555
640
|
collection: 'products',
|
package/build/src/Cursor.d.ts
CHANGED
|
@@ -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
|
-
...
|
|
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
|
-
|
|
221
|
+
const { id: _id, _id: _rawId, ...cleanBody } = body;
|
|
222
|
+
return { $set: cleanBody };
|
|
216
223
|
}
|
|
217
224
|
#convert(obj, fields) {
|
|
218
225
|
return {
|
package/build/src/MongoQuery.js
CHANGED
|
@@ -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
|
|
242
|
-
|
|
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
|
-
...
|
|
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
|
|
281
|
-
if (
|
|
282
|
-
clauses.push({ $or:
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
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
|
-
...
|
|
497
|
-
...
|
|
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 =
|
|
517
|
-
const $sort = this.#get_sorter(
|
|
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(
|
|
521
|
-
...this.#build_search_query(
|
|
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,
|
|
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(
|
|
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
|
+
}
|
package/build/src/index.d.ts
CHANGED
package/build/src/index.js
CHANGED
|
@@ -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":"
|
|
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.
|
|
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
|
|
34
|
-
"deploy": "rm -rf build && yarn build
|
|
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"
|