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 potent tool for expressive and efficient data querying.
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/App.md#with-database)
86
+ [How to setup a database →](docs/Application.md#with-database)
87
87
 
88
88
 
89
89
  ### Application
90
- [Application documentation →](docs/App.md)
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 params of OP token,
111
- // or subquery of a model:
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 end of OP token params,
148
- // or end of subquery:
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
- tree.node.addWhere(fullOp);
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
- this.setNodeParam(tree.node, token, value);
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 n-th value in value array,
229
- // or horizontal include:
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
- // If param value:
260
- if (tree.node.activeParam !== PARAM_TOKENS.INCLUDES) {
261
- value.push(token);
262
- token = '';
285
+ // Just quit from subquery:
286
+ if (token.length === 0) {
263
287
  continue;
264
288
  }
265
289
 
266
- // Just quit from subquery:
267
- if (token.length === 0) {
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 vertical include
292
- // or it can be a part of param for "where":
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 the end of key=value pair in root and sub query,
351
- // or the end of subincludes:
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
- // Set value.
367
- this.setNodeParam(tree.node, token, value);
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 start of 'in'/'notIn',
431
- // or 'notIn':
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 end of 'in'/'notIn':
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 -1:
199
- if (_value === -1)
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 === 0)
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 (let [ attribute, value ] of whereEntries) {
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 (let [ attribute, staticValue ] of staticAttributesEntries) {
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
- const [ opKey, rawValue ] = (Object.entries(value))[0];
20
-
21
- const op = Op[opKey];
22
- return { [op]: rawValue };
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodester",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "A versatile REST framework for Node.js",
5
5
  "directories": {
6
6
  "docs": "docs",
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: '?functions=count(comments)',
583
- count_short: '?fn=count(comments)',
665
+ count_long: 'functions=count(comments)',
666
+ count_short: 'fn=count(comments)',
584
667
 
585
- count_and_includes: '?fn=count(comments)&in=comments',
668
+ count_and_includes: 'fn=count(comments)&in=comments',
586
669
  }
587
670
 
588
671
  test('Count (full key name)', () => {