nodester 0.1.4 → 0.2.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.
Files changed (63) hide show
  1. package/Readme.md +16 -55
  2. package/lib/application/index.js +174 -63
  3. package/lib/body/extract.js +89 -0
  4. package/lib/constants/Bounds.js +15 -0
  5. package/lib/constants/Clauses.js +13 -0
  6. package/lib/constants/ResponseFormats.js +2 -2
  7. package/lib/controllers/methods/index.js +7 -0
  8. package/lib/controllers/mixins/index.js +36 -36
  9. package/lib/database/connection.js +6 -0
  10. package/lib/database/migration.js +14 -4
  11. package/lib/facades/methods/index.js +16 -16
  12. package/lib/facades/mixins/index.js +67 -13
  13. package/lib/factories/responses/rest.js +25 -13
  14. package/lib/http/codes/descriptions.js +82 -0
  15. package/lib/http/codes/index.js +70 -145
  16. package/lib/http/codes/symbols.js +82 -0
  17. package/lib/http/{request.js → request/index.js} +53 -75
  18. package/lib/http/request/utils.js +27 -0
  19. package/lib/http/response/headers.js +138 -0
  20. package/lib/http/response/index.js +248 -0
  21. package/lib/http/response/utils.js +38 -0
  22. package/lib/middlewares/SearchParams/index.js +25 -0
  23. package/lib/middlewares/cookies/index.js +44 -0
  24. package/lib/middlewares/etag/index.js +32 -15
  25. package/lib/middlewares/formidable/index.js +30 -25
  26. package/lib/middlewares/ql/sequelize/index.js +13 -4
  27. package/lib/middlewares/ql/sequelize/interpreter/QueryLexer.js +4 -3
  28. package/lib/middlewares/render/index.js +62 -0
  29. package/lib/models/associate.js +25 -1
  30. package/lib/models/define.js +26 -19
  31. package/lib/models/mixins.js +8 -1
  32. package/lib/{queries → query}/traverse.js +118 -77
  33. package/lib/router/handlers.util.js +1 -0
  34. package/lib/router/index.js +194 -99
  35. package/lib/router/markers.js +7 -0
  36. package/lib/router/route.js +5 -0
  37. package/lib/router/routes.util.js +16 -14
  38. package/lib/router/utils.js +7 -0
  39. package/lib/stacks/MarkersStack.js +41 -3
  40. package/lib/stacks/MiddlewaresStack.js +200 -0
  41. package/lib/structures/Enum.js +46 -0
  42. package/lib/structures/Filter.js +156 -0
  43. package/lib/structures/Params.js +55 -0
  44. package/lib/tools/sql.tool.js +7 -0
  45. package/lib/utils/objects.util.js +31 -24
  46. package/lib/utils/sanitizations.util.js +10 -4
  47. package/lib/validators/arguments.js +68 -0
  48. package/lib/validators/dates.js +7 -0
  49. package/lib/validators/numbers.js +7 -0
  50. package/package.json +20 -10
  51. package/lib/database/utils.js +0 -19
  52. package/lib/enums/Enum.js +0 -16
  53. package/lib/http/response.js +0 -1074
  54. package/lib/http/utils.js +0 -254
  55. package/lib/params/Params.js +0 -37
  56. package/lib/policies/Role.js +0 -77
  57. package/lib/policies/RoleExtracting.js +0 -97
  58. package/lib/preprocessors/BodyPreprocessor.js +0 -61
  59. package/lib/queries/Colander.js +0 -107
  60. package/lib/queries/NodesterQueryParams.js +0 -145
  61. package/lib/services/includes.service.js +0 -79
  62. package/lib/services/jwt.service.js +0 -147
  63. package/lib/stacks/MiddlewareStack.js +0 -159
@@ -0,0 +1,62 @@
1
+ /*!
2
+ * /nodester
3
+ * MIT Licensed
4
+ */
5
+
6
+ 'use strict';
7
+
8
+
9
+ module.exports = function initRenderMiddleware() {
10
+ return handle;
11
+ }
12
+
13
+
14
+ function handle(req, res, next) {
15
+ const context = { req, res, next };
16
+ res.render = _render.bind(context);
17
+ next();
18
+ }
19
+
20
+
21
+ /**
22
+ * Render `view` with the given `options` and optional callback `fn`.
23
+ * When a callback function is given a response will _not_ be made
24
+ * automatically, otherwise a response of _200_ and _text/html_ is given.
25
+ *
26
+ * Options:
27
+ *
28
+ * - `cache` boolean hinting to the engine it should cache
29
+ * - `filename` filename of the view being rendered
30
+ *
31
+ * @alias render
32
+ * @api public
33
+ */
34
+ function _render(view, options, callback) {
35
+ const app = this.req.app;
36
+ let done = callback;
37
+ let opts = options || {};
38
+
39
+ const req = this.req;
40
+ const res = this.res;
41
+ const next = this.next;
42
+
43
+ // support callback function as second arg
44
+ if (typeof options === 'function') {
45
+ done = options;
46
+ opts = {};
47
+ }
48
+
49
+ // merge res.locals
50
+ opts._locals = res.locals;
51
+
52
+ // default callback to respond
53
+ done = done || function (err, str) {
54
+ if (err)
55
+ return next(err);
56
+
57
+ res.send(str);
58
+ };
59
+
60
+ // render
61
+ app.render(view, opts, done);
62
+ };
@@ -1,5 +1,17 @@
1
+ /*!
2
+ * /nodester
3
+ * MIT Licensed
4
+ */
5
+
6
+ 'use strict';
1
7
 
2
- module.exports = async function associateModels(databaseConnection) {
8
+
9
+ module.exports = {
10
+ associateModels: _associateModels,
11
+ associateModelsSync: _associateModelsSync
12
+ }
13
+
14
+ async function _associateModels(databaseConnection) {
3
15
  try {
4
16
  const models = databaseConnection.models;
5
17
 
@@ -15,3 +27,15 @@ module.exports = async function associateModels(databaseConnection) {
15
27
  return Promise.reject(error);
16
28
  }
17
29
  }
30
+
31
+ function _associateModelsSync(databaseConnection) {
32
+ const models = databaseConnection.models;
33
+
34
+ const modelNames = Object.keys(models);
35
+
36
+ for (let modelName of modelNames) {
37
+ models[modelName].associate(models);
38
+ }
39
+
40
+ return models;
41
+ }
@@ -1,9 +1,14 @@
1
+ /*!
2
+ * /nodester
3
+ * MIT Licensed
4
+ */
5
+
6
+ 'use strict';
7
+
1
8
  // CRUD mixins.
2
9
  const { implementsCRUD } = require('./mixins');
3
10
  // ORM.
4
11
  const { DataTypes } = require('sequelize');
5
- // NQL.
6
- const Colander = require('../queries/Colander');
7
12
 
8
13
 
9
14
  module.exports = defineModel;
@@ -14,7 +19,7 @@ module.exports = defineModel;
14
19
  * @param {Function} definition
15
20
  * @param {Object} options
16
21
  * - ... Sequilize model options
17
- * - noCRUD (Bool)
22
+ * - @param {Boolean} noCRUD
18
23
  */
19
24
  function defineModel(
20
25
  databaseConnection,
@@ -26,16 +31,24 @@ function defineModel(
26
31
  const _options = {
27
32
  // Set snake-cased table name.
28
33
  // tableName: underscore( pluralize(modelName) ),
34
+
29
35
  // Set snake-case.
30
36
  underscored: true,
37
+
31
38
  // Enable automatic 'created_at' and 'updated_at' fields.
32
39
  timestamps: true,
33
40
 
34
- // The only way to get snake-cased timestamps (issue: https://github.com/sequelize/sequelize/issues/10857)
41
+ // The only way to get snake-cased timestamps:
42
+ // (issue: https://github.com/sequelize/sequelize/issues/10857)
35
43
  createdAt: 'created_at',
36
44
  updatedAt: 'updated_at',
37
45
  deletedAt: 'deleted_at',
38
46
 
47
+ // Configs related to nodester:
48
+ nodester: {
49
+ output: 'underscored'
50
+ },
51
+
39
52
  // Add user-defined options (they can override upper ones).
40
53
  ...options
41
54
  };
@@ -52,7 +65,7 @@ function defineModel(
52
65
  implementsCRUD(model);
53
66
  }
54
67
 
55
- // Association helpers:
68
+ // Associations:
56
69
  model.associate = (models) => {};
57
70
  model.getIncludesList = _getIncludesList.bind(model);
58
71
 
@@ -61,7 +74,6 @@ function defineModel(
61
74
  const values = { ...this.get() };
62
75
  return values;
63
76
  }
64
- // Instance methods\
65
77
 
66
78
  return model;
67
79
  }
@@ -73,11 +85,8 @@ function _getIncludesList(facadeData=null) {
73
85
  const associations = this.associations;
74
86
  const associationEntries = Object.entries(associations);
75
87
 
76
- associationEntries.forEach(([
77
- associationName,
78
- associationDefinition
79
- ]) => {
80
- const a = { association: associationName };
88
+ for (const [ associationName, associationDefinition ] of associationEntries) {
89
+ const formatted = { association: associationName };
81
90
 
82
91
  if (!!facadeData) {
83
92
  // If facade data is set, go deeper:
@@ -85,19 +94,17 @@ function _getIncludesList(facadeData=null) {
85
94
  if (keys.indexOf(associationName) > 0) {
86
95
  const associationModel = associationDefinition.target;
87
96
 
88
- const a = { association: associationName };
89
97
  if (Object.entries(associationModel.associations).length > 0) {
90
98
  const deepData = facadeData[ associationName ];
91
- a.include = associationModel.getIncludesList(Array.isArray(deepData) ? deepData[0] : deepData);
99
+ formatted.include = associationModel.getIncludesList(
100
+ Array.isArray(deepData) ? deepData[0] : deepData
101
+ );
92
102
  }
93
-
94
- result.push( a );
95
103
  }
96
104
  }
97
- else {
98
- result.push( a );
99
- }
100
- });
105
+
106
+ result.push( formatted );
107
+ }
101
108
 
102
109
  return result;
103
110
  }
@@ -1,3 +1,10 @@
1
+ /*!
2
+ * /nodester
3
+ * MIT Licensed
4
+ */
5
+
6
+ 'use strict';
7
+
1
8
  /*
2
9
  * CRUD mixins for any model:
3
10
  */
@@ -10,7 +17,7 @@ const {
10
17
 
11
18
  // Nodester query:
12
19
  const NQLexer = require('../middlewares/ql/sequelize/interpreter/QueryLexer');
13
- const traverseNQuery = require('../queries/traverse');
20
+ const traverseNQuery = require('nodester/query/traverse');
14
21
 
15
22
 
16
23
  module.exports = {
@@ -2,19 +2,40 @@
2
2
  * /nodester
3
3
  * MIT Licensed
4
4
  */
5
+
5
6
  'use strict';
6
7
 
8
+ const BOUNDS = require('../constants/Bounds');
9
+
7
10
  const { Op } = require('sequelize');
8
11
  const NQueryError = require('../factories/errors/NodesterQueryError');
12
+ const httpCodes = require('nodester/http/codes');
13
+
14
+ const { ensure } = require('../validators/arguments');
9
15
 
10
16
 
11
17
  module.exports = traverse;
12
18
 
13
- function traverse(queryNode, colander=null, model) {
19
+ function traverse(queryNode, filter=null, model=null) {
20
+ const _model = model ?? filter.model;
14
21
 
15
- const sequelize = model.sequelize;
16
- const fieldsAvailable = Object.keys(model.tableAttributes);
17
- const includesAvailable = model.getIncludesList();
22
+ try {
23
+ ensure(queryNode, 'object,required', 'queryNode');
24
+ ensure(filter, 'object,required', 'filter');
25
+ if (!_model) {
26
+ const err = new TypeError(`'model' must be provided either in 'filter.model' or as a third argument.`);
27
+ throw err;
28
+ }
29
+ }
30
+ catch(error) {
31
+ Error.captureStackTrace(error, traverse);
32
+ throw error;
33
+ }
34
+
35
+ const rootModelName = _model.options.name;
36
+ const rootModelAssociations = _model.associations;
37
+ const { sequelize } = _model;
38
+ const fieldsAvailable = Object.keys(_model.tableAttributes);
18
39
 
19
40
  const newQuery = {
20
41
  attributes: [],
@@ -33,9 +54,9 @@ function traverse(queryNode, colander=null, model) {
33
54
 
34
55
  // Fields:
35
56
  //
36
- // If Colander is not set,
57
+ // If Filter is not set,
37
58
  // use every available field:
38
- if (colander === null) {
59
+ if (filter === null) {
39
60
  for (let field of fieldsAvailable) {
40
61
  // If no query filter or field is requested:
41
62
  if (fields.length === 0 || fields.indexOf(field) > -1) {
@@ -44,20 +65,21 @@ function traverse(queryNode, colander=null, model) {
44
65
  }
45
66
  }
46
67
  }
47
- // Colander is present:
68
+ // Filter is present:
48
69
  else {
49
70
  // If no query fields were set,
50
- // use the ones from Colander,
71
+ // use the ones from Filter,
51
72
  // If query fields were set,
52
- // put them through Colander:
53
- for (let field of colander.fields) {
73
+ // put them through Filter:
74
+ for (let field of filter.fields) {
54
75
  if (fieldsAvailable.indexOf(field) === -1) {
55
- const err = new TypeError(`field ${ field } is not present in model.`);
76
+ const err = new TypeError(`Field '${ field }' is not present in model.`);
77
+ err.status = httpCodes.NOT_ACCEPTABLE;
56
78
  throw err;
57
79
  }
58
80
 
59
81
  // If field is not in available set:
60
- // if (colander.fields.indexOf(field) === -1) {
82
+ // if (filter.fields.indexOf(field) === -1) {
61
83
  // continue;
62
84
  // }
63
85
 
@@ -72,6 +94,7 @@ function traverse(queryNode, colander=null, model) {
72
94
  // At least 1 field is mandatory:
73
95
  if (newQuery.attributes.length === 0) {
74
96
  const err = new TypeError(`No fields were selected.`);
97
+ err.status = httpCodes.NOT_ACCEPTABLE;
75
98
  throw err;
76
99
  }
77
100
  // Fields\
@@ -84,10 +107,9 @@ function traverse(queryNode, colander=null, model) {
84
107
  const countParams = fnParams.args;
85
108
 
86
109
  const [ countTarget ] = countParams;
87
- const RootModelName = model.options.name;
88
110
  // Count can be requested for this model,
89
111
  // or for any of the available uncludes.
90
- const isForRootModel = countTarget === RootModelName.plural.toLowerCase();
112
+ const isForRootModel = countTarget === rootModelName.plural.toLowerCase();
91
113
 
92
114
  // Compile request:
93
115
  // Example:
@@ -101,21 +123,22 @@ function traverse(queryNode, colander=null, model) {
101
123
  if (!isForRootModel) {
102
124
  // Check if it's available:
103
125
  if (
104
- !colander
126
+ !filter
105
127
  ||
106
- !colander?.includes[countTarget]
128
+ !filter?.includes[countTarget]
107
129
  ||
108
- model.associations[countTarget] === undefined
130
+ rootModelAssociations[countTarget] === undefined
109
131
  ) {
110
- const err = new NQueryError(`Count for ${ countTarget } is not available.`);
132
+ const err = new NQueryError(`Count for '${ countTarget }' is not available.`);
133
+ err.status = httpCodes.NOT_ACCEPTABLE;
111
134
  throw err;
112
135
  }
113
136
 
114
137
  const {
115
138
  foreignKey,
116
139
  sourceKey
117
- } = model.associations[countTarget];
118
- rawSQL += `${ countTarget } where ${ countTarget }.${ foreignKey }=${ RootModelName.singular }.${ sourceKey })`;
140
+ } = rootModelAssociations[countTarget];
141
+ rawSQL += `${ countTarget } where ${ countTarget }.${ foreignKey }=${ rootModelName.singular }.${ sourceKey })`;
119
142
  countAttribute = `${ countTarget }_count`;
120
143
  }
121
144
 
@@ -132,28 +155,32 @@ function traverse(queryNode, colander=null, model) {
132
155
  const clausesEntries = Object.entries(clauses);
133
156
  for (let [clauseName, value] of clausesEntries) {
134
157
  // If clause is not available:
135
- if (colander != null) {
136
- if (colander.clauses.indexOf(clauseName) === -1)
158
+ if (filter != null) {
159
+ if (filter.clauses.indexOf(clauseName) === -1)
137
160
  continue;
138
161
  }
139
162
 
140
163
  switch(clauseName) {
141
- case 'limit':
164
+ case 'limit': {
165
+ const _value = _setValueWithBounds(value, 'number', filter.bounds.clauses.limit);
166
+
142
167
  // Do not set if -1:
143
- if (value === -1)
168
+ if (_value === -1)
144
169
  continue;
145
170
 
146
- newQuery.limit = value;
171
+ newQuery.limit = _value;
147
172
  continue;
173
+ }
174
+ case 'skip': {
175
+ const _value = _setValueWithBounds(value, 'number', filter.bounds.clauses.skip);
148
176
 
149
- case 'skip':
150
177
  // Do not set if 0:
151
- if (value === 0)
178
+ if (_value === 0)
152
179
  continue;
153
180
 
154
- newQuery.offset = value;
181
+ newQuery.offset = _value;
155
182
  continue;
156
-
183
+ }
157
184
  case 'order':
158
185
  order.order = value;
159
186
  continue;
@@ -167,9 +194,10 @@ function traverse(queryNode, colander=null, model) {
167
194
  }
168
195
  }
169
196
 
170
- // "statics" override or set any query Clause:
171
- if (colander !== null) {
172
- const staticClausesEntries = Object.entries(colander.statics.clauses);
197
+ // "statics" override or set any query in clauses:
198
+ if (filter !== null) {
199
+ const staticClausesEntries = Object.entries(filter.statics.clauses);
200
+
173
201
  for (let entry of staticClausesEntries) {
174
202
  const [clauseName, staticClauseValue] = entry;
175
203
 
@@ -191,6 +219,11 @@ function traverse(queryNode, colander=null, model) {
191
219
  }
192
220
  }
193
221
  }
222
+
223
+ // Check for undefined clauses:
224
+ if (newQuery.limit === undefined && typeof filter.bounds.clauses.limit === 'object') {
225
+ newQuery.limit = filter.bounds.clauses.limit.max ?? BOUNDS.limit.max;
226
+ }
194
227
  // Clauses\
195
228
 
196
229
 
@@ -229,27 +262,24 @@ function traverse(queryNode, colander=null, model) {
229
262
 
230
263
  // Includes:
231
264
  // If requested includes are not available:
232
- const leftIncludes = includesAvailable.map(i => i.association);
233
265
  for (let include of includes) {
234
266
  const includeName = include.model;
235
-
236
- const includeIndex = leftIncludes.indexOf(includeName);
237
- if (includeIndex === -1) {
238
- const err = new TypeError(`No include named ${ includeName }`);
267
+ if (rootModelAssociations[includeName] === undefined) {
268
+ const err = new NQueryError(`No include named '${ includeName }'.`);
269
+ err.status = httpCodes.NOT_ACCEPTABLE;
270
+ Error.captureStackTrace(err, traverse);
239
271
  throw err;
240
272
  }
241
-
242
- leftIncludes.splice(includeIndex, 1);
243
273
  }
244
274
 
245
- _traverseIncludes(includes, model, colander, newQuery)
275
+ _traverseIncludes(includes, _model, filter, newQuery)
246
276
  // Includes\
247
277
 
248
278
 
249
279
  // Where:
250
280
  const whereEntries = Object.entries(where);
251
281
  for (let [attribute, value] of whereEntries) {
252
- _parseWhereEntry(attribute, value, newQuery.where, colander.statics.attributes);
282
+ _parseWhereEntry(attribute, value, newQuery.where, filter.statics.attributes);
253
283
  }
254
284
 
255
285
  // If "where" was not set:
@@ -262,48 +292,30 @@ function traverse(queryNode, colander=null, model) {
262
292
  }
263
293
 
264
294
 
265
- function _traverseIncludes(includes, model, colander, resultQuery) {
266
- // If no Colander:
267
- if (colander === null) {
268
- for (let include of includes) {
269
- const includeName = include.model;
270
- const association = model.associations[includeName];
295
+ function _traverseIncludes(includes, model, filter, resultQuery) {
296
+ const filterIncludesEntries = Object.entries(filter.includes);
297
+ for (let [ includeName, includeFilter ] of filterIncludesEntries) {
271
298
 
272
- // If no such association:
273
- if (!association) {
274
- const err = new TypeError(`No include ${ includeName }`);
275
- throw err;
276
- }
277
-
278
- const includeModel = association.target;
279
- // Build query for this include.
280
- const associationQuery = traverse(include, null, includeModel);
299
+ const association = model.associations[includeName];
281
300
 
282
- _addAssociationQuery(associationQuery, includeName, resultQuery);
301
+ // If no such association:
302
+ if (!association) {
303
+ const err = new NQueryError(`No include named '${ includeName }'.`);
304
+ err.status = httpCodes.NOT_ACCEPTABLE;
305
+ Error.captureStackTrace(err, _traverseIncludes);
306
+ throw err;
283
307
  }
284
- }
285
- // Colander is present:
286
- else {
287
- const colanderIncludeEntries = Object.entries(colander.includes);
288
- for (let [includeName, includeColander] of colanderIncludeEntries) {
289
- const association = model.associations[includeName];
290
- // If no such association:
291
- if (!association) {
292
- const err = new TypeError(`No include ${ includeName }`);
293
- throw err;
294
- }
295
308
 
296
- // If include was not requested:
297
- const include = includes.find(({ model }) => model === includeName);
298
- if (!include)
299
- continue;
309
+ // If include was not requested:
310
+ const include = includes.find(({ model }) => model === includeName);
311
+ if (!include)
312
+ continue;
300
313
 
301
- const includeModel = association.target;
302
- // Build query for this include.
303
- const associationQuery = traverse(include, colander.includes[includeName], includeModel);
314
+ const includeModel = association.target;
315
+ // Build query for this include.
316
+ const associationQuery = traverse(include, filter.includes[includeName], includeModel);
304
317
 
305
- _addAssociationQuery(associationQuery, includeName, resultQuery);
306
- }
318
+ _addAssociationQuery(associationQuery, includeName, resultQuery);
307
319
  }
308
320
  }
309
321
 
@@ -379,3 +391,32 @@ function _parseValue(value, attribute) {
379
391
 
380
392
  return value;
381
393
  }
394
+
395
+ function _setValueWithBounds(value, type, bounds) {
396
+ if (typeof bounds === 'object') {
397
+
398
+ switch(type) {
399
+ case 'number': {
400
+ let _value = value;
401
+
402
+ const {
403
+ min,
404
+ max
405
+ } = bounds;
406
+
407
+ const _min = isNaN(min) ? 1 : min;
408
+ _value = _value < _min ? _min : _value;
409
+
410
+ const _max = isNaN(max) ? 1 : max;
411
+ _value = _value > _max ? _max : _value;
412
+
413
+ return _value;
414
+ }
415
+ default:
416
+ break;
417
+ }
418
+ }
419
+
420
+ // If bounds were not set, just use original value.
421
+ return value;
422
+ }
@@ -2,6 +2,7 @@
2
2
  * /nodester
3
3
  * MIT Licensed
4
4
  */
5
+
5
6
  'use strict';
6
7
 
7
8
  const { typeOf } = require('../utils/types.util');