@andrew_l/search-query-language 0.3.21 → 0.4.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.
package/dist/index.cjs DELETED
@@ -1,727 +0,0 @@
1
- 'use strict';
2
-
3
- const toolkit = require('@andrew_l/toolkit');
4
- const mongoose = require('mongoose');
5
-
6
- function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
7
-
8
- const mongoose__default = /*#__PURE__*/_interopDefaultCompat(mongoose);
9
-
10
- class TokenType {
11
- label;
12
- keyword;
13
- constructor(label, options) {
14
- this.label = label;
15
- this.keyword = options?.keyword;
16
- }
17
- }
18
-
19
- const NODE = Object.freeze({
20
- PROGRAM: "program",
21
- IDENTIFIER: "identifier",
22
- LITERAL: "literal",
23
- LOGICAL_EXPRESSION: "logical-expression",
24
- BINARY_EXPRESSION: "binary-expression"
25
- });
26
- const KEYWORDS = {};
27
- const TOKEN = Object.freeze({
28
- /**
29
- * Start of File token.
30
- */
31
- SOF: new TokenType("sof"),
32
- /**
33
- * End of File token.
34
- */
35
- EOF: new TokenType("eof"),
36
- /**
37
- * Number token.
38
- */
39
- NUM: new TokenType("num"),
40
- /**
41
- * String token.
42
- */
43
- STRING: new TokenType("string"),
44
- /**
45
- * Identifier token.
46
- */
47
- NAME: new TokenType("identifier"),
48
- /**
49
- * Parenthesis token.
50
- */
51
- PAREN_L: new TokenType("("),
52
- /**
53
- * Parenthesis token.
54
- */
55
- PAREN_R: new TokenType(")"),
56
- /**
57
- * Logical operator token.
58
- */
59
- LOGICAL_OR: kw("OR"),
60
- /**
61
- * Logical operator token.
62
- */
63
- LOGICAL_AND: kw("AND"),
64
- /**
65
- * Equality operator token.
66
- */
67
- EQUALITY: new TokenType("=/!="),
68
- /**
69
- * Relational operator token.
70
- */
71
- RELATIONAL: new TokenType("</>/<=/>="),
72
- /**
73
- * Minus operator token.
74
- */
75
- MINUS: new TokenType("-"),
76
- /**
77
- * Null literal token.
78
- */
79
- NULL: kw("null"),
80
- /**
81
- * True literal tokens.
82
- */
83
- TRUE: kw("true"),
84
- /**
85
- * False literal tokens.
86
- */
87
- FALSE: kw("false")
88
- });
89
- function kw(name, options = {}) {
90
- options.keyword = name;
91
- return KEYWORDS[name] = new TokenType(name, options);
92
- }
93
-
94
- class Token {
95
- type;
96
- value;
97
- start;
98
- end;
99
- constructor(p) {
100
- this.type = p.type;
101
- this.value = p.value;
102
- this.start = p.start;
103
- this.end = p.end;
104
- }
105
- }
106
-
107
- class Tokenizer {
108
- input;
109
- pos;
110
- length;
111
- state;
112
- prev;
113
- constructor(input) {
114
- this.input = input;
115
- this.length = input.length;
116
- this.pos = 0;
117
- this.state = {
118
- start: 0,
119
- end: 0,
120
- type: TOKEN.SOF,
121
- value: null
122
- };
123
- this.prev = { ...this.state };
124
- }
125
- [Symbol.iterator]() {
126
- return {
127
- next: () => {
128
- let token = this.getToken();
129
- return {
130
- done: token.type === TOKEN.EOF,
131
- value: token
132
- };
133
- }
134
- };
135
- }
136
- /**
137
- * Get the next token.
138
- */
139
- getToken() {
140
- this.nextToken();
141
- const token = new Token(this.state);
142
- return token;
143
- }
144
- nextToken() {
145
- this.skipSpace();
146
- this.state.start = this.pos;
147
- if (this.pos >= this.length) return this.finishToken(TOKEN.EOF);
148
- this.readToken(this.charCodeAtPos());
149
- }
150
- skipSpace() {
151
- let ch;
152
- while (this.pos < this.length) {
153
- ch = this.charCodeAtPos();
154
- if (ch === 32 || ch === 160) {
155
- this.pos++;
156
- } else {
157
- break;
158
- }
159
- }
160
- }
161
- finishToken(type, value) {
162
- Object.assign(this.prev, this.state);
163
- this.state.end = this.pos;
164
- this.state.type = type;
165
- this.state.value = value;
166
- }
167
- finishOp(type, size) {
168
- let str = this.input.slice(this.pos, this.pos + size);
169
- this.pos += size;
170
- return this.finishToken(type, str);
171
- }
172
- readToken(code) {
173
- if (isIdentifierStart(code)) {
174
- return this.readWord();
175
- }
176
- return this.getTokenFromCode(code);
177
- }
178
- getTokenFromCode(code) {
179
- switch (code) {
180
- case 40:
181
- ++this.pos;
182
- return this.finishToken(TOKEN.PAREN_L);
183
- case 41:
184
- ++this.pos;
185
- return this.finishToken(TOKEN.PAREN_R);
186
- case 49:
187
- case 50:
188
- case 51:
189
- case 52:
190
- case 53:
191
- case 54:
192
- case 55:
193
- case 56:
194
- case 57:
195
- return this.readNumber();
196
- case 45:
197
- ++this.pos;
198
- return this.finishToken(TOKEN.MINUS);
199
- case 61:
200
- case 33:
201
- return this.readEquality(code);
202
- case 60:
203
- case 62:
204
- return this.readLtGt();
205
- case 34:
206
- return this.readString();
207
- }
208
- this.raise(
209
- this.pos,
210
- "Unexpected character '" + String.fromCharCode(code) + "'"
211
- );
212
- }
213
- charCodeAtPos() {
214
- return this.input.charCodeAt(this.pos);
215
- }
216
- readEquality(code) {
217
- let next = this.input.charCodeAt(this.pos + 1);
218
- if (code === 33) {
219
- if (next === 61) return this.finishOp(TOKEN.EQUALITY, 2);
220
- } else {
221
- return this.finishOp(TOKEN.EQUALITY, 1);
222
- }
223
- }
224
- readLtGt() {
225
- let next = this.input.charCodeAt(this.pos + 1);
226
- let size = 1;
227
- if (next === 61) size = 2;
228
- return this.finishOp(TOKEN.RELATIONAL, size);
229
- }
230
- readInt() {
231
- const radix = 10;
232
- let start = this.pos;
233
- let code = 0;
234
- let total = 0;
235
- for (; ; ) {
236
- code = this.input.charCodeAt(this.pos);
237
- if (code >= 48 && code <= 57) {
238
- total = total * radix + (code - 48);
239
- this.pos++;
240
- } else {
241
- break;
242
- }
243
- }
244
- if (this.pos === start) return null;
245
- return total;
246
- }
247
- raise(pos, message) {
248
- throw new SyntaxError(`${message} (${pos})`);
249
- }
250
- readString() {
251
- let out = "";
252
- let chunkStart = ++this.pos;
253
- let ch;
254
- for (; ; ) {
255
- if (this.pos >= this.length) {
256
- this.raise(this.state.start, "Unterminated string constant");
257
- }
258
- ch = this.input.charCodeAt(this.pos);
259
- if (ch === 34) break;
260
- if (ch === 92) {
261
- out += this.input.slice(chunkStart, this.pos);
262
- out += this.readEscapedChar();
263
- chunkStart = this.pos;
264
- }
265
- ++this.pos;
266
- }
267
- out += this.input.slice(chunkStart, this.pos++);
268
- return this.finishToken(TOKEN.STRING, out);
269
- }
270
- readWord() {
271
- let chunkStart = this.pos;
272
- let ch;
273
- let cmp = isIdentifierStart;
274
- while (this.pos < this.input.length) {
275
- ch = this.charCodeAtPos();
276
- if (!cmp(ch)) break;
277
- cmp = isIdentifierChar;
278
- this.pos++;
279
- }
280
- const word = this.input.slice(chunkStart, this.pos);
281
- let type = TOKEN.NAME;
282
- if (word in KEYWORDS) {
283
- type = KEYWORDS[word];
284
- }
285
- return this.finishToken(type, word);
286
- }
287
- readEscapedChar() {
288
- let ch = this.input.charCodeAt(++this.pos);
289
- ++this.pos;
290
- return String.fromCharCode(ch);
291
- }
292
- readNumber() {
293
- let start = this.pos;
294
- if (this.readInt() === null) this.raise(start, "Invalid number");
295
- let next = this.input.charCodeAt(this.pos);
296
- if (next === 46) {
297
- ++this.pos;
298
- this.readInt();
299
- next = this.input.charCodeAt(this.pos);
300
- }
301
- let val = Number(this.input.slice(start, this.pos));
302
- return this.finishToken(TOKEN.NUM, val);
303
- }
304
- }
305
- function isIdentifierStart(code) {
306
- if (code < 65) return false;
307
- if (code < 91) return true;
308
- if (code < 97) return code === 95;
309
- if (code < 123) return true;
310
- return false;
311
- }
312
- function isIdentifierChar(code) {
313
- if (code < 48) return code === 46;
314
- if (code < 58) return true;
315
- if (code < 65) return false;
316
- if (code < 91) return true;
317
- if (code < 97) return code === 95;
318
- if (code < 123) return true;
319
- return false;
320
- }
321
-
322
- class Expression extends Tokenizer {
323
- constructor(input) {
324
- super(input);
325
- }
326
- /**
327
- * Parse input into a program AST.
328
- */
329
- parse() {
330
- let node = this.startNode(NODE.PROGRAM);
331
- node.body = [];
332
- this.nextToken();
333
- return this.parseTopLevel(node);
334
- }
335
- finishNode(node) {
336
- node.end = this.state.start;
337
- return node;
338
- }
339
- parseTopLevel(node) {
340
- while (this.state.type !== TOKEN.EOF) {
341
- let stmt = this.parseExpression();
342
- node.body.push(stmt);
343
- }
344
- this.nextToken();
345
- return this.finishNode(node);
346
- }
347
- parseExpression() {
348
- let left;
349
- if (this.state.type === TOKEN.PAREN_L) {
350
- this.nextToken();
351
- left = this.parseExpression();
352
- if (this.state.type !== TOKEN.PAREN_R) {
353
- this.raise(this.pos, "Expected closing paren.");
354
- }
355
- this.nextToken();
356
- if (this.state.type === TOKEN.EOF) {
357
- return left;
358
- }
359
- } else {
360
- left = this.parseExprAtom();
361
- }
362
- let node;
363
- for (; ; ) {
364
- node = this.parseExprOp(left);
365
- node.start = left.start;
366
- left = node;
367
- if (this.state.type === TOKEN.EOF || this.state.type === TOKEN.PAREN_R)
368
- break;
369
- }
370
- return node;
371
- }
372
- parseExprOp(left) {
373
- const node = this.startNode(NODE.BINARY_EXPRESSION);
374
- node.operator = this.state.value;
375
- node.left = left;
376
- switch (this.state.type) {
377
- case TOKEN.LOGICAL_AND:
378
- case TOKEN.LOGICAL_OR: {
379
- if (!isNodeExpression(node.left)) {
380
- this.raise(
381
- node.left.start,
382
- "Expected expression as left side of logical expression."
383
- );
384
- }
385
- this.nextToken();
386
- node.right = this.maybeLiteral() || this.parseExpression();
387
- node.type = NODE.LOGICAL_EXPRESSION;
388
- if (!isNodeExpression(node.right)) {
389
- this.raise(
390
- node.right.start,
391
- `Unexpected ${node.right.type} as right side of logical expression.`
392
- );
393
- }
394
- break;
395
- }
396
- case TOKEN.EQUALITY:
397
- case TOKEN.RELATIONAL: {
398
- this.nextToken();
399
- if (node.left.type !== NODE.IDENTIFIER) {
400
- this.raise(
401
- node.start,
402
- `Expected identifier as left side of binary expression.`
403
- );
404
- }
405
- node.right = this.parseExprAtom();
406
- if (node.right.type !== NODE.LITERAL) {
407
- this.raise(
408
- node.right.start,
409
- "Expected literal as right side of binary expression."
410
- );
411
- }
412
- break;
413
- }
414
- default: {
415
- if (this.state.type === TOKEN.EOF) {
416
- this.raise(
417
- this.pos,
418
- `Expected operator after ${this.prev.type.label}`
419
- );
420
- } else {
421
- this.raise(this.pos, `Unexpected operator: ${this.state.type.label}`);
422
- }
423
- }
424
- }
425
- return this.finishNode(node);
426
- }
427
- maybeLiteral() {
428
- switch (this.state.type) {
429
- case TOKEN.MINUS: {
430
- const start = this.state.start;
431
- this.nextToken();
432
- if (this.state.type !== TOKEN.NUM) {
433
- this.raise(this.pos, `Expected number after minus operator`);
434
- }
435
- const node = this.maybeLiteral();
436
- node.start = start;
437
- node.raw = `-${node.raw}`;
438
- node.value = node.value * -1;
439
- return node;
440
- }
441
- case TOKEN.NUM:
442
- case TOKEN.STRING: {
443
- let node = this.startNode(NODE.LITERAL);
444
- node.raw = this.input.slice(this.state.start, this.state.end);
445
- node.value = this.state.value;
446
- this.nextToken();
447
- return this.finishNode(node);
448
- }
449
- case TOKEN.NULL:
450
- case TOKEN.TRUE:
451
- case TOKEN.FALSE: {
452
- let node = this.startNode(NODE.LITERAL);
453
- node.raw = this.state.type.keyword;
454
- node.value = this.state.type === TOKEN.NULL ? null : this.state.type === TOKEN.TRUE;
455
- this.nextToken();
456
- return this.finishNode(node);
457
- }
458
- default: {
459
- return null;
460
- }
461
- }
462
- }
463
- maybeIdentifier() {
464
- switch (this.state.type) {
465
- case TOKEN.NAME: {
466
- let node = this.startNode(NODE.IDENTIFIER);
467
- node.name = String(this.state.value);
468
- this.nextToken();
469
- return this.finishNode(node);
470
- }
471
- default:
472
- return null;
473
- }
474
- }
475
- parseExprAtom() {
476
- const node = this.maybeIdentifier() || this.maybeLiteral();
477
- if (!node) {
478
- this.raise(this.pos, `Unexpected token ${this.state.type.label}.`);
479
- }
480
- return node;
481
- }
482
- startNode(type) {
483
- const node = { type, start: this.state.start, end: 0 };
484
- return node;
485
- }
486
- }
487
- function isNodeExpression(node) {
488
- return node.type === NODE.BINARY_EXPRESSION || node.type === NODE.LOGICAL_EXPRESSION;
489
- }
490
-
491
- function parseQuery(value) {
492
- return new Expression(value).parse();
493
- }
494
-
495
- const OPERATOR_MAP = {
496
- "=": "$eq",
497
- "!=": "$ne",
498
- ">": "$gt",
499
- ">=": "$gte",
500
- "<": "$lt",
501
- "<=": "$lte"
502
- };
503
- const NOOP_TRANSFORM = [];
504
- function parseToMongo(input, options = {}) {
505
- const opts = mergeOptions(options);
506
- if (input.trim() === "") {
507
- toolkit.assert.ok(opts.allowEmpty, "Search query cannot be empty.");
508
- return {};
509
- }
510
- const result = {};
511
- const ops = new OpsResource(opts.maxOps);
512
- const exp = new Expression(input).parse();
513
- for (const node of exp.body) {
514
- Object.assign(result, reduceNode(node, opts, ops));
515
- }
516
- return result;
517
- }
518
- function reduceNode(node, options, opsResource) {
519
- switch (node.type) {
520
- case NODE.BINARY_EXPRESSION: {
521
- opsResource.assert();
522
- toolkit.assert.ok(
523
- !options.allowedKeys || options.allowedKeys.has(node.left.name),
524
- `The search key "${node.left.name}" is not allowed.`
525
- );
526
- const transform = options.transform?.[node.left.name] || options.transform?.["*"] || NOOP_TRANSFORM;
527
- return {
528
- [node.left.name]: node.operator === "=" ? callTransform(transform, node.right.value, node.left.name) : {
529
- [OPERATOR_MAP[node.operator]]: callTransform(
530
- transform,
531
- node.right.value,
532
- node.left.name
533
- )
534
- }
535
- };
536
- }
537
- case NODE.LOGICAL_EXPRESSION: {
538
- const op = node.operator === "AND" ? "$and" : "$or";
539
- const right = reduceNode(node.right, options, opsResource);
540
- if (node.right.type === NODE.LOGICAL_EXPRESSION && node.right.operator === node.operator) {
541
- return {
542
- [op]: [reduceNode(node.left, options, opsResource), ...right[op]]
543
- };
544
- }
545
- return {
546
- [op]: [reduceNode(node.left, options, opsResource), right]
547
- };
548
- }
549
- default: {
550
- toolkit.assert.ok(false, `Unexpected ast node: ${node.type}`);
551
- }
552
- }
553
- }
554
- function mergeOptions(...options) {
555
- const optsTransform = {};
556
- const result = {
557
- transform: optsTransform,
558
- allowEmpty: false,
559
- maxOps: Infinity
560
- };
561
- for (const { transform, allowedKeys, ...rest } of options) {
562
- Object.assign(result, rest);
563
- if (allowedKeys?.length) {
564
- result.allowedKeys = new Set(allowedKeys);
565
- }
566
- if (Array.isArray(transform) || toolkit.isFunction(transform)) {
567
- optsTransform["*"] = optsTransform["*"] || [];
568
- optsTransform["*"].push(...toolkit.arrayable(transform));
569
- } else if (transform) {
570
- for (const [key, value] of Object.entries(transform)) {
571
- optsTransform[key] = optsTransform[key] || [];
572
- optsTransform[key].push(...toolkit.arrayable(value));
573
- }
574
- }
575
- }
576
- return result;
577
- }
578
- class OpsResource {
579
- constructor(max) {
580
- this.max = max;
581
- toolkit.assert.ok(
582
- max === Infinity || toolkit.isNumber(max) && max > 0,
583
- "maxOps must be a number greater than zero."
584
- );
585
- this.uses = 0;
586
- }
587
- uses;
588
- assert() {
589
- toolkit.assert.ok(
590
- ++this.uses <= this.max,
591
- `Maximum search operations reached: ${this.max}`
592
- );
593
- }
594
- }
595
- function callTransform(fns, value, key) {
596
- for (const fn of fns) {
597
- value = fn(value, key);
598
- }
599
- return value;
600
- }
601
-
602
- const MONGO_TRANSFORM = Object.freeze({
603
- /**
604
- * Ensures the value is not null.
605
- * Throws an error if the value is null.
606
- *
607
- * @throws {Error} If the value is null.
608
- */
609
- NOT_NULLABLE: (value, key) => {
610
- toolkit.assert.ok(value !== null, `The search key "${key}" cannot be null.`);
611
- return value;
612
- },
613
- /**
614
- * Validates that the value is a number.
615
- * If the value is `null`, it returns `null` without throwing an error.
616
- *
617
- * @throws {Error} If the value is not a valid number.
618
- */
619
- NUMBER: (value, key) => {
620
- if (value === null) return null;
621
- toolkit.assert.number(value, `The search key "${key}" is not valid number.`);
622
- return value;
623
- },
624
- /**
625
- * Validates that the value is a string.
626
- * If the value is `null`, it returns `null` without throwing an error.
627
- *
628
- * @throws {Error} If the value is not a valid string.
629
- */
630
- STRING: (value, key) => {
631
- if (value === null) return null;
632
- toolkit.assert.string(value, `The search key "${key}" is not valid string.`);
633
- return value;
634
- },
635
- /**
636
- * Validates that the value is a boolean.
637
- * If the value is `null`, it returns `null` without throwing an error.
638
- *
639
- * @throws {Error} If the value is not a valid boolean.
640
- */
641
- BOOLEAN: (value, key) => {
642
- if (value === null) return null;
643
- toolkit.assert.boolean(value, `The search key "${key}" is not valid boolean.`);
644
- return value;
645
- },
646
- /**
647
- * Validates and converts the value to a MongoDB ObjectId.
648
- * If the value is `null`, it returns `null` without throwing an error.
649
- *
650
- * @throws {Error} If the value is not a valid ObjectId.
651
- */
652
- OBJECT_ID: (value, key) => {
653
- if (value === null) return null;
654
- toolkit.assert.ok(
655
- mongoose.isObjectIdOrHexString,
656
- `The search key "${key}" is not valid object id.`
657
- );
658
- return new mongoose__default.Types.ObjectId(value);
659
- },
660
- /**
661
- * Validates and converts the value to a Date.
662
- * Supports string and number inputs for conversion.
663
- * If the value is `null`, it returns `null` without throwing an error.
664
- *
665
- * @throws {Error} If the value is not a valid date.
666
- */
667
- DATE: (value, key) => {
668
- if (value === null) return null;
669
- const result = toolkit.isString(value) ? new Date(value) : toolkit.isNumber(value) ? new Date(value) : void 0;
670
- toolkit.assert.date(result, `The search key "${key}" is not valid date.`);
671
- return result;
672
- }
673
- });
674
- const INSTANCE_TO_TRANSFORM = {
675
- Number: [MONGO_TRANSFORM.NUMBER],
676
- Decimal128: [MONGO_TRANSFORM.NUMBER],
677
- String: [MONGO_TRANSFORM.STRING],
678
- UUID: [MONGO_TRANSFORM.STRING],
679
- ObjectId: [MONGO_TRANSFORM.OBJECT_ID],
680
- Boolean: [MONGO_TRANSFORM.BOOLEAN],
681
- Date: [MONGO_TRANSFORM.DATE]
682
- };
683
- function parseToMongoose(reference, input, options = {}) {
684
- const opts = mergeOptions(
685
- extractFromSchema("schema" in reference ? reference.schema : reference),
686
- options
687
- );
688
- const result = {};
689
- const ops = new OpsResource(opts.maxOps);
690
- const exp = new Expression(input).parse();
691
- for (const node of exp.body) {
692
- Object.assign(result, reduceNode(node, opts, ops));
693
- }
694
- return result;
695
- }
696
- const extractFromSchema = toolkit.withCache(
697
- { objectStrategy: "ref" },
698
- (schema) => {
699
- const allowedKeys = [];
700
- const transform = {};
701
- eachPath(schema, (path, type) => {
702
- transform[path] = INSTANCE_TO_TRANSFORM[type.instance];
703
- allowedKeys.push(path);
704
- });
705
- return { allowedKeys, transform };
706
- }
707
- );
708
- function eachPath(schema, fn, prefix = "") {
709
- schema.eachPath((path, type) => {
710
- if (type.schema) {
711
- eachPath(type.schema, fn, `${path}.`);
712
- } else {
713
- fn(`${prefix}${path}`, type);
714
- }
715
- });
716
- }
717
-
718
- exports.Expression = Expression;
719
- exports.KEYWORDS = KEYWORDS;
720
- exports.MONGO_TRANSFORM = MONGO_TRANSFORM;
721
- exports.NODE = NODE;
722
- exports.TOKEN = TOKEN;
723
- exports.Tokenizer = Tokenizer;
724
- exports.parseQuery = parseQuery;
725
- exports.parseToMongo = parseToMongo;
726
- exports.parseToMongoose = parseToMongoose;
727
- //# sourceMappingURL=index.cjs.map