@livequery/mongodb 2.0.0 → 2.0.145
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 +74 -1
- package/build/src/MongoDatasource.d.ts +6 -2
- package/build/src/MongoDatasource.js +17 -3
- package/build/src/MongoQuery.js +69 -12
- package/build/src/MongodbRealtime.d.ts +23 -0
- package/build/src/MongodbRealtime.js +221 -0
- package/build/src/SmartCache.d.ts +1 -1
- package/build/src/SmartCache.js +2 -2
- package/build/src/index.d.ts +1 -0
- package/build/src/index.js +1 -0
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,6 +24,7 @@ For local development in this workspace, `@livequery/core` is installed as a dev
|
|
|
24
24
|
```ts
|
|
25
25
|
export * from './MongoDatasource.js'
|
|
26
26
|
export * from './DataChangePayload.js'
|
|
27
|
+
export * from './MongodbRealtime.js'
|
|
27
28
|
export * from './types.js'
|
|
28
29
|
```
|
|
29
30
|
|
|
@@ -305,6 +306,76 @@ Returns:
|
|
|
305
306
|
|
|
306
307
|
The collection cache key includes connection, database, and collection name to avoid reusing collection handles across tenants or connections.
|
|
307
308
|
|
|
309
|
+
### `MongodbRealtime`
|
|
310
|
+
|
|
311
|
+
MongoDB change stream watcher for realtime Livequery updates. This replaces the need to use the separate `@livequery/mongodb-mapper` package in native MongoDB projects.
|
|
312
|
+
|
|
313
|
+
```ts
|
|
314
|
+
import { WebsocketGateway } from '@livequery/core'
|
|
315
|
+
import { MongoDatasource, MongodbRealtime } from '@livequery/mongodb'
|
|
316
|
+
|
|
317
|
+
const routes = [
|
|
318
|
+
{
|
|
319
|
+
method: 'GET',
|
|
320
|
+
path: '/products',
|
|
321
|
+
collection: 'products',
|
|
322
|
+
realtime: true,
|
|
323
|
+
},
|
|
324
|
+
]
|
|
325
|
+
|
|
326
|
+
const datasource = new MongoDatasource({
|
|
327
|
+
connections: { default: client },
|
|
328
|
+
databases: ['main'],
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
await datasource.init(routes)
|
|
332
|
+
|
|
333
|
+
const websocketGateway = new WebsocketGateway(server)
|
|
334
|
+
|
|
335
|
+
new MongodbRealtime()
|
|
336
|
+
.watch(datasource.config, routes)
|
|
337
|
+
.subscribe(websocketGateway)
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
Realtime route requirements:
|
|
341
|
+
|
|
342
|
+
- `method` must be `GET` or legacy method `0`.
|
|
343
|
+
- `realtime` must be `true`.
|
|
344
|
+
- `collection` must be a static string. Dynamic collection, database, or connection resolver functions are skipped because database watchers must be known up front.
|
|
345
|
+
- When watching a `MongoClient`, `db` or `config.databases` decides which database names to watch. When watching a `Db`, that database is used directly.
|
|
346
|
+
|
|
347
|
+
By default, `MongodbRealtime` enables MongoDB pre/post images with `collMod` and watches with `fullDocument: 'updateLookup'` and `fullDocumentBeforeChange: 'whenAvailable'`.
|
|
348
|
+
|
|
349
|
+
Disable the `collMod` call when your deployment manages pre/post images separately:
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
new MongodbRealtime({ enablePreAndPostImages: false })
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
For nested collection refs, use `refFields` to map route params to document fields:
|
|
356
|
+
|
|
357
|
+
```ts
|
|
358
|
+
{
|
|
359
|
+
method: 'GET',
|
|
360
|
+
path: '/users/:id/posts',
|
|
361
|
+
collection: 'posts',
|
|
362
|
+
realtime: true,
|
|
363
|
+
refFields: {
|
|
364
|
+
id: 'userId',
|
|
365
|
+
},
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
An inserted `{ _id: 'post1', userId: 'user1', title: 'Hello' }` emits:
|
|
370
|
+
|
|
371
|
+
```ts
|
|
372
|
+
{
|
|
373
|
+
ref: 'users/user1/posts',
|
|
374
|
+
type: 'added',
|
|
375
|
+
data: { id: 'post1', userId: 'user1', title: 'Hello' },
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
308
379
|
### `DataChangePayload<T>`
|
|
309
380
|
|
|
310
381
|
Type-only realtime/change payload contract.
|
|
@@ -354,16 +425,18 @@ type RouteOptions = {
|
|
|
354
425
|
collection: string | ((req: LivequeryRequest) => Promise<string> | string)
|
|
355
426
|
db?: string | ((req: LivequeryRequest) => Promise<string> | string)
|
|
356
427
|
connection?: string | ((req: LivequeryRequest) => Promise<string> | string)
|
|
428
|
+
refFields?: Record<string, string | { field: string, array?: boolean }>
|
|
357
429
|
objectIdFields?: string[]
|
|
358
430
|
}
|
|
359
431
|
```
|
|
360
432
|
|
|
361
433
|
Fields:
|
|
362
434
|
|
|
363
|
-
- `realtime`:
|
|
435
|
+
- `realtime`: marks a static collection route for `MongodbRealtime.watch()`. Query execution itself is unchanged.
|
|
364
436
|
- `collection`: required collection name or resolver function.
|
|
365
437
|
- `db`: optional database name or resolver function.
|
|
366
438
|
- `connection`: optional connection name or resolver function.
|
|
439
|
+
- `refFields`: optional mapping from nested route params to document fields for realtime ref formatting.
|
|
367
440
|
- `objectIdFields`: top-level request fields that should be converted from valid string ids to `ObjectId`.
|
|
368
441
|
|
|
369
442
|
Use function values when tenant, database, or collection depends on request keys.
|
|
@@ -28,6 +28,10 @@ export type RouteOptions = {
|
|
|
28
28
|
collection: string | ((req: LivequeryRequest) => Promise<string> | string);
|
|
29
29
|
db?: string | ((req: LivequeryRequest) => Promise<string> | string);
|
|
30
30
|
connection?: string | ((req: LivequeryRequest) => Promise<string> | string);
|
|
31
|
+
refFields?: Record<string, string | {
|
|
32
|
+
field: string;
|
|
33
|
+
array?: boolean;
|
|
34
|
+
}>;
|
|
31
35
|
objectIdFields?: string[];
|
|
32
36
|
};
|
|
33
37
|
export declare class MongoDatasource extends Subject<WebsocketSyncPayload<LivequeryBaseEntity>> implements LivequeryDatasource<MongoDatasourceConfig, RouteOptions>, CoreLivequeryDatasource<RouteOptions> {
|
|
@@ -39,7 +43,7 @@ export declare class MongoDatasource extends Subject<WebsocketSyncPayload<Livequ
|
|
|
39
43
|
init(routes: Array<LivequeryDatasourceInitConfig<RouteOptions>>): Promise<void>;
|
|
40
44
|
init(config: MongoDatasourceConfig, routes: Array<LegacyRouteConfig<RouteOptions>>): Promise<void>;
|
|
41
45
|
handle(ctx: LivequeryContext): Promise<{}>;
|
|
42
|
-
query(req: LivequeryRequest, options: RouteOptions): Promise<
|
|
46
|
+
query(req: LivequeryRequest, options: RouteOptions): Promise<{
|
|
43
47
|
items: any[];
|
|
44
48
|
summary: any;
|
|
45
49
|
cursor: {
|
|
@@ -62,6 +66,6 @@ export declare class MongoDatasource extends Subject<WebsocketSyncPayload<Livequ
|
|
|
62
66
|
};
|
|
63
67
|
} | {
|
|
64
68
|
item: any;
|
|
65
|
-
}
|
|
69
|
+
}>;
|
|
66
70
|
}
|
|
67
71
|
export {};
|
|
@@ -186,13 +186,27 @@ export class MongoDatasource extends Subject {
|
|
|
186
186
|
};
|
|
187
187
|
}
|
|
188
188
|
async #put(req, collection) {
|
|
189
|
-
|
|
189
|
+
await collection.updateOne(this.#keys(req), this.#update(req.body));
|
|
190
|
+
return { item: this.#writtenItem(req) };
|
|
190
191
|
}
|
|
191
192
|
async #patch(req, collection) {
|
|
192
|
-
|
|
193
|
+
await collection.updateOne(this.#keys(req), this.#update(req.body));
|
|
194
|
+
return { item: this.#writtenItem(req) };
|
|
193
195
|
}
|
|
194
196
|
async #del(req, collection) {
|
|
195
|
-
|
|
197
|
+
await collection.deleteOne(this.#keys(req));
|
|
198
|
+
return { item: this.#writtenItem(req) };
|
|
199
|
+
}
|
|
200
|
+
#writtenItem(req) {
|
|
201
|
+
const keys = req.keys || {};
|
|
202
|
+
const isPlainBody = req.body && typeof req.body === 'object'
|
|
203
|
+
&& !Object.keys(req.body).some(k => k.startsWith('$'));
|
|
204
|
+
const id = keys.id ?? req.doc_id;
|
|
205
|
+
return {
|
|
206
|
+
...keys,
|
|
207
|
+
...isPlainBody ? req.body : {},
|
|
208
|
+
...id ? { id } : {}
|
|
209
|
+
};
|
|
196
210
|
}
|
|
197
211
|
#keys(req) {
|
|
198
212
|
return Object.entries(req.keys).reduce((p, [k, c]) => {
|
package/build/src/MongoQuery.js
CHANGED
|
@@ -91,6 +91,9 @@ export class MongoQuery {
|
|
|
91
91
|
}
|
|
92
92
|
return stack.pop();
|
|
93
93
|
}
|
|
94
|
+
static #escape_regex(value) {
|
|
95
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
96
|
+
}
|
|
94
97
|
static #parse_array(value) {
|
|
95
98
|
if (Array.isArray(value))
|
|
96
99
|
return value;
|
|
@@ -201,19 +204,25 @@ export class MongoQuery {
|
|
|
201
204
|
};
|
|
202
205
|
}
|
|
203
206
|
static #parse_conditions(filters) {
|
|
207
|
+
const $match = this.#build_match(filters);
|
|
208
|
+
return Object.keys($match).length > 0 ? [{ $match }] : [];
|
|
209
|
+
}
|
|
210
|
+
static #build_match(filters) {
|
|
204
211
|
if (!filters)
|
|
205
|
-
return
|
|
212
|
+
return {};
|
|
206
213
|
const { ':and': and, ':or': or, ':not': not, ...rest } = filters;
|
|
207
214
|
const $or = Object.entries(rest).filter(([k]) => k.endsWith(':like')).map(([k, v]) => {
|
|
208
215
|
const key = k.split(':like')[0];
|
|
209
216
|
const value = `${v}`;
|
|
210
217
|
return {
|
|
211
|
-
[key]: { $regex: value }
|
|
218
|
+
[key]: { $regex: this.#escape_regex(value), $options: 'i' }
|
|
212
219
|
};
|
|
213
220
|
});
|
|
214
|
-
const
|
|
221
|
+
const fields = Object.entries(rest).reduce((p, [k, value]) => {
|
|
215
222
|
if (k.startsWith('::'))
|
|
216
223
|
return p;
|
|
224
|
+
if (k.endsWith(':like'))
|
|
225
|
+
return p;
|
|
217
226
|
const [key, expression] = k.split(':');
|
|
218
227
|
const map = {
|
|
219
228
|
eq: () => ({ $eq: value }),
|
|
@@ -245,13 +254,28 @@ export class MongoQuery {
|
|
|
245
254
|
...fn()
|
|
246
255
|
}
|
|
247
256
|
};
|
|
248
|
-
},
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
257
|
+
}, {});
|
|
258
|
+
const clauses = [];
|
|
259
|
+
if (Object.keys(fields).length > 0)
|
|
260
|
+
clauses.push(fields);
|
|
261
|
+
if ($or.length > 0)
|
|
262
|
+
clauses.push({ $or });
|
|
263
|
+
const andMatch = and ? this.#build_match(and) : {};
|
|
264
|
+
if (Object.keys(andMatch).length > 0)
|
|
265
|
+
clauses.push(andMatch);
|
|
266
|
+
const orMatch = or ? this.#build_match(or) : {};
|
|
267
|
+
if (Object.keys(orMatch).length > 0) {
|
|
268
|
+
clauses.push({ $or: Object.entries(orMatch).map(([k, v]) => ({ [k]: v })) });
|
|
269
|
+
}
|
|
270
|
+
const notMatch = not ? this.#build_match(not) : {};
|
|
271
|
+
if (Object.keys(notMatch).length > 0) {
|
|
272
|
+
clauses.push({ $nor: Object.entries(notMatch).map(([k, v]) => ({ [k]: v })) });
|
|
273
|
+
}
|
|
274
|
+
if (clauses.length === 0)
|
|
275
|
+
return {};
|
|
276
|
+
if (clauses.length === 1)
|
|
277
|
+
return clauses[0];
|
|
278
|
+
return { $and: clauses };
|
|
255
279
|
}
|
|
256
280
|
static #build_search_query(req) {
|
|
257
281
|
const search = req.options[":search"];
|
|
@@ -384,7 +408,40 @@ export class MongoQuery {
|
|
|
384
408
|
];
|
|
385
409
|
}
|
|
386
410
|
static #build_offset_paging(req) {
|
|
387
|
-
|
|
411
|
+
const limit = this.#get_limit(req);
|
|
412
|
+
const page = Math.max(1, Math.floor(Number(req.options[':page'])) || 1);
|
|
413
|
+
const skip = (page - 1) * limit;
|
|
414
|
+
const { pipelines, summary } = this.#parse_summary(req);
|
|
415
|
+
return [
|
|
416
|
+
{
|
|
417
|
+
$facet: {
|
|
418
|
+
...pipelines,
|
|
419
|
+
items: [{ $skip: skip }, { $limit: limit }],
|
|
420
|
+
total: [{ $count: 'count' }],
|
|
421
|
+
}
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
$project: {
|
|
425
|
+
summary,
|
|
426
|
+
items: 1,
|
|
427
|
+
total: { $ifNull: [{ $arrayElemAt: ['$total.count', 0] }, 0] },
|
|
428
|
+
}
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
$project: {
|
|
432
|
+
summary: 1,
|
|
433
|
+
items: 1,
|
|
434
|
+
has: {
|
|
435
|
+
prev: { $gt: [skip, 0] },
|
|
436
|
+
next: { $gt: ['$total', skip + limit] },
|
|
437
|
+
},
|
|
438
|
+
count: {
|
|
439
|
+
prev: { $literal: skip },
|
|
440
|
+
next: { $max: [{ $subtract: ['$total', skip + limit] }, 0] },
|
|
441
|
+
},
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
];
|
|
388
445
|
}
|
|
389
446
|
static #build_query_filter(req) {
|
|
390
447
|
const { ":after": after, ":before": before, ':around': around, ":limit": _limit, ":page": _page, ":search": search, ...rest } = req.options;
|
|
@@ -436,7 +493,7 @@ export class MongoQuery {
|
|
|
436
493
|
summary: {}
|
|
437
494
|
};
|
|
438
495
|
}
|
|
439
|
-
const is_cursor_paging = req.options[':after'] || req.options[':before'] || req.options[':around'] || !req.options['page'];
|
|
496
|
+
const is_cursor_paging = req.options[':after'] || req.options[':before'] || req.options[':around'] || !req.options[':page'];
|
|
440
497
|
const $sort = this.#get_sorter(req);
|
|
441
498
|
const pipelines = [
|
|
442
499
|
{ $sort },
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Observable } from 'rxjs';
|
|
2
|
+
import type { WebsocketSyncPayload } from './types.js';
|
|
3
|
+
import type { MongoDatasourceConfig, RouteOptions } from './MongoDatasource.js';
|
|
4
|
+
export type MongoRealtimeChangeType = 'added' | 'modified' | 'removed';
|
|
5
|
+
export type MongoRealtimeRefField = string | {
|
|
6
|
+
field: string;
|
|
7
|
+
array?: boolean;
|
|
8
|
+
};
|
|
9
|
+
export type MongoRealtimeOptions = {
|
|
10
|
+
enablePreAndPostImages?: boolean;
|
|
11
|
+
};
|
|
12
|
+
export type MongoRealtimeRoute = {
|
|
13
|
+
path: string;
|
|
14
|
+
method: string | number;
|
|
15
|
+
options?: RouteOptions;
|
|
16
|
+
config?: RouteOptions;
|
|
17
|
+
} & Partial<RouteOptions>;
|
|
18
|
+
export declare class MongodbRealtime {
|
|
19
|
+
#private;
|
|
20
|
+
private options;
|
|
21
|
+
constructor(options?: MongoRealtimeOptions);
|
|
22
|
+
watch(config: MongoDatasourceConfig, routes: MongoRealtimeRoute[]): Observable<WebsocketSyncPayload>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { EMPTY, Observable, from, map, mergeAll, mergeMap, retry } from 'rxjs';
|
|
2
|
+
const changeTypes = {
|
|
3
|
+
insert: 'added',
|
|
4
|
+
update: 'modified',
|
|
5
|
+
replace: 'modified',
|
|
6
|
+
delete: 'removed',
|
|
7
|
+
};
|
|
8
|
+
export class MongodbRealtime {
|
|
9
|
+
options;
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
this.options = options;
|
|
12
|
+
}
|
|
13
|
+
#reformatId(obj) {
|
|
14
|
+
if (!obj)
|
|
15
|
+
return undefined;
|
|
16
|
+
const { _id, __v, id, ...rest } = obj;
|
|
17
|
+
return {
|
|
18
|
+
id: _id ? String(_id) : id || '#',
|
|
19
|
+
...Object.entries(rest).reduce((acc, [key, value]) => {
|
|
20
|
+
if (key.startsWith('_'))
|
|
21
|
+
return acc;
|
|
22
|
+
return { ...acc, [key]: value };
|
|
23
|
+
}, {}),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
#isGetRoute(method) {
|
|
27
|
+
return method === 0 || String(method).toUpperCase() === 'GET';
|
|
28
|
+
}
|
|
29
|
+
#getRouteOptions(route) {
|
|
30
|
+
if (route.options)
|
|
31
|
+
return route.options;
|
|
32
|
+
if (route.config)
|
|
33
|
+
return route.config;
|
|
34
|
+
const { method: _method, path: _path, options: _options, config: _config, ...options } = route;
|
|
35
|
+
return options.collection ? options : undefined;
|
|
36
|
+
}
|
|
37
|
+
#isDb(connection) {
|
|
38
|
+
return typeof connection.collection == 'function';
|
|
39
|
+
}
|
|
40
|
+
#collection(source) {
|
|
41
|
+
if (this.#isDb(source.connection))
|
|
42
|
+
return source.connection.collection(source.collection);
|
|
43
|
+
return source.connection.db(source.dbName).collection(source.collection);
|
|
44
|
+
}
|
|
45
|
+
#watchSources(config, routes) {
|
|
46
|
+
const sources = new Map();
|
|
47
|
+
for (const route of routes) {
|
|
48
|
+
const options = this.#getRouteOptions(route);
|
|
49
|
+
if (!options?.realtime || !this.#isGetRoute(route.method))
|
|
50
|
+
continue;
|
|
51
|
+
if (typeof options.collection != 'string')
|
|
52
|
+
continue;
|
|
53
|
+
if (typeof options.connection == 'function' || typeof options.db == 'function')
|
|
54
|
+
continue;
|
|
55
|
+
const connectionName = options.connection || Object.keys(config.connections)[0] || 'default';
|
|
56
|
+
const connection = config.connections[connectionName];
|
|
57
|
+
if (!connection)
|
|
58
|
+
continue;
|
|
59
|
+
const dbNames = this.#isDb(connection)
|
|
60
|
+
? [undefined]
|
|
61
|
+
: options.db ? [options.db] : config.databases || ['main'];
|
|
62
|
+
for (const dbName of dbNames) {
|
|
63
|
+
const key = `${connectionName}|${dbName || ''}|${options.collection}`;
|
|
64
|
+
sources.set(key, { connection, dbName, collection: options.collection });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return [...sources.values()];
|
|
68
|
+
}
|
|
69
|
+
#listenRawChanges(config, routes) {
|
|
70
|
+
const sources = this.#watchSources(config, routes);
|
|
71
|
+
if (sources.length === 0)
|
|
72
|
+
return EMPTY;
|
|
73
|
+
return from(sources).pipe(mergeMap(async (source) => {
|
|
74
|
+
const collection = this.#collection(source);
|
|
75
|
+
if (this.options.enablePreAndPostImages !== false) {
|
|
76
|
+
await collection.db.command({
|
|
77
|
+
collMod: source.collection,
|
|
78
|
+
changeStreamPreAndPostImages: { enabled: true },
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return new Observable(observer => {
|
|
82
|
+
const stream = collection.watch([], {
|
|
83
|
+
fullDocument: 'updateLookup',
|
|
84
|
+
fullDocumentBeforeChange: 'whenAvailable',
|
|
85
|
+
});
|
|
86
|
+
stream
|
|
87
|
+
.on('error', error => observer.error(error))
|
|
88
|
+
.on('change', (change) => {
|
|
89
|
+
const type = changeTypes[change.operationType];
|
|
90
|
+
if (!type)
|
|
91
|
+
return;
|
|
92
|
+
const fields = new Set(type == 'modified'
|
|
93
|
+
? [
|
|
94
|
+
...Object.keys(change.updateDescription?.updatedFields || {}).map(field => field.split('.')[0]),
|
|
95
|
+
...(change.updateDescription?.removedFields || []).map(field => field.split('.')[0]),
|
|
96
|
+
].filter(field => !field.startsWith('_'))
|
|
97
|
+
: []);
|
|
98
|
+
observer.next({
|
|
99
|
+
table: change.ns.coll,
|
|
100
|
+
type,
|
|
101
|
+
new_data: this.#reformatId(change.fullDocument),
|
|
102
|
+
old_data: this.#reformatId(change.fullDocumentBeforeChange),
|
|
103
|
+
fields,
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
return () => {
|
|
107
|
+
void stream.close();
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
}), mergeMap(stream => stream), retry());
|
|
111
|
+
}
|
|
112
|
+
#routeRefMetadata(route) {
|
|
113
|
+
const options = this.#getRouteOptions(route);
|
|
114
|
+
if (!options?.realtime || !this.#isGetRoute(route.method))
|
|
115
|
+
return;
|
|
116
|
+
if (typeof options.collection != 'string')
|
|
117
|
+
return;
|
|
118
|
+
const segments = route.path.split('/').filter(Boolean);
|
|
119
|
+
const collectionSegments = segments.length % 2 === 0 ? segments.slice(0, -1) : segments;
|
|
120
|
+
const ref = collectionSegments.map(segment => segment.startsWith(':') ? '' : segment).filter(Boolean).join('/');
|
|
121
|
+
if (!ref)
|
|
122
|
+
return;
|
|
123
|
+
const metadata = collectionSegments
|
|
124
|
+
.map((collection, index) => {
|
|
125
|
+
if (index % 2 === 1)
|
|
126
|
+
return [];
|
|
127
|
+
const param = collectionSegments[index + 1];
|
|
128
|
+
if (!param?.startsWith(':'))
|
|
129
|
+
return [{ collection }];
|
|
130
|
+
const paramName = param.slice(1);
|
|
131
|
+
const mapped = options.refFields?.[paramName] || paramName;
|
|
132
|
+
const field = typeof mapped == 'string' ? mapped : mapped.field;
|
|
133
|
+
const array = typeof mapped == 'string' ? undefined : mapped.array;
|
|
134
|
+
return [{ collection, field: field == 'id' ? '_id' : field, array }];
|
|
135
|
+
})
|
|
136
|
+
.flat();
|
|
137
|
+
return [ref, metadata];
|
|
138
|
+
}
|
|
139
|
+
#paths(routes) {
|
|
140
|
+
return routes.reduce((paths, route) => {
|
|
141
|
+
const options = this.#getRouteOptions(route);
|
|
142
|
+
const entry = this.#routeRefMetadata(route);
|
|
143
|
+
if (!options || !entry || typeof options.collection != 'string')
|
|
144
|
+
return paths;
|
|
145
|
+
const [ref, metadata] = entry;
|
|
146
|
+
const refs = paths.get(options.collection) || new Map();
|
|
147
|
+
refs.set(ref, metadata);
|
|
148
|
+
paths.set(options.collection, refs);
|
|
149
|
+
return paths;
|
|
150
|
+
}, new Map());
|
|
151
|
+
}
|
|
152
|
+
#format(paths, event) {
|
|
153
|
+
const refs = paths.get(event.table);
|
|
154
|
+
if (!refs)
|
|
155
|
+
return [];
|
|
156
|
+
const merged = {
|
|
157
|
+
...event.old_data || {},
|
|
158
|
+
...event.new_data || {},
|
|
159
|
+
};
|
|
160
|
+
const changes = Object.keys(merged)
|
|
161
|
+
.filter(key => event.fields.has(key))
|
|
162
|
+
.reduce((acc, key) => ({ ...acc, [key]: event.new_data?.[key] }), { id: merged.id });
|
|
163
|
+
const typeForField = (field, pathValue) => {
|
|
164
|
+
const oldValue = event.old_data?.[field];
|
|
165
|
+
const newValue = event.new_data?.[field];
|
|
166
|
+
const value = String(pathValue);
|
|
167
|
+
if (Array.isArray(oldValue) || Array.isArray(newValue)) {
|
|
168
|
+
const oldArray = (Array.isArray(oldValue) ? oldValue : []).map(item => String(item));
|
|
169
|
+
const newArray = (Array.isArray(newValue) ? newValue : []).map(item => String(item));
|
|
170
|
+
if (oldArray.includes(value) && !newArray.includes(value))
|
|
171
|
+
return 'removed';
|
|
172
|
+
if (!oldArray.includes(value) && newArray.includes(value))
|
|
173
|
+
return 'added';
|
|
174
|
+
return 'modified';
|
|
175
|
+
}
|
|
176
|
+
if (String(newValue) == value && String(oldValue) != value)
|
|
177
|
+
return 'added';
|
|
178
|
+
if (String(oldValue) == value && String(newValue) != value)
|
|
179
|
+
return 'removed';
|
|
180
|
+
return 'modified';
|
|
181
|
+
};
|
|
182
|
+
const buildRefs = ([{ array, collection, field }, ...fields]) => {
|
|
183
|
+
if (fields.length === 0 || !field)
|
|
184
|
+
return [{ refs: [collection], type: event.type }];
|
|
185
|
+
const values = array
|
|
186
|
+
? Array.isArray(merged[field])
|
|
187
|
+
? [...new Set([
|
|
188
|
+
...(event.old_data?.[field]?.map((item) => String(item)) || []),
|
|
189
|
+
...(event.new_data?.[field]?.map((item) => String(item)) || []),
|
|
190
|
+
])]
|
|
191
|
+
: ['-']
|
|
192
|
+
: [merged[field] ?? '-'];
|
|
193
|
+
return values.flatMap(value => {
|
|
194
|
+
return buildRefs(fields).map(next => ({
|
|
195
|
+
type: event.type == 'added' || event.type == 'removed'
|
|
196
|
+
? event.type
|
|
197
|
+
: next.type == 'added' || next.type == 'removed'
|
|
198
|
+
? next.type
|
|
199
|
+
: typeForField(field, value),
|
|
200
|
+
refs: [collection, String(value), ...next.refs],
|
|
201
|
+
}));
|
|
202
|
+
});
|
|
203
|
+
};
|
|
204
|
+
return [...refs.values()].flatMap(metadata => {
|
|
205
|
+
return buildRefs(metadata).map(({ refs, type }) => ({
|
|
206
|
+
ref: refs.join('/'),
|
|
207
|
+
type,
|
|
208
|
+
data: type == 'added' ? merged : {
|
|
209
|
+
...type == 'modified' ? changes : {},
|
|
210
|
+
id: merged.id,
|
|
211
|
+
},
|
|
212
|
+
}));
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
watch(config, routes) {
|
|
216
|
+
const paths = this.#paths(routes);
|
|
217
|
+
if (paths.size === 0)
|
|
218
|
+
return EMPTY;
|
|
219
|
+
return this.#listenRawChanges(config, routes).pipe(map(event => this.#format(paths, event)), mergeAll());
|
|
220
|
+
}
|
|
221
|
+
}
|
package/build/src/SmartCache.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
export class SmartCache {
|
|
2
2
|
#storage = new Map();
|
|
3
|
-
async get(key,
|
|
3
|
+
async get(key, resolver) {
|
|
4
4
|
const cache = this.#storage.get(key);
|
|
5
5
|
if (cache)
|
|
6
6
|
return await cache;
|
|
7
|
-
const value =
|
|
7
|
+
const value = resolver();
|
|
8
8
|
this.#storage.set(key, value);
|
|
9
9
|
return await value;
|
|
10
10
|
}
|
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/smartcache.ts","../src/index.ts","../src/types.ts"],"version":"5.9.3"}
|
|
1
|
+
{"root":["../src/cursor.ts","../src/datachangepayload.ts","../src/mongodatasource.ts","../src/mongoquery.ts","../src/mongodbrealtime.ts","../src/smartcache.ts","../src/index.ts","../src/types.ts"],"version":"5.9.3"}
|
package/package.json
CHANGED