@livequery/mongodb 2.0.0

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.
@@ -0,0 +1,454 @@
1
+ import { Cursor } from "./Cursor.js";
2
+ import { ObjectId } from "bson";
3
+ export class MongoQuery {
4
+ static #is_operator(c) {
5
+ return ['+', '-', '*', '/', '(', ')', '~'].indexOf(c) !== -1;
6
+ }
7
+ static #get_precedence(op) {
8
+ if (op == '~')
9
+ return 3;
10
+ if (op === '+' || op === '-') {
11
+ return 1;
12
+ }
13
+ else if (op === '*' || op === '/') {
14
+ return 2;
15
+ }
16
+ return 0;
17
+ }
18
+ static #infix_to_postfix(expression) {
19
+ let stack = [];
20
+ let output = [];
21
+ let current = '';
22
+ for (let i = 0; i < expression.length; i++) {
23
+ const token = expression[i];
24
+ if (this.#is_operator(token)) {
25
+ if (current) {
26
+ output.push(current);
27
+ current = '';
28
+ }
29
+ if (token === '(') {
30
+ stack.push(token);
31
+ }
32
+ else if (token === ')') {
33
+ while (stack.length > 0 && stack[stack.length - 1] !== '(') {
34
+ output.push(stack.pop());
35
+ }
36
+ stack.pop();
37
+ }
38
+ else {
39
+ while (stack.length > 0 && this.#get_precedence(token) <= this.#get_precedence(stack[stack.length - 1])) {
40
+ output.push(stack.pop());
41
+ }
42
+ stack.push(token);
43
+ }
44
+ }
45
+ else if (/\s/.test(token)) {
46
+ continue;
47
+ }
48
+ else {
49
+ current += token;
50
+ }
51
+ }
52
+ if (current) {
53
+ output.push(current);
54
+ }
55
+ while (stack.length > 0) {
56
+ output.push(stack.pop());
57
+ }
58
+ return output;
59
+ }
60
+ static #postfix_to_mongodb(postfixExpression) {
61
+ let stack = [];
62
+ for (let token of postfixExpression) {
63
+ if (!isNaN(Number(token))) {
64
+ stack.push(Number(token));
65
+ }
66
+ else if (token.includes('~')) {
67
+ let value = stack.pop();
68
+ stack.push({ $round: [value, 0] });
69
+ }
70
+ else if (this.#is_operator(token)) {
71
+ const right = stack.pop();
72
+ const left = stack.pop();
73
+ switch (token) {
74
+ case '+':
75
+ stack.push({ $add: [left, right] });
76
+ break;
77
+ case '-':
78
+ stack.push({ $subtract: [left, right] });
79
+ break;
80
+ case '*':
81
+ stack.push({ $multiply: [left, right] });
82
+ break;
83
+ case '/':
84
+ stack.push({ $divide: [left, right] });
85
+ break;
86
+ }
87
+ }
88
+ else {
89
+ stack.push(`$${token}`);
90
+ }
91
+ }
92
+ return stack.pop();
93
+ }
94
+ static #parse_array(value) {
95
+ if (Array.isArray(value))
96
+ return value;
97
+ if (typeof value != 'string')
98
+ return [];
99
+ try {
100
+ const parsed = JSON.parse(value);
101
+ return Array.isArray(parsed) ? parsed : [];
102
+ }
103
+ catch {
104
+ return [];
105
+ }
106
+ }
107
+ static #parse_summary(req) {
108
+ const parsed = Object
109
+ .entries(req.options)
110
+ .map(([key, v], index) => {
111
+ if (!key.startsWith('::'))
112
+ return [];
113
+ const exprs = `${v}`.split('|').filter(Boolean);
114
+ const fns = exprs.map(l => {
115
+ const exp = l.split('(')[0];
116
+ if (!['sum', 'avg', 'max', 'min', 'count', 'distinct'].includes(exp))
117
+ return [];
118
+ if (exp == 'count' || exp == 'distinct')
119
+ return [{ key: exp, query: { $sum: 1 } }];
120
+ const infix = l.split('(')?.[1]?.split(')')?.[0];
121
+ if (!infix)
122
+ return [];
123
+ const key = `${exp}_${infix}`;
124
+ const query = this.#postfix_to_mongodb(this.#infix_to_postfix(infix));
125
+ return [{ key, query: { [`$${exp}`]: query } }];
126
+ }).flat(2);
127
+ const groups = exprs.filter(g => g.match(/^[a-zA-Z_]+$/));
128
+ const $match = exprs.map(exp => {
129
+ for (const { c, f } of [
130
+ { c: '==', f: 'eq' },
131
+ { c: '<>', f: 'ne' },
132
+ { c: '>=', f: 'gte' },
133
+ { c: '<=', f: 'lte' },
134
+ { c: '>', f: 'gt' },
135
+ { c: '<', f: 'lt' },
136
+ { c: '=', f: 'eq' },
137
+ ]) {
138
+ if (exp.includes(c)) {
139
+ const [a, b] = exp.split(c);
140
+ return { [a]: { [f]: isNaN(Number(b)) ? (c == '==' ? (b == 'true') : b) : Number(b) } };
141
+ }
142
+ }
143
+ }).filter(Boolean);
144
+ const is_distinc_count = `${v}`.includes('distinc');
145
+ const simple = is_distinc_count ? key : (exprs.length == 1 ? fns[0].key : false);
146
+ const pipelines = [
147
+ ...$match.length > 0 ? [{ $match }] : [],
148
+ {
149
+ $group: {
150
+ _id: groups.length == 0 ? null : groups.reduce((p, by) => {
151
+ return {
152
+ ...p,
153
+ [by]: `$${by}`
154
+ };
155
+ }, {}),
156
+ ...fns.reduce((p, { query, key }) => ({
157
+ ...p,
158
+ [key]: query
159
+ }), {})
160
+ }
161
+ },
162
+ ...is_distinc_count ? [
163
+ {
164
+ $count: key
165
+ }
166
+ ] : [
167
+ {
168
+ $project: {
169
+ ...groups.reduce((p, c) => ({
170
+ ...p,
171
+ [c]: `$_id.${c}`
172
+ }), {}),
173
+ ...fns.reduce((p, { key }) => ({
174
+ ...p,
175
+ [key]: 1
176
+ }), {}),
177
+ _id: 0
178
+ }
179
+ },
180
+ {
181
+ $limit: 50
182
+ }
183
+ ]
184
+ ];
185
+ return [{ key, pipelines, simple }];
186
+ })
187
+ .flat(1);
188
+ const pipelines = parsed.reduce((p, { key, pipelines }) => ({
189
+ ...p,
190
+ [key]: pipelines
191
+ }), {});
192
+ const summary = parsed.length == 0 ? undefined : parsed.reduce((p, { key, simple }) => {
193
+ return {
194
+ ...p,
195
+ [key]: simple ? { $arrayElemAt: [`$${key}.${simple}`, 0] } : `$${key}`
196
+ };
197
+ }, {});
198
+ return {
199
+ pipelines,
200
+ summary
201
+ };
202
+ }
203
+ static #parse_conditions(filters) {
204
+ if (!filters)
205
+ return [];
206
+ const { ':and': and, ':or': or, ':not': not, ...rest } = filters;
207
+ const $or = Object.entries(rest).filter(([k]) => k.endsWith(':like')).map(([k, v]) => {
208
+ const key = k.split(':like')[0];
209
+ const value = `${v}`;
210
+ return {
211
+ [key]: { $regex: value }
212
+ };
213
+ });
214
+ const $match = Object.entries(rest).reduce((p, [k, value]) => {
215
+ if (k.startsWith('::'))
216
+ return p;
217
+ const [key, expression] = k.split(':');
218
+ const map = {
219
+ eq: () => ({ $eq: value }),
220
+ lt: () => ({ $lt: !isNaN(Number(value)) ? Number(value) : 0 }),
221
+ lte: () => ({ $lte: !isNaN(Number(value)) ? Number(value) : 0 }),
222
+ gt: () => ({ $gt: !isNaN(Number(value)) ? Number(value) : 0 }),
223
+ gte: () => ({ $gte: !isNaN(Number(value)) ? Number(value) : 0 }),
224
+ ne: () => {
225
+ return { $ne: value };
226
+ },
227
+ in: () => ({ $in: this.#parse_array(value) }),
228
+ nin: () => ({ $nin: this.#parse_array(value) }),
229
+ 'eq-number': () => ({ $eq: !isNaN(Number(value)) ? Number(value) : 0 }),
230
+ 'neq-number': () => ({ $ne: !isNaN(Number(value)) ? Number(value) : 0 }),
231
+ 'eq-boolean': () => ({ $eq: `${value}`.toLowerCase() == 'true' ? true : false }),
232
+ 'neq-boolean': () => ({ $ne: `${value}`.toLowerCase() == 'false' ? false : true }),
233
+ 'eq-null': () => ({ $eq: null }),
234
+ 'neq-null': () => ({ $ne: null }),
235
+ 'eq-oid': () => ({ $eq: ObjectId.isValid(value) ? new ObjectId(value) : value }),
236
+ 'neq-oid': () => ({ $ne: ObjectId.isValid(value) ? new ObjectId(value) : value }),
237
+ };
238
+ const fn = map[expression || 'eq'];
239
+ if (!fn)
240
+ return p;
241
+ return {
242
+ ...p,
243
+ [key]: {
244
+ ...p[key] || {},
245
+ ...fn()
246
+ }
247
+ };
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
+ ];
255
+ }
256
+ static #build_search_query(req) {
257
+ const search = req.options[":search"];
258
+ return search ? [{ $match: { $text: { $search: `${search}` } } }] : [];
259
+ }
260
+ static #get_limit(req) {
261
+ const l = Number(req.options[':limit']);
262
+ if (isNaN(l))
263
+ return 10;
264
+ if (l < 1)
265
+ return 1;
266
+ if (l > 100)
267
+ return 100;
268
+ return l;
269
+ }
270
+ static #rename_id() {
271
+ return [
272
+ {
273
+ $set: {
274
+ id: "$_id"
275
+ }
276
+ }
277
+ ];
278
+ }
279
+ static #build_cursor_query($sort, req, reverse = false) {
280
+ const limit = this.#get_limit(req);
281
+ const after = req.options[':after'];
282
+ const before = req.options[':before'];
283
+ const around = req.options[':around'];
284
+ const pagination_token = around || before || after;
285
+ const cursor = pagination_token ? Cursor.parse(pagination_token) : (reverse ? null : {});
286
+ if (!cursor)
287
+ return [{ $limit: 1 }, { $match: { _id: 0 } }];
288
+ const $or = Object.entries({ ...$sort, _id: $sort._id || -1 }).map(([key, order], index, arr) => {
289
+ const desc = order == -1;
290
+ const value = cursor[key == '_id' ? 'id' : key];
291
+ const type = typeof value;
292
+ if (type == 'string' || type == 'number') {
293
+ const expr = `${desc ? (reverse ? '$gt' : '$lt') : (reverse ? '$lt' : '$gt')}${reverse || around ? 'e' : ''}`;
294
+ const prevs = arr.slice(0, index).reduce((p, [key]) => ({ ...p, [key]: cursor[key] }), {});
295
+ const cpr = (key == 'id' || key == '_id') ? new ObjectId(value) : value;
296
+ return {
297
+ ...prevs,
298
+ [key]: { [expr]: cpr }
299
+ };
300
+ }
301
+ return {};
302
+ });
303
+ const items_visible = around || (reverse ? before : after) || (!reverse && !pagination_token);
304
+ return [
305
+ {
306
+ $match: { $or }
307
+ },
308
+ {
309
+ $project: {
310
+ _id: 0
311
+ }
312
+ },
313
+ {
314
+ $group: {
315
+ _id: null,
316
+ ...items_visible ? {
317
+ items: {
318
+ [reverse ? '$bottomN' : '$topN']: {
319
+ n: limit,
320
+ sortBy: {},
321
+ output: "$$ROOT"
322
+ }
323
+ }
324
+ } : {},
325
+ count: {
326
+ $sum: 1
327
+ }
328
+ }
329
+ },
330
+ {
331
+ $project: {
332
+ items: items_visible ? 1 : [],
333
+ count: 1
334
+ }
335
+ }
336
+ ];
337
+ }
338
+ static #build_cursor_paging($sort, req) {
339
+ if (req.options[':after'] || req.options[':before'] || req.options[':around']) {
340
+ }
341
+ const { pipelines, summary } = this.#parse_summary(req);
342
+ const limit = this.#get_limit(req);
343
+ return [
344
+ {
345
+ $facet: {
346
+ ...pipelines,
347
+ prev: this.#build_cursor_query($sort, req, true),
348
+ next: this.#build_cursor_query($sort, req),
349
+ }
350
+ },
351
+ {
352
+ $project: {
353
+ summary,
354
+ prev: {
355
+ $ifNull: [
356
+ { $arrayElemAt: ["$prev", 0] },
357
+ { items: [], count: 0 }
358
+ ]
359
+ },
360
+ next: {
361
+ $ifNull: [
362
+ { $arrayElemAt: ["$next", 0] },
363
+ { items: [], count: 0 }
364
+ ]
365
+ }
366
+ }
367
+ },
368
+ {
369
+ $project: {
370
+ summary: 1,
371
+ items: {
372
+ $concatArrays: ["$prev.items", "$next.items"]
373
+ },
374
+ has: {
375
+ prev: { $gt: ["$prev.count", 0] },
376
+ next: { $gt: ["$next.count", limit] }
377
+ },
378
+ count: {
379
+ prev: { $max: [{ $subtract: ["$prev.count", 0] }, 0] },
380
+ next: { $max: [{ $subtract: ["$next.count", limit] }, 0] }
381
+ }
382
+ }
383
+ }
384
+ ];
385
+ }
386
+ static #build_offset_paging(req) {
387
+ return [];
388
+ }
389
+ static #build_query_filter(req) {
390
+ const { ":after": after, ":before": before, ':around': around, ":limit": _limit, ":page": _page, ":search": search, ...rest } = req.options;
391
+ return this.#parse_conditions({ ...rest, ...req.keys });
392
+ }
393
+ static #get_sorter(req) {
394
+ let default_sort = -1;
395
+ const $sort = Object.entries(req.options).reduce((p, [k, order]) => {
396
+ if (!k.endsWith(':sort'))
397
+ return p;
398
+ const by = k.split(':sort')[0];
399
+ const key = by == 'id' ? '_id' : by;
400
+ if (key == '_id') {
401
+ if (order == 'asc' || order == '1' || order == 1) {
402
+ default_sort = 1;
403
+ }
404
+ return p;
405
+ }
406
+ return {
407
+ ...p,
408
+ [key]: order == 'asc' ? 1 : -1
409
+ };
410
+ }, {});
411
+ $sort['_id'] = default_sort;
412
+ return $sort;
413
+ }
414
+ static async query(req, collection) {
415
+ if (!req.is_collection) {
416
+ const aggregates = [
417
+ {
418
+ $match: {
419
+ ...req.keys,
420
+ ...req.keys.id ? { id: undefined, _id: ObjectId.createFromHexString(req.keys.id) } : {}
421
+ }
422
+ },
423
+ ...this.#rename_id(),
424
+ {
425
+ $project: {
426
+ _id: 0
427
+ }
428
+ }
429
+ ];
430
+ const items = await collection.aggregate(aggregates).toArray();
431
+ return {
432
+ items,
433
+ limit: 1,
434
+ count: { next: 0, prev: 0 },
435
+ has: { next: false, prev: false },
436
+ summary: {}
437
+ };
438
+ }
439
+ const is_cursor_paging = req.options[':after'] || req.options[':before'] || req.options[':around'] || !req.options['page'];
440
+ const $sort = this.#get_sorter(req);
441
+ const pipelines = [
442
+ { $sort },
443
+ ...this.#build_query_filter(req),
444
+ ...this.#build_search_query(req),
445
+ ...this.#rename_id(),
446
+ ...is_cursor_paging ? this.#build_cursor_paging($sort, req) : this.#build_offset_paging(req)
447
+ ];
448
+ const response = await collection.aggregate(pipelines).toArray();
449
+ return {
450
+ ...response[0],
451
+ limit: this.#get_limit(req)
452
+ };
453
+ }
454
+ }
@@ -0,0 +1,4 @@
1
+ export declare class SmartCache {
2
+ #private;
3
+ get<T>(key: any, reslover: () => Promise<T>): Promise<any>;
4
+ }
@@ -0,0 +1,11 @@
1
+ export class SmartCache {
2
+ #storage = new Map();
3
+ async get(key, reslover) {
4
+ const cache = this.#storage.get(key);
5
+ if (cache)
6
+ return await cache;
7
+ const value = reslover();
8
+ this.#storage.set(key, value);
9
+ return await value;
10
+ }
11
+ }
@@ -0,0 +1,3 @@
1
+ export * from './MongoDatasource.js';
2
+ export * from './DataChangePayload.js';
3
+ export * from './types.js';
@@ -0,0 +1,3 @@
1
+ export * from './MongoDatasource.js';
2
+ export * from './DataChangePayload.js';
3
+ export * from './types.js';
@@ -0,0 +1,46 @@
1
+ import type { LivequeryRequest as CoreLivequeryRequest } from '@livequery/core';
2
+ export type LivequeryBaseEntity = {
3
+ id?: string;
4
+ [key: string]: any;
5
+ };
6
+ export type QueryOption<T extends LivequeryBaseEntity = LivequeryBaseEntity> = Record<string, any>;
7
+ export type FilterConditions<T extends LivequeryBaseEntity = LivequeryBaseEntity> = Record<string, any>;
8
+ export type Paging = {
9
+ cursor: {
10
+ last: string | null;
11
+ first: string | null;
12
+ };
13
+ has: {
14
+ prev: boolean;
15
+ next: boolean;
16
+ };
17
+ count: {
18
+ prev: number;
19
+ next: number;
20
+ current: number;
21
+ total: number;
22
+ };
23
+ page: {
24
+ current: number;
25
+ total: number;
26
+ };
27
+ };
28
+ export type WebsocketSyncPayload<T extends LivequeryBaseEntity = LivequeryBaseEntity> = {
29
+ ref?: string;
30
+ type?: string;
31
+ data?: T;
32
+ [key: string]: any;
33
+ };
34
+ export type LivequeryRequest<T = any> = Partial<CoreLivequeryRequest<T>> & {
35
+ keys: Record<string, any>;
36
+ ref: string;
37
+ collection_ref?: string;
38
+ schema_collection_ref?: string;
39
+ method: string;
40
+ body?: T;
41
+ query?: Record<string, any>;
42
+ options: Record<string, any>;
43
+ is_collection: boolean;
44
+ doc_id?: string;
45
+ document_id?: string;
46
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +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"}
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@livequery/mongodb",
3
+ "author": "Duong Van Ba",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "repository": {
7
+ "url": "git@github.com:livequery/mongodb.git"
8
+ },
9
+ "version": "2.0.0",
10
+ "description": "MongoDB datasource mapping for @livequery ecosystem",
11
+ "main": "./build/src/index.js",
12
+ "types": "./build/src/index.d.ts",
13
+ "files": [
14
+ "build/**/*"
15
+ ],
16
+ "exports": {
17
+ ".": {
18
+ "import": {
19
+ "types": "./build/src/index.d.ts",
20
+ "default": "./build/src/index.js"
21
+ }
22
+ }
23
+ },
24
+ "devDependencies": {
25
+ "@livequery/core": "file:../core",
26
+ "@types/node": "^18.11.9",
27
+ "mongodb": "^6.20.0",
28
+ "rxjs": "*",
29
+ "typescript": "*"
30
+ },
31
+ "scripts": {
32
+ "test": "bun test",
33
+ "build": "rm -rf build; tsc -b .",
34
+ "deploy": "rm -rf build && yarn build; git add .; git commit -m \"Update\"; git push origin master; npm publish --access public"
35
+ },
36
+ "peerDependencies": {
37
+ "bson": "*",
38
+ "mongodb": "^6.20.0"
39
+ }
40
+ }