@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 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`: optional marker for realtime routes. The current adapter does not use it for query execution.
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<import("mongodb").DeleteResult | {
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
- } | import("mongodb").UpdateResult<any>>;
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
- return await collection.updateOne(this.#keys(req), this.#update(req.body));
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
- return await collection.updateOne(this.#keys(req), this.#update(req.body));
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
- return await collection.deleteOne(this.#keys(req));
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]) => {
@@ -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 $match = Object.entries(rest).reduce((p, [k, value]) => {
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
- }, $or.length > 0 ? { $or } : {});
249
- return [
250
- ...Object.keys($match).length > 0 ? [{ $match }] : [],
251
- ...and && Object.keys(and).length > 0 ? [{ $expr: { $and: this.#parse_conditions(and) } }] : [],
252
- ...or && Object.keys(and).length > 0 ? [{ $expr: { $or: this.#parse_conditions(or) } }] : [],
253
- ...not && Object.keys(and).length > 0 ? [{ $not: { $and: this.#parse_conditions(not) } }] : [],
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
- return [];
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
+ }
@@ -1,4 +1,4 @@
1
1
  export declare class SmartCache {
2
2
  #private;
3
- get<T>(key: any, reslover: () => Promise<T>): Promise<any>;
3
+ get<T>(key: any, resolver: () => Promise<T>): Promise<any>;
4
4
  }
@@ -1,10 +1,10 @@
1
1
  export class SmartCache {
2
2
  #storage = new Map();
3
- async get(key, reslover) {
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 = reslover();
7
+ const value = resolver();
8
8
  this.#storage.set(key, value);
9
9
  return await value;
10
10
  }
@@ -1,3 +1,4 @@
1
1
  export * from './MongoDatasource.js';
2
2
  export * from './DataChangePayload.js';
3
+ export * from './MongodbRealtime.js';
3
4
  export * from './types.js';
@@ -1,3 +1,4 @@
1
1
  export * from './MongoDatasource.js';
2
2
  export * from './DataChangePayload.js';
3
+ export * from './MongodbRealtime.js';
3
4
  export * from './types.js';
@@ -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
@@ -6,7 +6,7 @@
6
6
  "repository": {
7
7
  "url": "git@github.com:livequery/mongodb.git"
8
8
  },
9
- "version": "2.0.0",
9
+ "version": "2.0.145",
10
10
  "description": "MongoDB datasource mapping for @livequery ecosystem",
11
11
  "main": "./build/src/index.js",
12
12
  "types": "./build/src/index.d.ts",