nodester 0.2.3 → 0.2.5

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
@@ -3,9 +3,9 @@
3
3
  [![NPM version](https://img.shields.io/npm/v/nodester)](https://www.npmjs.com/package/nodester)
4
4
  [![License](https://img.shields.io/npm/l/nodester)](https://www.npmjs.com/package/nodester)
5
5
 
6
- > **nodester** is a modern and versatile Node.js framework designed to streamline the development of robust and scalable web applications.
6
+ > **nodester** is a Node.js framework designed to solve the problem of a complex data querying over HTTP.
7
7
 
8
- The main reason of nodester's existence is the [nodester Query Language (NQL)](docs/Queries.md), an extension of standard REST API syntax, it lets you craft complex queries with hierarchical associations.
8
+ The main reason of nodester's existence is the [nodester Query Language (NQL)](docs/nql/Introduction.md), an extension of standard REST API syntax, it lets you craft complex queries with hierarchical associations.
9
9
 
10
10
 
11
11
  ## Installation
@@ -49,11 +49,11 @@ app.listen(8080, function() {
49
49
  [Core concepts documentation ➡️](docs/CoreConcepts.md)
50
50
 
51
51
 
52
- ### Queries & Querying - Nodester Query Language (NQL)
52
+ ### Queries & Querying - nodester Query Language (NQL)
53
53
  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.
54
54
 
55
55
  Read more about it in the documentation:
56
- [NQL documentaion ➡️](docs/Queries.md)
56
+ [NQL documentaion ➡️](docs/nql/Introduction.md)
57
57
 
58
58
 
59
59
  ### Database
@@ -62,29 +62,27 @@ function _createGenericResponse(res, options) {
62
62
 
63
63
  switch(error.name) {
64
64
  case 'Unauthorized': {
65
- statusCode = 401;
65
+ status = 401;
66
66
  break;
67
67
  }
68
68
  case 'NotFound': {
69
- statusCode = 404;
69
+ status = 404;
70
70
  break;
71
71
  }
72
72
  case 'ValidationError': {
73
- statusCode = 422;
73
+ status = 422;
74
74
  break;
75
75
  }
76
76
  case 'ConflictError': {
77
- statusCode = 409;
77
+ status = 409;
78
78
  break;
79
79
  }
80
80
  case 'SequelizeUniqueConstraintError': {
81
- statusCode = 409;
81
+ status = 409;
82
82
  details.errors = error?.errors;
83
83
  break;
84
84
  }
85
85
  default:
86
- statusCode = status;
87
-
88
86
  if (!!error?.errors) {
89
87
  details.errors = error?.errors;
90
88
  }
@@ -121,8 +119,7 @@ function _createGenericResponse(res, options) {
121
119
  * @api public
122
120
  */
123
121
  function _createOKResponse(res, options={}) {
124
-
125
- return this.createGenericResponse(res, {
122
+ return _createGenericResponse(res, {
126
123
  ...options,
127
124
  status: options?.status ?? 200,
128
125
  });
@@ -143,8 +140,7 @@ function _createOKResponse(res, options={}) {
143
140
  * @api public
144
141
  */
145
142
  function _createErrorResponse(res, options) {
146
-
147
- return this.createGenericResponse(res, {
143
+ return _createGenericResponse(res, {
148
144
  ...options,
149
145
  status: options?.status ?? 500,
150
146
  });
@@ -11,6 +11,15 @@ const { ModelsTree, ModelsTreeNode } = require('./ModelsTree');
11
11
  const util = require('util');
12
12
  const debug = require('debug')('nodester:interpreter:QueryLexer');
13
13
 
14
+ const PARAM_TOKENS = new Enum({
15
+ FIELDS: Symbol('fields'),
16
+ INCLUDES: Symbol('includes'),
17
+ LIMIT: Symbol('limit'),
18
+ ORDER: Symbol('order'),
19
+ ORDER_BY: Symbol('order_by'),
20
+ SKIP: Symbol('skip'),
21
+ });
22
+
14
23
  const OP_TOKENS = new Enum({
15
24
  AND: 'and',
16
25
  BETWEEN: 'between',
@@ -21,8 +30,14 @@ const OP_TOKENS = new Enum({
21
30
  XOR: 'xor',
22
31
  NOT: 'not',
23
32
  NOT_MARK: '!',
33
+
34
+ IN: 'in',
24
35
  NOT_IN: 'notIn',
36
+
25
37
  LIKE: 'like',
38
+ NOT_LIKE: 'notLike',
39
+ NOT_LIKE_SHORT: '!like',
40
+
26
41
  GREATER: 'gt',
27
42
  GREATER_OR_EQUAL: 'gte',
28
43
  LOWER: 'lt',
@@ -59,13 +74,12 @@ module.exports = class QueryLexer {
59
74
 
60
75
  parseIsolatedQuery(queryString='', startAt=0, tree) {
61
76
  const isSubQuery = tree.node.model !== 'root';
62
- debug({ isSubQuery, startAt });
63
77
 
64
- // Token is String, accumulated char-by-char.
78
+ // Token is a String, accumulated char-by-char.
65
79
  let token = '';
66
80
  // Value of param ('id=10' OR 'fields=id,text').
67
81
  let value = [];
68
- // Model, that was active before cursor went up in the tree.
82
+ // Model, that was active before a cursor went up in the tree.
69
83
  let previousActive = null;
70
84
 
71
85
  for (let i=startAt; i < queryString.length; i++) {
@@ -78,7 +92,7 @@ module.exports = class QueryLexer {
78
92
 
79
93
  // If OP token:
80
94
  if (OP_TOKENS.asArray.indexOf(token) > -1) {
81
- // Set operation token.
95
+ // Set operator token.
82
96
  tree.node.op = this.parseOP(token);
83
97
  token = '';
84
98
  continue;
@@ -127,6 +141,7 @@ module.exports = class QueryLexer {
127
141
  switch (tree.node.op) {
128
142
  case OP_TOKENS.NOT:
129
143
  case OP_TOKENS.LIKE:
144
+ case OP_TOKENS.NOT_LIKE:
130
145
  case OP_TOKENS.GREATER:
131
146
  case OP_TOKENS.GREATER_OR_EQUAL:
132
147
  case OP_TOKENS.LOWER:
@@ -143,7 +158,7 @@ module.exports = class QueryLexer {
143
158
 
144
159
  // Reset:
145
160
  tree.node.resetOP();
146
- tree.node.activeParam = 'includes';
161
+ tree.node.activeParam = PARAM_TOKENS.INCLUDES;
147
162
  token = '';
148
163
  value = [];
149
164
  continue;
@@ -177,41 +192,54 @@ module.exports = class QueryLexer {
177
192
 
178
193
  // Reset:
179
194
  tree.node.resetFN();
180
- tree.node.activeParam = 'includes';
195
+ tree.node.activeParam = PARAM_TOKENS.INCLUDES;
181
196
  token = '';
182
197
  value = [];
183
198
  continue;
184
199
  }
185
200
 
186
201
  // If end of subquery:
187
- if (!!tree.node.activeParam && tree.node.activeParam !== 'includes') {
202
+ if (!!tree.node.activeParam && tree.node.activeParam !== PARAM_TOKENS.INCLUDES) {
188
203
  // Set value.
189
204
  this.setNodeParam(tree.node, token, value);
205
+
190
206
  // Reset:
191
207
  tree.node.resetActiveParam();
192
208
  tree.node.resetOP();
209
+
210
+ // Lift from subquery.
211
+ tree.up();
193
212
  }
194
213
  const numberOfProcessedChars = i - startAt;
195
214
  return [ numberOfProcessedChars ];
196
215
  }
197
216
 
198
217
  // , can mean n-th value in value array,
199
- // or it can be n-th key-value pair in subquery,
200
218
  // or horizontal include:
201
219
  if (char === ',') {
202
220
  debug('char', char, { token, node: tree.node });
203
221
 
204
222
  // If OP token:
205
223
  if (!!tree.node.op) {
206
- value.push({
207
- [tree.node.activeParam]: [token]
208
- });
224
+ switch(tree.node.op) {
225
+ case OP_TOKENS.NOT_IN:
226
+ case OP_TOKENS.IN:
227
+ value.push(token);
228
+ break;
229
+ default:
230
+ value.push({
231
+ [tree.node.activeParam]: [token]
232
+ });
233
+ break;
234
+ }
235
+
236
+ // Reset.
209
237
  token = '';
210
238
  continue;
211
239
  }
212
240
 
213
241
  // If param value:
214
- if (tree.node.activeParam !== 'includes') {
242
+ if (tree.node.activeParam !== PARAM_TOKENS.INCLUDES) {
215
243
  value.push(token);
216
244
  token = '';
217
245
  continue;
@@ -223,7 +251,7 @@ module.exports = class QueryLexer {
223
251
  }
224
252
 
225
253
  // Horizontal include:
226
- if (tree.node.activeParam === 'includes') {
254
+ if (tree.node.activeParam === PARAM_TOKENS.INCLUDES) {
227
255
  const model = token;
228
256
  tree.use(model) ?? tree.include(model);
229
257
 
@@ -232,7 +260,7 @@ module.exports = class QueryLexer {
232
260
  tree.node.resetActiveParam();
233
261
  tree.upToRoot();
234
262
 
235
- tree.node.activeParam = 'includes';
263
+ tree.node.activeParam = PARAM_TOKENS.INCLUDES;
236
264
 
237
265
  token = '';
238
266
  continue;
@@ -250,7 +278,7 @@ module.exports = class QueryLexer {
250
278
  // Vertical include:
251
279
  if (!!previousActive) {
252
280
  tree.use(previousActive);
253
- tree.node.activeParam = 'includes';
281
+ tree.node.activeParam = PARAM_TOKENS.INCLUDES;
254
282
  token = '';
255
283
  continue;
256
284
  }
@@ -261,7 +289,7 @@ module.exports = class QueryLexer {
261
289
  tree.use(model) ?? tree.include(model).use(model);
262
290
 
263
291
  // Prepare for more includes:
264
- tree.node.activeParam = 'includes';
292
+ tree.node.activeParam = PARAM_TOKENS.INCLUDES;
265
293
 
266
294
  token = '';
267
295
  continue;
@@ -283,7 +311,7 @@ module.exports = class QueryLexer {
283
311
  tree.up();
284
312
 
285
313
  // Prepare for more includes:
286
- tree.node.activeParam = 'includes';
314
+ tree.node.activeParam = PARAM_TOKENS.INCLUDES;
287
315
 
288
316
  token = '';
289
317
  continue;
@@ -295,12 +323,12 @@ module.exports = class QueryLexer {
295
323
  }
296
324
 
297
325
  tree.up();
298
- tree.node.activeParam = 'includes';
326
+ tree.node.activeParam = PARAM_TOKENS.INCLUDES;
299
327
 
300
328
  continue;
301
329
  }
302
330
 
303
- // & can mean the end of key=value pair,
331
+ // & can mean the end of key=value pair in root and sub query,
304
332
  // or the end of subincludes:
305
333
  if (char === '&') {
306
334
  debug('char', char, { token, node: tree.node });
@@ -312,7 +340,7 @@ module.exports = class QueryLexer {
312
340
  }
313
341
 
314
342
  // If end of key=value pair:
315
- if (!!tree.node.activeParam && tree.node.activeParam !== 'includes') {
343
+ if (!!tree.node.activeParam && tree.node.activeParam !== PARAM_TOKENS.INCLUDES) {
316
344
  // Set value.
317
345
  this.setNodeParam(tree.node, token, value);
318
346
  // Reset:
@@ -321,13 +349,19 @@ module.exports = class QueryLexer {
321
349
  value = [];
322
350
  continue;
323
351
  }
324
- else if (tree.node.activeParam === 'includes') {
325
- // If include of new model:
352
+ else if (tree.node.activeParam === PARAM_TOKENS.INCLUDES) {
353
+ // If token has some chars,
354
+ // then it's include of a new model:
326
355
  if (token.length > 0) {
327
356
  const model = token;
328
357
  // Just include, no use.
329
358
  tree.include(model);
330
359
  }
360
+ // If token is empty,
361
+ // it's most possibly a subquery
362
+ else {
363
+ continue;
364
+ }
331
365
 
332
366
  // Then jump to root.
333
367
  tree.upToRoot();
@@ -357,27 +391,51 @@ module.exports = class QueryLexer {
357
391
  throw err;
358
392
  }
359
393
 
360
- // [ can only mean start of 'in':
394
+ // [ can mean start of 'in'/'notIn',
395
+ // or 'notIn':
361
396
  if (char === '[') {
362
- tree.node.op = 'in';
397
+ tree.node.op = OP_TOKENS.IN;
398
+ if (token.length > 0) {
399
+ if (token === '!' || token === 'not') {
400
+ tree.node.op = OP_TOKENS.NOT_IN;
401
+ }
402
+ else {
403
+ const err = UnexpectedCharError(i - token.length, token);
404
+ throw err;
405
+ }
406
+ }
407
+
408
+ // Reset:
409
+ token = '';
363
410
  continue;
364
411
  }
365
412
 
366
- // ] can only mean end if 'in':
413
+ // ] can mean end of 'in'/'notIn':
367
414
  if (char === ']') {
368
415
  // User missed first '[' :
369
- if (tree.node.op !== 'in') {
416
+ if (
417
+ tree.node.op !== OP_TOKENS.IN
418
+ &&
419
+ tree.node.op !== OP_TOKENS.NOT_IN
420
+ ) {
370
421
  const err = UnexpectedCharError(i, char);
371
422
  throw err;
372
423
  }
373
424
 
425
+ // Token is the last element in this array:
426
+ if (token.length > 0) {
427
+ value.push(token);
428
+ }
429
+
374
430
  tree.node.addWhere({
375
431
  [tree.node.activeParam]: {
376
432
  [tree.node.op]: value
377
433
  }
378
434
  });
435
+
379
436
  // Reset:
380
437
  tree.node.resetOP();
438
+ tree.node.resetActiveParam();
381
439
  value = [];
382
440
  token = '';
383
441
  continue;
@@ -387,7 +445,7 @@ module.exports = class QueryLexer {
387
445
  if (char === '=') {
388
446
  const param = this.parseParamFromToken(token);
389
447
 
390
- if (isSubQuery === true && param === 'includes') {
448
+ if (isSubQuery === true && param === PARAM_TOKENS.INCLUDES) {
391
449
  const err = new TypeError(`'include' is forbidden inside subquery (position ${ i }). Use: 'model.submodel' or 'model.submodel1+submodel2'.`);
392
450
  throw err;
393
451
  }
@@ -433,23 +491,29 @@ module.exports = class QueryLexer {
433
491
  switch(token) {
434
492
  case 'limit':
435
493
  case 'l':
436
- return 'limit';
494
+ return PARAM_TOKENS.LIMIT;
495
+
437
496
  case 'skip':
438
497
  case 's':
439
498
  case 'offset':
440
- return 'skip';
499
+ return PARAM_TOKENS.SKIP;
500
+
441
501
  case 'order':
442
502
  case 'o':
443
- return 'order';
503
+ return PARAM_TOKENS.ORDER;
504
+
444
505
  case 'order_by':
445
506
  case 'o_by':
446
- return 'order_by';
507
+ return PARAM_TOKENS.ORDER_BY;
508
+
447
509
  case 'fields':
448
510
  case 'f':
449
- return 'fields';
511
+ return PARAM_TOKENS.FIELDS;
512
+
450
513
  case 'includes':
451
514
  case 'in':
452
- return 'includes';
515
+ return PARAM_TOKENS.INCLUDES;
516
+
453
517
  default:
454
518
  return token;
455
519
  }
@@ -458,30 +522,35 @@ module.exports = class QueryLexer {
458
522
  setNodeParam(treeNode, token, value) {
459
523
  const param = treeNode.activeParam;
460
524
 
461
- debug(`set param ${ param }`, { token, value });
525
+ debug(`set param`, { param, token, value });
462
526
 
463
527
  switch(param) {
464
- case 'limit':
528
+ case PARAM_TOKENS.LIMIT:
465
529
  treeNode.limit = parseInt(token);
466
530
  break;
467
- case 'skip':
468
- case 'offset':
531
+
532
+ case PARAM_TOKENS.SKIP:
469
533
  treeNode.skip = parseInt(token);
470
534
  break;
471
- case 'order':
535
+
536
+ case PARAM_TOKENS.ORDER:
472
537
  treeNode.order = token;
473
538
  break;
474
- case 'order_by':
539
+
540
+ case PARAM_TOKENS.ORDER_BY:
475
541
  treeNode.order_by = token;
476
542
  break;
477
- case 'fields':
543
+
544
+ case PARAM_TOKENS.FIELDS:
478
545
  if (token) value.push(token);
479
546
  treeNode.fields = value;
480
547
  break;
481
- case 'includes':
548
+
549
+ case PARAM_TOKENS.INCLUDES:
482
550
  const node = new ModelsTreeNode(token);
483
551
  treeNode.include(node);
484
552
  break;
553
+
485
554
  default:
486
555
  if (token) value.push(token);
487
556
  treeNode.addWhere({ [param]: value });
@@ -493,10 +562,16 @@ module.exports = class QueryLexer {
493
562
  switch(opToken) {
494
563
  case '|':
495
564
  case 'or':
496
- return 'or';
565
+ return OP_TOKENS.OR;
566
+
567
+ case '!like':
568
+ case 'notLike':
569
+ return OP_TOKENS.NOT_LIKE;
570
+
497
571
  case 'not':
498
572
  case '!':
499
- return 'not';
573
+ return OP_TOKENS.NOT;
574
+
500
575
  default:
501
576
  return opToken;
502
577
  }
@@ -102,49 +102,57 @@ function traverse(queryNode, filter=null, model=null) {
102
102
  // Functions:
103
103
  for (const fnParams of functions) {
104
104
 
105
- // If COUNT() is requested:
106
- if (fnParams.fn === 'count') {
107
- const countParams = fnParams.args;
108
-
109
- const [ countTarget ] = countParams;
110
- // Count can be requested for this model,
111
- // or for any of the available uncludes.
112
- const isForRootModel = countTarget === rootModelName.plural.toLowerCase();
113
-
114
- // Compile request:
115
- // Example:
116
- // `(SELECT COUNT(*) FROM comments WHERE comments.morph_id=Morph.id)`
117
-
118
- // Params for attribute:
119
- let rawSQL = '(SELECT COUNT(*) FROM ';
120
- let countAttribute = '_count';
121
-
122
- // If request to count one of the includes:
123
- if (!isForRootModel) {
124
- // Check if it's available:
125
- if (
105
+ switch(fnParams.fn) {
106
+ // SQL COUNT():
107
+ case 'count': {
108
+ const countParams = fnParams.args;
109
+
110
+ const [ countTarget ] = countParams;
111
+ // Count can be requested for this model,
112
+ // or for any of the available uncludes.
113
+ const isForRootModel = countTarget === rootModelName.plural.toLowerCase();
114
+
115
+ // Compile request:
116
+ // Example of desired SQL:
117
+ // `(SELECT COUNT(*) FROM comments WHERE comments.post_id=Post.id)`
118
+ //
119
+ let rawSQL = '(SELECT COUNT(*) FROM ';
120
+ let countAttribute = 'count';
121
+
122
+ // If request to count one of the includes:
123
+ if (!isForRootModel) {
124
+ // Check if it's available:
125
+ if (
126
126
  !filter
127
127
  ||
128
128
  !filter?.includes[countTarget]
129
129
  ||
130
130
  rootModelAssociations[countTarget] === undefined
131
131
  ) {
132
- const err = new NodesterQueryError(`Count for '${ countTarget }' is not available.`);
133
- Error.captureStackTrace(err, traverse);
134
- throw err;
132
+ const err = new NodesterQueryError(`Count for '${ countTarget }' is not available.`);
133
+ Error.captureStackTrace(err, traverse);
134
+ throw err;
135
+ }
136
+
137
+ const {
138
+ as,
139
+ target,
140
+ foreignKey,
141
+ sourceKey
142
+ } = rootModelAssociations[countTarget];
143
+ const { tableName } = target;
144
+
145
+ rawSQL += `${ tableName } where ${ tableName }.${ foreignKey }=${ rootModelName.singular }.${ sourceKey })`;
146
+ countAttribute = `${ as }_count`;
135
147
  }
136
148
 
137
- const {
138
- foreignKey,
139
- sourceKey
140
- } = rootModelAssociations[countTarget];
141
- rawSQL += `${ countTarget } where ${ countTarget }.${ foreignKey }=${ rootModelName.singular }.${ sourceKey })`;
142
- countAttribute = `${ countTarget }_count`;
149
+ newQuery.attributes.push(
150
+ [sequelize.literal(rawSQL), countAttribute]
151
+ );
143
152
  }
144
-
145
- newQuery.attributes.push(
146
- [sequelize.literal(rawSQL), countAttribute]
147
- );
153
+ // Unknow function:
154
+ default:
155
+ break;
148
156
  }
149
157
  }
150
158
  // Functions\
@@ -194,7 +202,7 @@ function traverse(queryNode, filter=null, model=null) {
194
202
  }
195
203
  }
196
204
 
197
- // "statics" override or set any query in clauses:
205
+ // Override clauses with "statics":
198
206
  if (filter !== null) {
199
207
  const staticClausesEntries = Object.entries(filter.statics.clauses);
200
208
 
@@ -228,7 +236,11 @@ function traverse(queryNode, filter=null, model=null) {
228
236
 
229
237
 
230
238
  // Order:
231
- if ( ['rand', 'random'].indexOf(order.order) > -1) {
239
+ if (
240
+ order.order === 'rand'
241
+ ||
242
+ order.order === 'random'
243
+ ) {
232
244
  newQuery.order = sequelize.random();
233
245
  }
234
246
  else {
@@ -292,11 +304,21 @@ function traverse(queryNode, filter=null, model=null) {
292
304
  }
293
305
 
294
306
 
295
- function _traverseIncludes(includes, model, filter, resultQuery) {
307
+ /**
308
+ * Traverses each include in the array.
309
+ *
310
+ * @param {Array} includes
311
+ * @param {Model} rootModel
312
+ * @param {NodesterFilter} filter
313
+ * @param {Object} resultQuery
314
+ *
315
+ * @api private
316
+ */
317
+ function _traverseIncludes(includes, rootModel, filter, resultQuery) {
296
318
  const filterIncludesEntries = Object.entries(filter.includes);
297
319
  for (let [ includeName, includeFilter ] of filterIncludesEntries) {
298
320
 
299
- const association = model.associations[includeName];
321
+ const association = rootModel.associations[includeName];
300
322
 
301
323
  // If no such association:
302
324
  if (!association) {
@@ -375,17 +397,10 @@ function _disassembleQueryNode(queryNode) {
375
397
  function _parseValue(value, attribute) {
376
398
  // If value is Object:
377
399
  if (typeof value === 'object' && Array.isArray(value) === false) {
378
- const [opKey, rawValue] = (Object.entries(value))[0];
400
+ const [ opKey, rawValue ] = (Object.entries(value))[0];
379
401
 
380
- // If operation is "in":
381
- if (opKey === 'in') {
382
- // Unwrap rawValue.
383
- return rawValue[0][attribute];
384
- }
385
- else {
386
- const op = Op[opKey];
387
- return { [op]: rawValue };
388
- }
402
+ const op = Op[opKey];
403
+ return { [op]: rawValue };
389
404
  }
390
405
 
391
406
  return value;