alchemymvc 1.1.10 → 1.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.
@@ -228,7 +228,7 @@ Mongo.setMethod(function collection(name, callback) {
228
228
  *
229
229
  * @author Jelle De Loecker <jelle@develry.be>
230
230
  * @since 0.2.0
231
- * @version 1.1.0
231
+ * @version 1.2.0
232
232
  */
233
233
  Mongo.setMethod(function _read(model, criteria, callback) {
234
234
 
@@ -250,22 +250,87 @@ Mongo.setMethod(function _read(model, criteria, callback) {
250
250
 
251
251
  if (compiled.pipeline) {
252
252
 
253
- collection.aggregate(compiled.pipeline, {}, function gotAggregate(err, cursor) {
253
+ // Sorting should happen in the pipeline
254
+ if (options.sort && options.sort.length) {
255
+ let sort_object = {};
254
256
 
255
- if (err) {
256
- return callback(err);
257
+ for (let entry of options.sort) {
258
+ sort_object[entry[0]] = entry[1];
257
259
  }
258
260
 
259
- cursor.toArray(function gotArray(err, items) {
261
+ compiled.pipeline.unshift({$sort: sort_object});
262
+ }
263
+
264
+ // Skipping also happens in the pipeline
265
+ if (options.skip) {
266
+ compiled.pipeline.push({$skip: options.skip});
267
+ }
268
+
269
+ let aggregate_options = {};
260
270
 
261
- if (err) {
262
- return callback(err);
271
+ // Limits can still be set as an option though
272
+ if (options.limit) {
273
+ aggregate_options.limit = options.limit;
274
+ }
275
+
276
+ Function.parallel({
277
+ available: function getAvailable(next) {
278
+
279
+ if (criteria.options.available === false) {
280
+ return next(null, null);
263
281
  }
264
282
 
265
- items = that.organizeResultItems(model, items);
283
+ let pipeline = JSON.clone(compiled.pipeline),
284
+ cloned_options = JSON.clone(aggregate_options);
285
+
286
+ pipeline.push({$count: 'available'});
287
+
288
+ // Expensive aggregate just to get the available count...
289
+ collection.aggregate(pipeline, cloned_options, function gotAggregate(err, cursor) {
290
+
291
+ if (err) {
292
+ return next(err);
293
+ }
294
+
295
+ cursor.toArray(function gotAvailableArray(err, items) {
296
+
297
+ if (err) {
298
+ return next(err);
299
+ }
300
+
301
+ if (!items || !items.length) {
302
+ return next(null, null);
303
+ }
304
+
305
+ let available = items[0].available;
306
+
307
+ if (options.skip) {
308
+ available += options.skip;
309
+ }
310
+
311
+ return next(null, available);
312
+ });
313
+ });
314
+ },
315
+ items: function getItems(next) {
316
+ collection.aggregate(compiled.pipeline, aggregate_options, function gotAggregate(err, cursor) {
317
+
318
+ if (err) {
319
+ return next(err);
320
+ }
321
+
322
+ cursor.toArray(next);
323
+ });
324
+ }
325
+ }, function done(err, data) {
326
+
327
+ if (err) {
328
+ return callback(err);
329
+ }
330
+
331
+ data.items = that.organizeResultItems(model, data.items);
266
332
 
267
- callback(null, items);
268
- });
333
+ callback(err, data.items, data.available);
269
334
  });
270
335
 
271
336
  return;
@@ -611,6 +611,15 @@ NoSQL.setMethod(function compileCriteria(criteria, group) {
611
611
  group = criteria.group;
612
612
  }
613
613
 
614
+ let getAggregate = () => {
615
+ if (!aggregate) {
616
+ aggregate = {
617
+ pipeline: [],
618
+ lookups: {}
619
+ };
620
+ }
621
+ }
622
+
614
623
  for (i = 0; i < group.items.length; i++) {
615
624
  entry = group.items[i];
616
625
 
@@ -620,12 +629,7 @@ NoSQL.setMethod(function compileCriteria(criteria, group) {
620
629
  }
621
630
 
622
631
  if (entry.association) {
623
- if (!aggregate) {
624
- aggregate = {
625
- pipeline: [],
626
- lookups: {}
627
- };
628
- }
632
+ getAggregate();
629
633
 
630
634
  // Get the association info
631
635
  assoc = criteria.model.getAssociation(entry.association);
@@ -705,9 +709,45 @@ NoSQL.setMethod(function compileCriteria(criteria, group) {
705
709
  not,
706
710
  obj = {};
707
711
 
712
+ let field_entry = {},
713
+ name = entry.target_path;
714
+
715
+ // Do we need to look into an object itself?
716
+ // (Like the "timestamp" property of a date field when stored with units)
717
+ if (entry.db_property) {
718
+ name += '.' + entry.db_property;
719
+ }
720
+
721
+ if (entry.association) {
722
+ name = entry.association + '.' + name;
723
+ }
724
+
708
725
  for (let i = 0; i < entry.items.length; i++) {
709
726
  item = entry.items[i];
710
727
 
728
+ // If the value is a RegExp, we might have to stringify the value
729
+ if (shouldStringify(entry, item) && RegExp.isRegExp(item.value)) {
730
+
731
+ let stringified_field = name + '_stringified';
732
+
733
+ getAggregate();
734
+
735
+ aggregate.pipeline.push({
736
+ $addFields: {
737
+ [stringified_field]: {
738
+ $convert: {
739
+ input: '$' + name,
740
+ to: 'string',
741
+ onError: '',
742
+ onNull: ''
743
+ }
744
+ }
745
+ }
746
+ });
747
+
748
+ name = stringified_field;
749
+ }
750
+
711
751
  if (item.type == 'ne') {
712
752
  obj.$ne = item.value;
713
753
  } else if (item.type == 'not') {
@@ -771,19 +811,6 @@ NoSQL.setMethod(function compileCriteria(criteria, group) {
771
811
  throw new Error('Unknown criteria expression: "' + item.type + '"');
772
812
  }
773
813
 
774
- let field_entry = {},
775
- name = entry.target_path;
776
-
777
- // Do we need to look into an object itself?
778
- // (Like the "timestamp" property of a date field when stored with units)
779
- if (entry.db_property) {
780
- name += '.' + entry.db_property;
781
- }
782
-
783
- if (entry.association) {
784
- name = entry.association + '.' + name;
785
- }
786
-
787
814
  if (obj && obj.$or) {
788
815
 
789
816
  let $or = [],
@@ -870,6 +897,33 @@ NoSQL.setMethod(function compileCriteria(criteria, group) {
870
897
  return {$and: result};
871
898
  });
872
899
 
900
+ /**
901
+ * Is the given item about a string field?
902
+ *
903
+ * @author Jelle De Loecker <jelle@elevenways.be>
904
+ * @since 1.2.0
905
+ * @version 1.2.0
906
+ *
907
+ * @param {Object} entry
908
+ * @param {Object} item
909
+ *
910
+ * @return {boolean}
911
+ */
912
+ function shouldStringify(entry, item) {
913
+
914
+ if (!item || !item.value) {
915
+ return false;
916
+ }
917
+
918
+ try {
919
+ let field = entry.model.getField(entry.target_path);
920
+
921
+ // The field value should only be stringified if it isn't a string already
922
+ return !(field instanceof Classes.Alchemy.Field.String);
923
+ } catch (err) {}
924
+
925
+ return false;
926
+ }
873
927
 
874
928
  /**
875
929
  * Get the MongoDB options from this criteria
@@ -388,15 +388,26 @@ Criteria.setMethod(function clone() {
388
388
  *
389
389
  * @author Jelle De Loecker <jelle@develry.be>
390
390
  * @since 1.1.0
391
- * @version 1.1.0
391
+ * @version 1.2.0
392
392
  *
393
- * @return {Array}
393
+ * @return {String[]}
394
394
  */
395
395
  Criteria.setMethod(function getFieldsToSelect() {
396
396
 
397
- var result = this.options.select.fields || [];
397
+ let result;
398
398
 
399
- return result;
399
+ if (this.options.select.fields && this.options.select.fields.length) {
400
+ result = this.options.select.fields.slice(0);
401
+ }
402
+
403
+ // Fields can sometimes be required for a query (like in a join) but they
404
+ // won't be selected if other fields are explicitly set.
405
+ // So in that case: add these special fields to the projection
406
+ if (result && this.options.select.query_fields && this.options.select.query_fields) {
407
+ result.push(...this.options.select.query_fields);
408
+ }
409
+
410
+ return result || [];
400
411
  });
401
412
 
402
413
  /**
@@ -663,7 +674,7 @@ Criteria.setMethod(function page(page, page_size) {
663
674
  *
664
675
  * @author Jelle De Loecker <jelle@develry.be>
665
676
  * @since 1.1.0
666
- * @version 1.1.3
677
+ * @version 1.2.0
667
678
  *
668
679
  * @param {String|Array} field
669
680
  *
@@ -681,12 +692,6 @@ Criteria.setMethod(function select(field) {
681
692
  }
682
693
  } else {
683
694
 
684
- if (typeof field == 'object') {
685
- if (field instanceof Classes.Alchemy.Criteria.FieldConfig || field.name) {
686
- field = field.name;
687
- }
688
- }
689
-
690
695
  if (this._select) {
691
696
  context = this._select.parse(field);
692
697
  } else {
@@ -1383,7 +1388,7 @@ Select.setMethod(Blast.checksumSymbol, function toChecksum() {
1383
1388
  *
1384
1389
  * @author Jelle De Loecker <jelle@develry.be>
1385
1390
  * @since 1.1.0
1386
- * @version 1.1.0
1391
+ * @version 1.2.0
1387
1392
  *
1388
1393
  * @param {String} name
1389
1394
  *
@@ -1419,9 +1424,39 @@ Select.setMethod(function addAssociation(name) {
1419
1424
  this.associations[name].association_name = name;
1420
1425
  }
1421
1426
 
1427
+ // Get the association data
1428
+ try {
1429
+ let info = this.criteria.model.getAssociation(name);
1430
+
1431
+ if (info) {
1432
+ // Make sure the localkey is added to the resultset
1433
+ this.requireFieldForQuery(info.options.localKey);
1434
+ }
1435
+ } catch (err) {
1436
+ console.warn('Failed to find "' + name + '" association for ' + this.criteria.model.modelName);
1437
+ }
1438
+
1422
1439
  return this.associations[name];
1423
1440
  });
1424
1441
 
1442
+ /**
1443
+ * Require a field for query purposes
1444
+ *
1445
+ * @author Jelle De Loecker <jelle@develry.be>
1446
+ * @since 1.2.0
1447
+ * @version 1.2.0
1448
+ *
1449
+ * @param {String} path
1450
+ */
1451
+ Select.setMethod(function requireFieldForQuery(path) {
1452
+
1453
+ if (!this.query_fields) {
1454
+ this.query_fields = [];
1455
+ }
1456
+
1457
+ this.query_fields.push(path);
1458
+ });
1459
+
1425
1460
  /**
1426
1461
  * Add a field
1427
1462
  *
@@ -1445,17 +1480,33 @@ Select.setMethod(function addField(path) {
1445
1480
  *
1446
1481
  * @author Jelle De Loecker <jelle@develry.be>
1447
1482
  * @since 1.1.0
1448
- * @version 1.1.0
1483
+ * @version 1.2.0
1449
1484
  *
1450
- * @param {String} path
1485
+ * @param {String|Object} path
1451
1486
  *
1452
1487
  * @return {Criteria|Null} A criteria object if the context has changed
1453
1488
  */
1454
1489
  Select.setMethod(function parse(path) {
1455
1490
 
1456
- var context,
1491
+ let context,
1457
1492
  select = this,
1458
- parsed = Criteria.parsePath(path, this.criteria);
1493
+ parsed;
1494
+
1495
+ if (typeof path == 'object' && path && path.name) {
1496
+
1497
+ if (path.path) {
1498
+ path = path.path;
1499
+ } else {
1500
+ let obj = path;
1501
+ path = obj.name;
1502
+
1503
+ if (obj.association) {
1504
+ path = obj.association + '.' + path;
1505
+ }
1506
+ }
1507
+ }
1508
+
1509
+ parsed = Criteria.parsePath(path, this.criteria);
1459
1510
 
1460
1511
  // Associations were found,
1461
1512
  // like "Comment._id" or "Comment.User"
@@ -1470,7 +1521,7 @@ Select.setMethod(function parse(path) {
1470
1521
  continue;
1471
1522
  }
1472
1523
 
1473
- select = this.addAssociation(name);
1524
+ select = select.addAssociation(name);
1474
1525
  }
1475
1526
  }
1476
1527
 
@@ -1489,7 +1540,7 @@ Select.setMethod(function parse(path) {
1489
1540
  *
1490
1541
  * @author Jelle De Loecker <jelle@develry.be>
1491
1542
  * @since 1.1.0
1492
- * @version 1.1.0
1543
+ * @version 1.2.0
1493
1544
  *
1494
1545
  * @param {Criteria} criteria
1495
1546
  *
@@ -1507,6 +1558,10 @@ Select.setMethod(function cloneForCriteria(criteria) {
1507
1558
  clone.fields = this.fields.slice(0);
1508
1559
  }
1509
1560
 
1561
+ if (this.query_fields && this.query_fields.length) {
1562
+ clone.query_fields = this.query_fields.slice(0);
1563
+ }
1564
+
1510
1565
  if (this.associations) {
1511
1566
  let key;
1512
1567
 
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * @author Jelle De Loecker <jelle@elevenways.be>
9
9
  * @since 1.1.3
10
- * @version 1.1.7
10
+ * @version 1.2.0
11
11
  *
12
12
  * @param {string} path
13
13
  * @param {Object} options
@@ -20,6 +20,9 @@ const FieldConfig = Fn.inherits('Alchemy.Base', 'Alchemy.Criteria', function Fie
20
20
  // The full path to the value
21
21
  this.path = null;
22
22
 
23
+ // The local path to the value (subfields)
24
+ this.local_path = null;
25
+
23
26
  // The pieces of the path
24
27
  this.pieces = null;
25
28
 
@@ -89,7 +92,7 @@ FieldConfig.setMethod(function toDry() {
89
92
  *
90
93
  * @author Jelle De Loecker <jelle@elevenways.be>
91
94
  * @since 1.1.3
92
- * @version 1.1.4
95
+ * @version 1.2.0
93
96
  *
94
97
  * @return {Object}
95
98
  */
@@ -97,6 +100,7 @@ FieldConfig.setMethod(function toJSON() {
97
100
  return {
98
101
  name : this.name,
99
102
  path : this.path,
103
+ local_path : this.local_path,
100
104
  association : this.association,
101
105
  options : this.options,
102
106
  };
@@ -107,7 +111,7 @@ FieldConfig.setMethod(function toJSON() {
107
111
  *
108
112
  * @author Jelle De Loecker <jelle@elevenways.be>
109
113
  * @since 1.1.3
110
- * @version 1.1.3
114
+ * @version 1.2.0
111
115
  *
112
116
  * @param {string} path
113
117
  */
@@ -120,6 +124,7 @@ FieldConfig.setMethod(function parsePath(path) {
120
124
  } else if (path.indexOf('.') == -1) {
121
125
  this.name = path;
122
126
  this.path = path;
127
+ this.local_path = path;
123
128
  this.pieces = [path];
124
129
  return;
125
130
  } else {
@@ -132,13 +137,27 @@ FieldConfig.setMethod(function parsePath(path) {
132
137
 
133
138
  this.path = path;
134
139
  this.pieces = pieces;
140
+ this.local_path = '';
135
141
 
136
142
  for (i = 0; i < pieces.length; i++) {
137
143
  piece = pieces[i];
138
144
 
139
- if (i == 0 && piece[0].isUpperCase()) {
140
- this.association = piece;
145
+ if (piece[0].isUpperCase()) {
146
+
147
+ if (this.association) {
148
+ this.association += '.';
149
+ } else {
150
+ this.association = '';
151
+ }
152
+
153
+ this.association += piece;
141
154
  continue;
155
+ } else {
156
+ if (this.local_path) {
157
+ this.local_path += '.';
158
+ }
159
+
160
+ this.local_path += piece;
142
161
  }
143
162
  }
144
163
 
@@ -403,7 +403,7 @@ Model.setMethod(function getFindOptions(options) {
403
403
  *
404
404
  * @author Jelle De Loecker <jelle@develry.be>
405
405
  * @since 1.1.0
406
- * @version 1.1.0
406
+ * @version 1.2.0
407
407
  *
408
408
  * @param {String} alias
409
409
  *
@@ -415,7 +415,30 @@ Model.setMethod(function getAssociation(alias) {
415
415
  throw new Error('Unable to find ' + this.constructor.name + ' schema associations');
416
416
  }
417
417
 
418
- let config = this.schema.associations[alias];
418
+ let config;
419
+
420
+ // @TODO: Test nested association getting
421
+ if (alias && alias.indexOf('.') > -1) {
422
+ let pieces = alias.split('.'),
423
+ context = this,
424
+ piece,
425
+ temp;
426
+
427
+ for (piece of pieces) {
428
+ temp = context.getAssociation(piece);
429
+
430
+ if (!temp) {
431
+ break;
432
+ }
433
+
434
+ context = this.getModel(temp.modelName);
435
+ }
436
+
437
+ // The config s the last association gotten
438
+ config = temp;
439
+ } else {
440
+ config = this.schema.associations[alias];
441
+ }
419
442
 
420
443
  if (!config) {
421
444
  throw new Error('Unable to find ' + JSON.stringify(alias) + ' association in ' + this.constructor.name + ' model');
@@ -1401,15 +1424,30 @@ Model.setProperty(function associations() {
1401
1424
  *
1402
1425
  * @author Jelle De Loecker <jelle@develry.be>
1403
1426
  * @since 1.0.0
1404
- * @version 1.1.0
1427
+ * @version 1.2.0
1405
1428
  *
1406
- * @param {String} name The name of the field
1429
+ * @param {string} path The path to the field
1407
1430
  *
1408
1431
  * @return {Object}
1409
1432
  */
1410
- Model.setMethod(function getField(name) {
1411
- return this.schema.getField(name);
1412
- }, false);
1433
+ Model.setMethod(function getField(path) {
1434
+
1435
+ if (path.indexOf('.') > -1) {
1436
+ let config = new Classes.Alchemy.Criteria.FieldConfig(path);
1437
+
1438
+ // If part of the path is an association, look for that now
1439
+ if (config.association) {
1440
+ let association = this.getAssociation(config.association);
1441
+ let model = this.getModel(association.modelName);
1442
+ return model.getField(config.local_path);
1443
+ }
1444
+
1445
+ // If not, just use the local_path
1446
+ path = config.local_path;
1447
+ }
1448
+
1449
+ return this.schema.getField(path);
1450
+ });
1413
1451
 
1414
1452
  // Make this class easily available
1415
1453
  Hawkejs.Model = Model;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The Alchemy Migration Model class
3
+ *
4
+ * @constructor
5
+ *
6
+ * @author Jelle De Loecker <jelle@elevenways.be>
7
+ * @since 1.2.0
8
+ * @version 1.2.0
9
+ */
10
+ const AlchemyMigration = Function.inherits('Alchemy.Model.App', 'AlchemyMigration');
11
+
12
+ /**
13
+ * Constitute the class wide schema
14
+ *
15
+ * @author Jelle De Loecker <jelle@elevenways.be>
16
+ * @since 1.2.0
17
+ * @version 1.2.0
18
+ */
19
+ AlchemyMigration.constitute(function addTaskFields() {
20
+
21
+ // The name of the migration
22
+ this.addField('name', 'String');
23
+
24
+ // The full path of the file
25
+ this.addField('path', 'String');
26
+
27
+ // When the migration ended
28
+ this.addField('ended', 'Datetime');
29
+
30
+ // Was there an error?
31
+ this.addField('error', 'String');
32
+
33
+ });
package/lib/bootstrap.js CHANGED
@@ -61,6 +61,15 @@ require('./core/base');
61
61
  */
62
62
  require('./core/client_base');
63
63
 
64
+ /**
65
+ * The migration class
66
+ *
67
+ * @author Jelle De Loecker <jelle@elevenways.be>
68
+ * @since 1.2.0
69
+ * @version 1.2.0
70
+ */
71
+ require('./class/migration');
72
+
64
73
  var hawkejs_options = {
65
74
  server : false,
66
75
  make_commonjs: true,
@@ -165,7 +165,7 @@ Datasource.setMethod(function getSchema(schema) {
165
165
  *
166
166
  * @author Jelle De Loecker <jelle@develry.be>
167
167
  * @since 0.2.0
168
- * @version 1.1.0
168
+ * @version 1.2.0
169
169
  *
170
170
  * @param {Schema|Model} schema
171
171
  * @param {Object} data
@@ -187,7 +187,7 @@ Datasource.setMethod(function toDatasource(schema, data, callback) {
187
187
  }
188
188
 
189
189
  if (!schema) {
190
- log.todo('Schema not found: not normalizing data');
190
+ log.todo('Schema not found: not normalizing data', data);
191
191
  pledge = Pledge.resolve(data);
192
192
  pledge.done(callback);
193
193
  return pledge;
@@ -594,7 +594,7 @@ Field.setMethod(function castCondition(value, field_paths) {
594
594
  *
595
595
  * @author Jelle De Loecker <jelle@develry.be>
596
596
  * @since 0.5.0
597
- * @version 1.0.7
597
+ * @version 1.2.0
598
598
  *
599
599
  * @param {Mixed} value
600
600
  * @param {Array} field_paths The path to the field
@@ -602,6 +602,12 @@ Field.setMethod(function castCondition(value, field_paths) {
602
602
  * @return {Mixed}
603
603
  */
604
604
  Field.setMethod(function _castCondition(value, field_paths) {
605
+
606
+ // Always allow regex values, we'll use those in the projection stage
607
+ if (value && RegExp.isRegExp(value)) {
608
+ return value;
609
+ }
610
+
605
611
  return this.cast(value, true);
606
612
  });
607
613