nodester 0.6.1 → 0.6.3
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
|
@@ -40,7 +40,7 @@ const app = new nodester();
|
|
|
40
40
|
app.set.database(db);
|
|
41
41
|
|
|
42
42
|
// Optional beforeStart hook:
|
|
43
|
-
app.beforeStart(async ()=>{
|
|
43
|
+
app.beforeStart(async () => {
|
|
44
44
|
// Do any asynchronous initializations
|
|
45
45
|
// before app.listen
|
|
46
46
|
// ...
|
|
@@ -71,7 +71,7 @@ process.once('SIGTERM', () => {
|
|
|
71
71
|
|
|
72
72
|
|
|
73
73
|
### Queries & querying - nodester Query Language (NQL)
|
|
74
|
-
The true strength of nodester lies in its query language. Serving as an extension of standard REST API syntax, it brings many aspects of SQL into REST requests, providing developers with a simple yet
|
|
74
|
+
The true strength of nodester lies in its query language. Serving as an extension of standard REST API syntax, it brings many aspects of SQL into REST requests, providing developers with a simple yet powerful tool for expressive and efficient data querying.
|
|
75
75
|
|
|
76
76
|
Read more about it in the documentation:
|
|
77
77
|
[NQL documentaion →](docs/nql/Introduction.md)
|
|
@@ -83,11 +83,11 @@ Supported drivers:
|
|
|
83
83
|
- MySQL
|
|
84
84
|
- PostgreSQL
|
|
85
85
|
|
|
86
|
-
[How to setup a database →](docs/
|
|
86
|
+
[How to setup a database →](docs/Application.md#with-database)
|
|
87
87
|
|
|
88
88
|
|
|
89
89
|
### Application
|
|
90
|
-
[Application documentation →](docs/
|
|
90
|
+
[Application documentation →](docs/Application.md)
|
|
91
91
|
|
|
92
92
|
|
|
93
93
|
### Comments
|
|
@@ -99,7 +99,7 @@ module.exports = class QueryLexer {
|
|
|
99
99
|
|
|
100
100
|
// Token is a String, accumulated char-by-char.
|
|
101
101
|
let token = '';
|
|
102
|
-
// Value of param ('id=10' OR 'attributes=id,text').
|
|
102
|
+
// Value of a param ('id=10' OR 'attributes=id,text').
|
|
103
103
|
let value = [];
|
|
104
104
|
// Model, that was active before a cursor went up in the tree.
|
|
105
105
|
let previousActive = null;
|
|
@@ -107,8 +107,10 @@ module.exports = class QueryLexer {
|
|
|
107
107
|
for (let i=startAt; i < queryString.length; i++) {
|
|
108
108
|
const char = queryString[i];
|
|
109
109
|
|
|
110
|
-
// ( can mean
|
|
111
|
-
//
|
|
110
|
+
// ( can mean:
|
|
111
|
+
// • params of OP token
|
|
112
|
+
// • arguments of a function
|
|
113
|
+
// • subquery of a model
|
|
112
114
|
if (char === '(') {
|
|
113
115
|
debug('char', char, { token, node: tree.node });
|
|
114
116
|
|
|
@@ -144,8 +146,10 @@ module.exports = class QueryLexer {
|
|
|
144
146
|
continue;
|
|
145
147
|
}
|
|
146
148
|
|
|
147
|
-
// ) can mean
|
|
148
|
-
//
|
|
149
|
+
// ) can mean:
|
|
150
|
+
// • end of OP token params
|
|
151
|
+
// • end of a function arguments
|
|
152
|
+
// • end of a subquery of a model
|
|
149
153
|
if (char === ')') {
|
|
150
154
|
debug('char', char, { token, node: tree.node });
|
|
151
155
|
|
|
@@ -176,11 +180,29 @@ module.exports = class QueryLexer {
|
|
|
176
180
|
break;
|
|
177
181
|
}
|
|
178
182
|
|
|
179
|
-
|
|
183
|
+
// If this param is already in WHERE,
|
|
184
|
+
// treat it like SQL AND:
|
|
185
|
+
if (tree.node.where[tree.node.activeParam]) {
|
|
186
|
+
// If this OP is already in the set:
|
|
187
|
+
if (tree.node.where[tree.node.activeParam][tree.node.op]) {
|
|
188
|
+
tree.node.where[tree.node.activeParam][tree.node.op] = [
|
|
189
|
+
...tree.node.where[tree.node.activeParam][tree.node.op],
|
|
190
|
+
...fullOp[tree.node.activeParam][tree.node.op]
|
|
191
|
+
]
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
tree.node.where[tree.node.activeParam] = {
|
|
195
|
+
...tree.node.where[tree.node.activeParam],
|
|
196
|
+
...fullOp[tree.node.activeParam]
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
tree.node.addWhere(fullOp);
|
|
202
|
+
}
|
|
180
203
|
|
|
181
204
|
// Reset:
|
|
182
205
|
tree.node.resetOP();
|
|
183
|
-
tree.node.activeParam = PARAM_TOKENS.INCLUDES;
|
|
184
206
|
token = '';
|
|
185
207
|
value = [];
|
|
186
208
|
continue;
|
|
@@ -211,8 +233,10 @@ module.exports = class QueryLexer {
|
|
|
211
233
|
|
|
212
234
|
// If end of subquery:
|
|
213
235
|
if (!!tree.node.activeParam && tree.node.activeParam !== PARAM_TOKENS.INCLUDES) {
|
|
214
|
-
// Set value
|
|
215
|
-
|
|
236
|
+
// Set value:
|
|
237
|
+
if (token.length > 0) {
|
|
238
|
+
this.setNodeParam(tree.node, token, value);
|
|
239
|
+
}
|
|
216
240
|
|
|
217
241
|
// Reset:
|
|
218
242
|
tree.node.resetActiveParam();
|
|
@@ -225,8 +249,10 @@ module.exports = class QueryLexer {
|
|
|
225
249
|
return [ numberOfProcessedChars ];
|
|
226
250
|
}
|
|
227
251
|
|
|
228
|
-
// , can mean
|
|
229
|
-
//
|
|
252
|
+
// , can mean:
|
|
253
|
+
// • new param in a Op params array
|
|
254
|
+
// • new argument for a function
|
|
255
|
+
// • new horizontal include
|
|
230
256
|
if (char === ',') {
|
|
231
257
|
debug('char', char, { token, node: tree.node });
|
|
232
258
|
|
|
@@ -249,22 +275,22 @@ module.exports = class QueryLexer {
|
|
|
249
275
|
continue;
|
|
250
276
|
}
|
|
251
277
|
|
|
252
|
-
// If new function:
|
|
278
|
+
// If a new function:
|
|
253
279
|
if (tree.node.activeParam === PARAM_TOKENS.FUNCTIONS) {
|
|
254
|
-
// Prepare for new function:
|
|
280
|
+
// Prepare for a new function:
|
|
255
281
|
tree.node.resetFN();
|
|
256
282
|
continue;
|
|
257
283
|
}
|
|
258
284
|
|
|
259
|
-
//
|
|
260
|
-
if (
|
|
261
|
-
value.push(token);
|
|
262
|
-
token = '';
|
|
285
|
+
// Just quit from subquery:
|
|
286
|
+
if (token.length === 0) {
|
|
263
287
|
continue;
|
|
264
288
|
}
|
|
265
289
|
|
|
266
|
-
//
|
|
267
|
-
if (
|
|
290
|
+
// If param is a value:
|
|
291
|
+
if (tree.node.activeParam !== PARAM_TOKENS.INCLUDES) {
|
|
292
|
+
value.push(token);
|
|
293
|
+
token = '';
|
|
268
294
|
continue;
|
|
269
295
|
}
|
|
270
296
|
|
|
@@ -274,7 +300,7 @@ module.exports = class QueryLexer {
|
|
|
274
300
|
tree.use(model) ?? tree.include(model);
|
|
275
301
|
|
|
276
302
|
// Last token (model) was included,
|
|
277
|
-
// now jump to root and proceed to collect next token (model).
|
|
303
|
+
// now jump to the root and proceed to collect the next token (model).
|
|
278
304
|
tree.node.resetActiveParam();
|
|
279
305
|
tree.upToRoot();
|
|
280
306
|
|
|
@@ -288,8 +314,9 @@ module.exports = class QueryLexer {
|
|
|
288
314
|
throw err;
|
|
289
315
|
}
|
|
290
316
|
|
|
291
|
-
// . can mean
|
|
292
|
-
//
|
|
317
|
+
// . can mean:
|
|
318
|
+
// • vertical include
|
|
319
|
+
// • or it can be a part of a param for "where"
|
|
293
320
|
if (char === '.') {
|
|
294
321
|
debug('char', char, { token, node: tree.node });
|
|
295
322
|
|
|
@@ -302,7 +329,7 @@ module.exports = class QueryLexer {
|
|
|
302
329
|
continue;
|
|
303
330
|
}
|
|
304
331
|
|
|
305
|
-
// If include of new model:
|
|
332
|
+
// If include of a new model:
|
|
306
333
|
if (token.length > 0) {
|
|
307
334
|
const model = token;
|
|
308
335
|
tree.use(model) ?? tree.include(model).use(model);
|
|
@@ -322,7 +349,7 @@ module.exports = class QueryLexer {
|
|
|
322
349
|
if (char === '+') {
|
|
323
350
|
debug('char', char, { token, node: tree.node });
|
|
324
351
|
|
|
325
|
-
// If include of new model:
|
|
352
|
+
// If include of a new model:
|
|
326
353
|
if (token.length > 0) {
|
|
327
354
|
const model = token;
|
|
328
355
|
// Include, but do not use:
|
|
@@ -347,8 +374,9 @@ module.exports = class QueryLexer {
|
|
|
347
374
|
continue;
|
|
348
375
|
}
|
|
349
376
|
|
|
350
|
-
// & can mean
|
|
351
|
-
//
|
|
377
|
+
// & can mean:
|
|
378
|
+
// • the end of key=value pair in root and sub query
|
|
379
|
+
// • the end of subincludes
|
|
352
380
|
if (char === '&') {
|
|
353
381
|
debug('char', char, { token, node: tree.node });
|
|
354
382
|
|
|
@@ -358,13 +386,16 @@ module.exports = class QueryLexer {
|
|
|
358
386
|
throw err;
|
|
359
387
|
}
|
|
360
388
|
|
|
361
|
-
// If end of key=value pair:
|
|
389
|
+
// If end of a key=value pair:
|
|
362
390
|
if (!!tree.node.activeParam
|
|
363
391
|
&& tree.node.activeParam !== PARAM_TOKENS.FUNCTIONS
|
|
364
392
|
&& tree.node.activeParam !== PARAM_TOKENS.INCLUDES
|
|
365
393
|
) {
|
|
366
|
-
|
|
367
|
-
|
|
394
|
+
if (token.length > 0) {
|
|
395
|
+
// Set value.
|
|
396
|
+
this.setNodeParam(tree.node, token, value);
|
|
397
|
+
}
|
|
398
|
+
|
|
368
399
|
// Reset:
|
|
369
400
|
tree.node.resetActiveParam();
|
|
370
401
|
token = '';
|
|
@@ -427,8 +458,8 @@ module.exports = class QueryLexer {
|
|
|
427
458
|
throw err;
|
|
428
459
|
}
|
|
429
460
|
|
|
430
|
-
// [ can mean
|
|
431
|
-
//
|
|
461
|
+
// [ can mean:
|
|
462
|
+
// • start of 'in' / 'notIn'
|
|
432
463
|
if (char === '[') {
|
|
433
464
|
tree.node.op = OP_TOKENS.IN;
|
|
434
465
|
if (token.length > 0) {
|
|
@@ -446,7 +477,8 @@ module.exports = class QueryLexer {
|
|
|
446
477
|
continue;
|
|
447
478
|
}
|
|
448
479
|
|
|
449
|
-
// ] can mean
|
|
480
|
+
// ] can mean:
|
|
481
|
+
// • end of 'in' / 'notIn'
|
|
450
482
|
if (char === ']') {
|
|
451
483
|
// User missed first '[' :
|
|
452
484
|
if (
|
|
@@ -477,7 +509,7 @@ module.exports = class QueryLexer {
|
|
|
477
509
|
continue;
|
|
478
510
|
}
|
|
479
511
|
|
|
480
|
-
// = can only mean the end of param name:
|
|
512
|
+
// = can only mean the end of a param name:
|
|
481
513
|
if (char === '=') {
|
|
482
514
|
const param = this.parseParamFromToken(token);
|
|
483
515
|
|
|
@@ -153,6 +153,7 @@ function traverse(queryNode, filter=null, model=null, association=null) {
|
|
|
153
153
|
|
|
154
154
|
newQuery
|
|
155
155
|
);
|
|
156
|
+
break;
|
|
156
157
|
}
|
|
157
158
|
// Any other function:
|
|
158
159
|
default:
|
|
@@ -195,8 +196,8 @@ function traverse(queryNode, filter=null, model=null, association=null) {
|
|
|
195
196
|
case 'limit': {
|
|
196
197
|
const _value = _setValueWithBounds(value, 'number', filter.bounds.clauses.limit);
|
|
197
198
|
|
|
198
|
-
// Do not set if
|
|
199
|
-
if (_value
|
|
199
|
+
// Do not set if negative:
|
|
200
|
+
if (_value < 0)
|
|
200
201
|
continue;
|
|
201
202
|
|
|
202
203
|
newQuery.limit = _value;
|
|
@@ -205,8 +206,8 @@ function traverse(queryNode, filter=null, model=null, association=null) {
|
|
|
205
206
|
case 'skip': {
|
|
206
207
|
const _value = _setValueWithBounds(value, 'number', filter.bounds.clauses.skip);
|
|
207
208
|
|
|
208
|
-
// Do not set if 0:
|
|
209
|
-
if (_value
|
|
209
|
+
// Do not set if 0 or negative:
|
|
210
|
+
if (_value <= 0)
|
|
210
211
|
continue;
|
|
211
212
|
|
|
212
213
|
newQuery.offset = _value;
|
|
@@ -329,14 +330,14 @@ function traverse(queryNode, filter=null, model=null, association=null) {
|
|
|
329
330
|
|
|
330
331
|
// Set aatributes from Query:
|
|
331
332
|
const whereEntries = Object.entries(where);
|
|
332
|
-
for (
|
|
333
|
-
parseWhereEntry(attribute, value, newQuery.where);
|
|
333
|
+
for (const [ attribute, value ] of whereEntries) {
|
|
334
|
+
parseWhereEntry(attribute, value, newQuery.where, _model);
|
|
334
335
|
}
|
|
335
336
|
|
|
336
337
|
// Static attributes override previously set attributes:
|
|
337
338
|
const staticAttributesEntries = Object.entries(filter.statics.attributes);
|
|
338
|
-
for (
|
|
339
|
-
newQuery.where[attribute] = parseValue(staticValue, attribute);
|
|
339
|
+
for (const [ attribute, staticValue ] of staticAttributesEntries) {
|
|
340
|
+
newQuery.where[attribute] = parseValue(staticValue, attribute, _model);
|
|
340
341
|
}
|
|
341
342
|
|
|
342
343
|
// If "where" was not set in any way,
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
'use strict';
|
|
7
7
|
|
|
8
8
|
const { Op } = require('sequelize');
|
|
9
|
+
const { DataTypes } = require('sequelize');
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
module.exports = {
|
|
@@ -13,31 +14,50 @@ module.exports = {
|
|
|
13
14
|
parseWhereEntry: _parseWhereEntry,
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
function _parseValue(value, attribute) {
|
|
17
|
+
function _parseValue(value, attribute, model) {
|
|
17
18
|
// If value is Object:
|
|
18
19
|
if (typeof value === 'object' && Array.isArray(value) === false) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
20
|
+
// Combine all OPs into one query:
|
|
21
|
+
const allOPs = {};
|
|
22
|
+
const entries = Object.entries(value);
|
|
23
|
+
for (const [ opKey, rawValue ] of entries) {
|
|
24
|
+
const op = Op[opKey];
|
|
25
|
+
|
|
26
|
+
let _value = rawValue;
|
|
27
|
+
|
|
28
|
+
// Sequilize does not allow Op comparisons of dates
|
|
29
|
+
// without converting the value to the Date object:
|
|
30
|
+
switch(model.tableAttributes[attribute].type.key) {
|
|
31
|
+
case DataTypes.DATE.key:
|
|
32
|
+
case DataTypes.DATEONLY.key:
|
|
33
|
+
_value = new Date(rawValue);
|
|
34
|
+
break;
|
|
35
|
+
|
|
36
|
+
default:
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
allOPs[op] = _value;
|
|
41
|
+
}
|
|
42
|
+
return allOPs;
|
|
23
43
|
}
|
|
24
44
|
|
|
25
45
|
return value;
|
|
26
46
|
}
|
|
27
47
|
|
|
28
|
-
function _parseWhereEntry(attribute, value, whereHolder) {
|
|
48
|
+
function _parseWhereEntry(attribute, value, whereHolder, model) {
|
|
29
49
|
let _value = value;
|
|
30
50
|
|
|
31
51
|
// If attribute is Op (not, like, or, etc.):
|
|
32
52
|
if (attribute in Op) {
|
|
33
53
|
// Parse value:
|
|
34
|
-
_value = _parseValue(_value, attribute);
|
|
54
|
+
_value = _parseValue(_value, attribute, model);
|
|
35
55
|
|
|
36
56
|
const op = Op[attribute];
|
|
37
57
|
whereHolder[op] = _value;
|
|
38
58
|
return;
|
|
39
59
|
}
|
|
40
60
|
|
|
41
|
-
whereHolder[attribute] = _parseValue(_value, attribute);
|
|
61
|
+
whereHolder[attribute] = _parseValue(_value, attribute, model);
|
|
42
62
|
}
|
|
43
63
|
|
package/package.json
CHANGED
package/tests/nql.test.js
CHANGED
|
@@ -576,13 +576,96 @@ describe('nodester Query Language', () => {
|
|
|
576
576
|
});
|
|
577
577
|
});
|
|
578
578
|
|
|
579
|
+
describe('operators:and', () => {
|
|
580
|
+
const queryStrings = {
|
|
581
|
+
and_simple: 'id=gte(2),lt(5)',
|
|
582
|
+
and_more_op: 'title=like(book),notLike(book #3),notLike(book #4)',
|
|
583
|
+
|
|
584
|
+
and_in_subincludes_0: 'in=comments(text=like(hi),notLike(hi!))',
|
|
585
|
+
and_in_subincludes_1: 'id=4&in=comments(text=like(hi),notLike(hi!))',
|
|
586
|
+
and_in_subincludes_2: 'title=like(book),notLike(book #3)&in=comments(text=like(hi),notLike(hi!))',
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
test('AND (simple)', () => {
|
|
590
|
+
const lexer = new QueryLexer( queryStrings.and_simple );
|
|
591
|
+
const result = lexer.query;
|
|
592
|
+
|
|
593
|
+
const tree = new ModelsTree();
|
|
594
|
+
tree.node.addWhere({ id: { gte: ['2'], lt: ['5'] }});
|
|
595
|
+
const expected = tree.root.toObject();
|
|
596
|
+
|
|
597
|
+
expect(result).toMatchObject(expected);
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
test('AND (more OP)', () => {
|
|
601
|
+
const lexer = new QueryLexer( queryStrings.and_more_op );
|
|
602
|
+
const result = lexer.query;
|
|
603
|
+
|
|
604
|
+
const tree = new ModelsTree();
|
|
605
|
+
tree.node.addWhere({
|
|
606
|
+
title: { like: ['book'], notLike: ['book #3', 'book #4'] }
|
|
607
|
+
});
|
|
608
|
+
const expected = tree.root.toObject();
|
|
609
|
+
|
|
610
|
+
expect(result).toMatchObject(expected);
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
test('AND (in subincludes #0)', () => {
|
|
614
|
+
const lexer = new QueryLexer( queryStrings.and_in_subincludes_0 );
|
|
615
|
+
const result = lexer.query;
|
|
616
|
+
|
|
617
|
+
const tree = new ModelsTree();
|
|
618
|
+
tree.include('comments').use('comments');
|
|
619
|
+
tree.node.addWhere({
|
|
620
|
+
text: { like: ['hi'], notLike: ['hi!'] }
|
|
621
|
+
});
|
|
622
|
+
const expected = tree.root.toObject();
|
|
623
|
+
|
|
624
|
+
expect(result).toMatchObject(expected);
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
test('AND (in subincludes #1)', () => {
|
|
628
|
+
const lexer = new QueryLexer( queryStrings.and_in_subincludes_1 );
|
|
629
|
+
const result = lexer.query;
|
|
630
|
+
|
|
631
|
+
const tree = new ModelsTree();
|
|
632
|
+
tree.node.addWhere({
|
|
633
|
+
id: ["4"]
|
|
634
|
+
});
|
|
635
|
+
tree.include('comments').use('comments');
|
|
636
|
+
tree.node.addWhere({
|
|
637
|
+
text: { like: ['hi'], notLike: ['hi!'] }
|
|
638
|
+
});
|
|
639
|
+
const expected = tree.root.toObject();
|
|
640
|
+
|
|
641
|
+
expect(result).toMatchObject(expected);
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
test('AND (in subincludes #2)', () => {
|
|
645
|
+
const lexer = new QueryLexer( queryStrings.and_in_subincludes_2 );
|
|
646
|
+
const result = lexer.query;
|
|
647
|
+
|
|
648
|
+
const tree = new ModelsTree();
|
|
649
|
+
tree.node.addWhere({
|
|
650
|
+
title: { like: ['book'], notLike: ['book #3'] }
|
|
651
|
+
});
|
|
652
|
+
tree.include('comments').use('comments');
|
|
653
|
+
tree.node.addWhere({
|
|
654
|
+
text: { like: ['hi'], notLike: ['hi!'] }
|
|
655
|
+
});
|
|
656
|
+
const expected = tree.root.toObject();
|
|
657
|
+
|
|
658
|
+
expect(result).toMatchObject(expected);
|
|
659
|
+
});
|
|
660
|
+
});
|
|
661
|
+
|
|
579
662
|
|
|
580
663
|
describe('functions', () => {
|
|
581
664
|
const queryStrings = {
|
|
582
|
-
count_long: '
|
|
583
|
-
count_short: '
|
|
665
|
+
count_long: 'functions=count(comments)',
|
|
666
|
+
count_short: 'fn=count(comments)',
|
|
584
667
|
|
|
585
|
-
count_and_includes: '
|
|
668
|
+
count_and_includes: 'fn=count(comments)&in=comments',
|
|
586
669
|
}
|
|
587
670
|
|
|
588
671
|
test('Count (full key name)', () => {
|