nodester 0.3.4 → 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.
@@ -65,17 +65,32 @@ function extract(body, filter=null, model) {
65
65
  }
66
66
 
67
67
  if (isInclude) {
68
- const filterIncludes = Object.keys(filter.includes);
68
+ const availableIncludeFilters = Object.keys(filter.includes);
69
69
 
70
- if (filterIncludes.indexOf(key) === -1) {
70
+ if (availableIncludeFilters.indexOf(key) === -1) {
71
71
  const err = new Error(`Include '${ key }' is not available.`);
72
72
  err.status = httpCodes.NOT_ACCEPTABLE;
73
73
  throw err;
74
74
  }
75
75
 
76
+ const thisIncludeFilter = filter.includes[key];
77
+
76
78
  const association = model.associations[key];
77
79
 
78
- filteredBody[key] = extract(value[0], filter.includes[key], association.target);
80
+ // Mutliple includes:
81
+ if (Array.isArray(value)) {
82
+ filteredBody[key] = [];
83
+
84
+ for (let thisIncludeBody of value) {
85
+ filteredBody[key].push(
86
+ extract(thisIncludeBody, thisIncludeFilter, association.target)
87
+ )
88
+ }
89
+ }
90
+ else {
91
+ const thisIncludeBody = value;
92
+ filteredBody[key] = extract(thisIncludeBody, thisIncludeFilter, association.target);
93
+ }
79
94
 
80
95
  continue;
81
96
  }
@@ -106,8 +106,8 @@ async function _createOne(params) {
106
106
  });
107
107
 
108
108
  const instance = await this.model.create({ ...data }, {
109
- include: this.model.getIncludesTree(data)
110
- });
109
+ include: this.model.getIncludesTree(data)
110
+ });
111
111
 
112
112
  const result = {
113
113
  [this.outputName.singular]: instance,
@@ -145,12 +145,11 @@ async function _updateOne(params) {
145
145
  data: null
146
146
  });
147
147
 
148
- const updateResult = await this.model.updateOne(query.where, data);;
149
-
150
- const [ isNewRecord, instance ] = updateResult;
148
+ const instance = await this.model.updateOne(query.where, data, {
149
+ include: this.model.getIncludesTree(data)
150
+ });
151
151
 
152
152
  const result = {
153
- success: isNewRecord === false,
154
153
  [this.outputName.singular]: instance,
155
154
  count: 0 + (instance !== null)
156
155
  }
@@ -29,8 +29,9 @@ class ModelsTreeNode {
29
29
  this.limit = -1; // No limit
30
30
 
31
31
  this._includes = opts.includes ?? [];
32
- this.order = opts.order ?? 'asc';
33
- this.order_by = opts.order_by ?? 'id';
32
+
33
+ this.order = opts.order ?? undefined;
34
+ this.order_by = opts.order_by ?? undefined;
34
35
  }
35
36
 
36
37
  // Getters:
@@ -86,7 +86,7 @@ async function _createWithIncludes(
86
86
  associatedModel,
87
87
  foreignKey,
88
88
  associationType
89
- } = getModelAssociationProps(associationDefinition, data);
89
+ } = getModelAssociationProps(associationDefinition);
90
90
 
91
91
  // If association type is HasMany or HasOne (We don't work with any other):
92
92
  if (associationType === 'HasMany' || associationType === 'HasOne') {
@@ -189,71 +189,108 @@ function _findMany(opts={}) {
189
189
  async function _updateOne(
190
190
  where,
191
191
  data,
192
- include=[]
192
+ opts={}
193
193
  ) {
194
194
  try {
195
- const instance = await this.findOne({ where, include });
195
+ const include = opts.include ?? [];
196
+ let instance = await this.findOne({ where, include });
197
+ let isNewRecord = false;
196
198
 
197
199
  if (!instance) {
198
- const err = new Error(`Model not found`);
199
- err.name = 'NotFound';
200
- throw err;
201
- }
202
200
 
203
- // Update parent model instance:
204
- instance.set({ ...data });
205
- const saveResult = await instance.save();
206
- const { isNewRecord } = saveResult;
207
-
208
- // Variable that will contain data from parent instance and associations, mentioned in data.
209
- let fullInstanceData = instance.toJSON();
201
+ if (opts.findOrCreate === true) {
202
+ instance = await this.create(data);
203
+ isNewRecord = true;
204
+ }
205
+ else {
206
+ const err = new Error(`Model not found`);
207
+ err.name = 'NotFound';
208
+ throw err;
209
+ }
210
210
 
211
- // If this model has associations:
212
- if (modelHasAssociations(this)) {
211
+ }
213
212
 
214
- const associationsEntries = Object.entries(this.associations);
215
- for (const [ associationName, associationDefinition ] of associationsEntries) {
213
+ // Will contain data from parent instance and associations.
214
+ const fullInstanceData = instance.toJSON();
216
215
 
217
- // If data of this association is present:
218
- if (!!data[associationName]) {
219
-
220
- // Preparation to work with association:
221
- const {
222
- associatedModel,
223
- foreignKey,
224
- associationType
225
- } = getModelAssociationProps(associationDefinition, data);
216
+ const parentData = {
217
+ ...data
218
+ }
219
+ for (let includeConfig of include) {
220
+ const { association } = includeConfig;
226
221
 
227
- // If association type is HasMany or HasOne (We don't work with any other):
228
- if (associationType === 'HasMany' || associationType === 'HasOne') {
222
+ if (parentData[association] === undefined) {
223
+ continue;
224
+ }
229
225
 
230
- // Process current instance.
231
- const operationsResults = await _updateOrCreateOrDeleteBasedOnAssociationData({
232
- associatedModel: associatedModel,
233
- dataOfAssociation: data[associationName],
234
- parentModelId: instance.id,
235
- parentForeignKey: foreignKey,
236
- });
226
+ delete parentData[association];
227
+
228
+ const associationDefinition = this.associations[association];
229
+ const includeData = data[association];
230
+
231
+ const {
232
+ associatedModel,
233
+ associationType,
234
+
235
+ foreignKey,
236
+ sourceKey
237
+ } = getModelAssociationProps(associationDefinition);
238
+
239
+ const associationUpdateOpts = {
240
+ findOrCreate: true,
241
+ include: associatedModel.getIncludesTree(includeData)
242
+ };
243
+
244
+ const pkField = associatedModel.primaryKeyField;
245
+
246
+ // If association type is HasMany or HasOne (We don't work with any other):
247
+ switch(associationType) {
248
+ case 'HasMany': {
249
+ const promises = includeData.map(singleData => {
250
+ // Note: for now we are only able to work with a model with single PrimaryKey:
251
+ const where = {
252
+ [pkField]: singleData[pkField]
253
+ }
254
+ return associatedModel.updateOne(
255
+ where,
256
+ singleData,
257
+ associationUpdateOpts
258
+ )
259
+ });
260
+ fullInstanceData[association] = await Promise.all(promises);
261
+
262
+ continue;
263
+ }
237
264
 
238
- fullInstanceData[associationName] = _unwrapUpdateOrCreateOrDeleteOperationsResults(
239
- operationsResults,
240
- associationType
241
- );
265
+ case 'HasOne': {
266
+ // Note: for now we are only able to work with a model with single PrimaryKey:
267
+ const where = {
268
+ [pkField]: includeData[pkField]
242
269
  }
270
+ fullInstanceData[association] = await associatedModel.updateOne(
271
+ where,
272
+ includeData,
273
+ associationUpdateOpts
274
+ );
243
275
 
244
- fullInstanceData[associationName] = fullInstanceData[associationName] ?? data[associationName];
276
+ continue;
245
277
  }
246
- }
247
278
 
279
+ default:
280
+ continue;
281
+ }
248
282
  }
249
-
250
- // Select this instance again, if includes was set:
251
- if (include?.length > 0) {
252
- const updatedInstance = await this.findOne({ where, include });
253
- fullInstanceData = updatedInstance.toJSON();
283
+
284
+ // Update parent model instance:
285
+ if (isNewRecord === false) {
286
+ instance.set(parentData);
287
+ const saveResult = await instance.save();
254
288
  }
255
289
 
256
- return Promise.resolve([ isNewRecord, fullInstanceData ]);
290
+ return Promise.resolve({
291
+ _is_new_record: isNewRecord,
292
+ ...fullInstanceData
293
+ });
257
294
  }
258
295
  catch(error) {
259
296
  return Promise.reject(error);
@@ -263,13 +300,13 @@ async function _updateOne(
263
300
  async function _updateById(
264
301
  id=null,
265
302
  data={},
266
- include=[]
303
+ opts={}
267
304
  ) {
268
305
  const where = { id };
269
306
  return this.updateOne(
270
307
  where,
271
308
  data,
272
- include
309
+ opts
273
310
  );
274
311
  }
275
312
 
@@ -5,7 +5,7 @@
5
5
 
6
6
  'use strict';
7
7
 
8
- const BOUNDS = require('../constants/Bounds');
8
+ const BOUNDS = require('../../constants/Bounds');
9
9
 
10
10
  const { Op } = require('sequelize');
11
11
  const { NodesterQueryError } = require('nodester/errors');
@@ -13,6 +13,20 @@ const httpCodes = require('nodester/http/codes');
13
13
 
14
14
  const { ensure } = require('nodester/validators/arguments');
15
15
 
16
+ const {
17
+ parseValue,
18
+ parseWhereEntry,
19
+ } = require('./parsers');
20
+
21
+ const {
22
+ disassembleQueryNode,
23
+ addAssociationQuery,
24
+ } = require('./utils');
25
+
26
+ const {
27
+ getModelAssociationProps
28
+ } = require('../../utils/modelAssociations.util');
29
+
16
30
 
17
31
  module.exports = traverse;
18
32
 
@@ -58,7 +72,7 @@ function traverse(queryNode, filter=null, model=null) {
58
72
  where,
59
73
 
60
74
  includes,
61
- } = _disassembleQueryNode(queryNode);
75
+ } = disassembleQueryNode(queryNode);
62
76
 
63
77
 
64
78
  // Attribute:
@@ -200,10 +214,16 @@ function traverse(queryNode, filter=null, model=null) {
200
214
  continue;
201
215
  }
202
216
  case 'order':
217
+ if (value === undefined)
218
+ continue;
219
+
203
220
  order.order = value;
204
221
  continue;
205
222
 
206
223
  case 'order_by':
224
+ if (value === undefined)
225
+ continue;
226
+
207
227
  order.by = value;
208
228
  continue;
209
229
 
@@ -303,13 +323,13 @@ function traverse(queryNode, filter=null, model=null) {
303
323
  // Set aatributes from Query:
304
324
  const whereEntries = Object.entries(where);
305
325
  for (let [ attribute, value ] of whereEntries) {
306
- _parseWhereEntry(attribute, value, newQuery.where);
326
+ parseWhereEntry(attribute, value, newQuery.where);
307
327
  }
308
328
 
309
329
  // Static attributes override previously set attributes:
310
330
  const staticAttributesEntries = Object.entries(filter.statics.attributes);
311
331
  for (let [ attribute, staticValue ] of staticAttributesEntries) {
312
- newQuery.where[attribute] = _parseValue(staticValue, attribute);
332
+ newQuery.where[attribute] = parseValue(staticValue, attribute);
313
333
  }
314
334
 
315
335
  // If "where" was not set in any way,
@@ -319,6 +339,12 @@ function traverse(queryNode, filter=null, model=null) {
319
339
  }
320
340
  // Where\
321
341
 
342
+ // Combine included orders into one at the top level:
343
+ // - Why?
344
+ // - Sequelize ingores included orders for association types like:
345
+ // • HasMany
346
+ _traverseIncludedOrders(newQuery, _model);
347
+
322
348
  return newQuery;
323
349
  }
324
350
 
@@ -355,66 +381,46 @@ function _traverseIncludes(includes, rootModel, filter, resultQuery) {
355
381
  // Build query for this include.
356
382
  const associationQuery = traverse(include, filter.includes[includeName], includeModel);
357
383
 
358
- _addAssociationQuery(associationQuery, includeName, resultQuery);
384
+ addAssociationQuery(associationQuery, includeName, resultQuery);
359
385
  }
360
386
  }
361
387
 
388
+ function _traverseIncludedOrders(resultQuery, rootModel) {
389
+ for (let i=0; i < resultQuery.include.length; i++) {
390
+ const include = resultQuery.include[i];
362
391
 
363
- function _addAssociationQuery(associationQuery, includeName, resultQuery) {
364
-
365
- // Add all association info into query.
366
- resultQuery.include.push({
367
- association: includeName,
368
- ...associationQuery
369
- });
370
- }
371
-
372
-
373
- function _parseWhereEntry(attribute, value, whereHolder) {
374
- let _value = value;
375
-
376
- // If attribute is Op (not, like, or, etc.):
377
- if (attribute in Op) {
378
- // Parse value:
379
- _value = _parseValue(_value, attribute);
380
-
381
- const op = Op[attribute];
382
- whereHolder[op] = _value;
383
- return;
384
- }
385
-
386
- whereHolder[attribute] = _parseValue(_value, attribute);
387
- }
392
+ console.log({ irder: include?.order });
393
+ if (!include?.order) {
394
+ continue;
395
+ }
388
396
 
389
- function _disassembleQueryNode(queryNode) {
390
- // Disassemble current query node:
391
- const {
392
- attributes,
393
- functions,
394
- where,
395
- includes,
396
- ...clauses
397
- } = queryNode;
398
-
399
- return {
400
- attributes: attributes ?? [],
401
- clauses: clauses ?? [],
402
- functions: functions ?? [],
403
- where: where ?? {},
404
- includes: includes ?? [],
405
- };
406
- }
397
+ if (!resultQuery.order) {
398
+ resultQuery.order = [];
399
+ }
407
400
 
408
- function _parseValue(value, attribute) {
409
- // If value is Object:
410
- if (typeof value === 'object' && Array.isArray(value) === false) {
411
- const [ opKey, rawValue ] = (Object.entries(value))[0];
401
+ const { association } = include;
402
+ const {
403
+ associatedModel,
404
+ associationType
405
+ } = getModelAssociationProps(rootModel.associations[association]);
406
+
407
+ switch(associationType) {
408
+ case 'HasMany': {
409
+ resultQuery.order.push([
410
+ { association },
411
+ ...include.order[0]
412
+ ]);
413
+ delete resultQuery.include[i].order;
414
+ break;
415
+ }
416
+ default:
417
+ break;
418
+ }
412
419
 
413
- const op = Op[opKey];
414
- return { [op]: rawValue };
420
+ _traverseIncludedOrders(resultQuery.include[i], associatedModel);
415
421
  }
416
422
 
417
- return value;
423
+ return resultQuery;
418
424
  }
419
425
 
420
426
  function _setValueWithBounds(value, type, bounds) {
@@ -0,0 +1,41 @@
1
+ /**
2
+ * nodester
3
+ * MIT Licensed
4
+ */
5
+
6
+ 'use strict';
7
+
8
+
9
+ module.exports = {
10
+ parseValue: _parseValue,
11
+ parseWhereEntry: _parseWhereEntry,
12
+ }
13
+
14
+ function _parseValue(value, attribute) {
15
+ // If value is Object:
16
+ if (typeof value === 'object' && Array.isArray(value) === false) {
17
+ const [ opKey, rawValue ] = (Object.entries(value))[0];
18
+
19
+ const op = Op[opKey];
20
+ return { [op]: rawValue };
21
+ }
22
+
23
+ return value;
24
+ }
25
+
26
+ function _parseWhereEntry(attribute, value, whereHolder) {
27
+ let _value = value;
28
+
29
+ // If attribute is Op (not, like, or, etc.):
30
+ if (attribute in Op) {
31
+ // Parse value:
32
+ _value = _parseValue(_value, attribute);
33
+
34
+ const op = Op[attribute];
35
+ whereHolder[op] = _value;
36
+ return;
37
+ }
38
+
39
+ whereHolder[attribute] = _parseValue(_value, attribute);
40
+ }
41
+
@@ -0,0 +1,40 @@
1
+ /**
2
+ * nodester
3
+ * MIT Licensed
4
+ */
5
+
6
+ 'use strict';
7
+
8
+
9
+ module.exports = {
10
+ disassembleQueryNode: _disassembleQueryNode,
11
+ addAssociationQuery: _addAssociationQuery,
12
+ }
13
+
14
+ function _disassembleQueryNode(queryNode) {
15
+ // Disassemble current query node:
16
+ const {
17
+ attributes,
18
+ functions,
19
+ where,
20
+ includes,
21
+ ...clauses
22
+ } = queryNode;
23
+
24
+ return {
25
+ attributes: attributes ?? [],
26
+ clauses: clauses ?? [],
27
+ functions: functions ?? [],
28
+ where: where ?? {},
29
+ includes: includes ?? [],
30
+ };
31
+ }
32
+
33
+ function _addAssociationQuery(associationQuery, includeName, resultQuery) {
34
+
35
+ // Add all association info into query.
36
+ resultQuery.include.push({
37
+ association: includeName,
38
+ ...associationQuery
39
+ });
40
+ }
@@ -1,3 +1,10 @@
1
+ /**
2
+ * nodester
3
+ * MIT Licensed
4
+ */
5
+
6
+ 'use strict';
7
+
1
8
 
2
9
  module.exports = {
3
10
  modelHasAssociations: _modelHasAssociations,
@@ -10,12 +17,16 @@ function _modelHasAssociations(modelDifinition) {
10
17
  }
11
18
 
12
19
  function _getModelAssociationProps(
13
- associationDefinition,
14
- requestData
20
+ associationDefinition
15
21
  ) {
16
22
  // Extract neccessary variables and functions:
17
23
  const associatedModel = associationDefinition.target;
18
- const { foreignKey } = associationDefinition.options;
24
+
25
+ const {
26
+ foreignKey,
27
+ sourceKey
28
+ } = associationDefinition;
29
+
19
30
  const {
20
31
  associationType,
21
32
  accessors,
@@ -23,8 +34,11 @@ function _getModelAssociationProps(
23
34
 
24
35
  return {
25
36
  associatedModel,
26
- foreignKey,
27
37
  associationType,
38
+
39
+ foreignKey,
40
+ sourceKey,
41
+
28
42
  accessors,
29
43
  };
30
44
  }
@@ -7,8 +7,14 @@
7
7
 
8
8
 
9
9
  module.exports = {
10
+ UUID: _UUID,
11
+
10
12
  CHAR: _STRING,
11
13
  VARCHAR: _STRING,
14
+ STRING: _STRING,
15
+ TEXT: _TEXT,
16
+
17
+ ENUM: _ENUM,
12
18
 
13
19
  NUMBER: _NUMBER,
14
20
 
@@ -17,8 +23,6 @@ module.exports = {
17
23
 
18
24
  BOOLEAN: _BOOLEAN,
19
25
 
20
- STRING: _STRING,
21
- TEXT: _TEXT,
22
26
 
23
27
  DATE: _DATE,
24
28
  DATETIME: _DATE,
@@ -26,6 +30,52 @@ module.exports = {
26
30
  JSON: _JSON
27
31
  }
28
32
 
33
+ function _UUID(value=undefined, options={ fallback:undefined }) {
34
+ try {
35
+ if (typeof value !== 'string')
36
+ throw new Error(`Not a valid UUID`);
37
+
38
+ return value;
39
+ }
40
+ catch(ex) {
41
+ return options?.fallback;
42
+ }
43
+ }
44
+
45
+
46
+ function _STRING(value=undefined, options={ fallback:undefined }) {
47
+ try {
48
+ if (typeof value !== 'string')
49
+ throw new Error(`Not a String`);
50
+
51
+ return value;
52
+ }
53
+ catch(ex) {
54
+ return options?.fallback;
55
+ }
56
+ }
57
+
58
+ function _TEXT(value=undefined, options={ fallback:undefined }) {
59
+ try {
60
+ if (typeof value !== 'string')
61
+ throw new Error(`Not a String`);
62
+
63
+ return value;
64
+ }
65
+ catch(ex) {
66
+ return options?.fallback;
67
+ }
68
+ }
69
+
70
+
71
+ function _ENUM(value=undefined, options={ fallback:undefined }) {
72
+ if (value === undefined)
73
+ return options.fallback;
74
+
75
+ return value;
76
+ }
77
+
78
+
29
79
  function _isNumber(value) {
30
80
  return !isNaN(`${value}`);
31
81
  }
@@ -70,29 +120,6 @@ function _BOOLEAN(value=undefined, options={ fallback:undefined }) {
70
120
  }
71
121
  }
72
122
 
73
- function _STRING(value=undefined, options={ fallback:undefined }) {
74
- try {
75
- if (typeof value !== 'string')
76
- throw new Error(`Not a String`);
77
-
78
- return value;
79
- }
80
- catch(ex) {
81
- return options?.fallback;
82
- }
83
- }
84
-
85
- function _TEXT(value=undefined, options={ fallback:undefined }) {
86
- try {
87
- if (typeof value !== 'string')
88
- throw new Error(`Not a String`);
89
-
90
- return value;
91
- }
92
- catch(ex) {
93
- return options?.fallback;
94
- }
95
- }
96
123
 
97
124
  function _DATE(value=undefined, options={ fallback:undefined }) {
98
125
  try {
@@ -123,6 +150,7 @@ function _DATE(value=undefined, options={ fallback:undefined }) {
123
150
  }
124
151
  }
125
152
 
153
+
126
154
  function _JSON(value=undefined, options={ fallback:undefined }) {
127
155
  try {
128
156
  if (typeof value === 'string')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodester",
3
- "version": "0.3.4",
3
+ "version": "0.4.0",
4
4
  "description": "A versatile REST framework for Node.js",
5
5
  "directories": {
6
6
  "docs": "docs",
@@ -57,7 +57,7 @@
57
57
  "./params": "./lib/structures/Params.js",
58
58
 
59
59
  "./ql/sequelize": "./lib/middlewares/ql/sequelize/index.js",
60
- "./query/traverse": "./lib/query/traverse.js",
60
+ "./query/traverse": "./lib/query/traverse/index.js",
61
61
 
62
62
  "./route": "./lib/router/route.js",
63
63
  "./router": "./lib/router/index.js",