alchemymvc 1.1.7 → 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.
Files changed (39) hide show
  1. package/lib/app/behaviour/sluggable_behaviour.js +2 -4
  2. package/lib/app/conduit/http_conduit.js +173 -192
  3. package/lib/app/conduit/loopback_conduit.js +0 -0
  4. package/lib/app/conduit/socket_conduit.js +620 -620
  5. package/lib/app/datasource/mongo_datasource.js +75 -10
  6. package/lib/app/element/time_ago.js +0 -0
  7. package/lib/app/helper/socket_helper.js +613 -613
  8. package/lib/app/helper_datasource/00-nosql_datasource.js +73 -19
  9. package/lib/app/helper_datasource/remote_datasource.js +0 -0
  10. package/lib/app/helper_field/05-string_field.js +0 -0
  11. package/lib/app/helper_field/schema_field.js +7 -2
  12. package/lib/app/helper_model/criteria.js +76 -18
  13. package/lib/app/helper_model/field_config.js +24 -5
  14. package/lib/app/helper_model/model.js +65 -7
  15. package/lib/app/model/alchemy_migration_model.js +33 -0
  16. package/lib/bootstrap.js +9 -0
  17. package/lib/class/conduit.js +2474 -2412
  18. package/lib/class/datasource.js +8 -8
  19. package/lib/class/field.js +7 -1
  20. package/lib/class/inode.js +62 -0
  21. package/lib/class/inode_dir.js +115 -0
  22. package/lib/class/inode_file.js +112 -111
  23. package/lib/class/migration.js +138 -0
  24. package/lib/class/model.js +1765 -1772
  25. package/lib/class/reciprocal.js +8 -2
  26. package/lib/class/router.js +0 -0
  27. package/lib/class/schema.js +14 -12
  28. package/lib/core/base.js +37 -10
  29. package/lib/core/client_base.js +1 -9
  30. package/lib/core/middleware.js +4 -8
  31. package/lib/core/socket.js +159 -159
  32. package/lib/init/alchemy.js +1779 -1779
  33. package/lib/init/devwatch.js +1 -1
  34. package/lib/init/functions.js +2 -56
  35. package/lib/init/load_functions.js +8 -2
  36. package/lib/init/requirements.js +101 -96
  37. package/lib/stages.js +12 -0
  38. package/package.json +74 -76
  39. package/CHANGELOG.md +0 -445
@@ -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
File without changes
File without changes
@@ -277,7 +277,12 @@ SchemaField.setMethod(function _toApp(query, options, value, callback) {
277
277
 
278
278
  name = this.name + 'FieldModel';
279
279
  Dummy = alchemy.getModel('Model', false);
280
- Dummy = new Dummy({name: name});
280
+
281
+ Dummy = new Dummy({
282
+ root_model : this.root_model,
283
+ name : name
284
+ });
285
+
281
286
  item = {};
282
287
 
283
288
  item[name] = value;
@@ -338,7 +343,7 @@ SchemaField.setMethod(function _toApp(query, options, value, callback) {
338
343
 
339
344
  if (field != null) {
340
345
  tasks[field_name] = function doToDatasource(next) {
341
- field.toApp({}, {}, field_value, next);
346
+ field.toApp({}, {_root_data: options._root_data}, field_value, next);
342
347
  };
343
348
  }
344
349
  });
@@ -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
  /**
@@ -510,6 +521,9 @@ Criteria.setMethod(function getCriteriaForAssociation(name, item) {
510
521
  assoc_crit.where(assoc_key).equals(value);
511
522
  }
512
523
 
524
+ assoc_crit.setOption('assoc_key', assoc_key);
525
+ assoc_crit.setOption('assoc_value', value);
526
+
513
527
  // Take over the locale option
514
528
  if (options.locale) {
515
529
  assoc_crit.setOption('locale', options.locale);
@@ -660,7 +674,7 @@ Criteria.setMethod(function page(page, page_size) {
660
674
  *
661
675
  * @author Jelle De Loecker <jelle@develry.be>
662
676
  * @since 1.1.0
663
- * @version 1.1.3
677
+ * @version 1.2.0
664
678
  *
665
679
  * @param {String|Array} field
666
680
  *
@@ -678,12 +692,6 @@ Criteria.setMethod(function select(field) {
678
692
  }
679
693
  } else {
680
694
 
681
- if (typeof field == 'object') {
682
- if (field instanceof Classes.Alchemy.Criteria.FieldConfig || field.name) {
683
- field = field.name;
684
- }
685
- }
686
-
687
695
  if (this._select) {
688
696
  context = this._select.parse(field);
689
697
  } else {
@@ -1380,7 +1388,7 @@ Select.setMethod(Blast.checksumSymbol, function toChecksum() {
1380
1388
  *
1381
1389
  * @author Jelle De Loecker <jelle@develry.be>
1382
1390
  * @since 1.1.0
1383
- * @version 1.1.0
1391
+ * @version 1.2.0
1384
1392
  *
1385
1393
  * @param {String} name
1386
1394
  *
@@ -1416,9 +1424,39 @@ Select.setMethod(function addAssociation(name) {
1416
1424
  this.associations[name].association_name = name;
1417
1425
  }
1418
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
+
1419
1439
  return this.associations[name];
1420
1440
  });
1421
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
+
1422
1460
  /**
1423
1461
  * Add a field
1424
1462
  *
@@ -1442,17 +1480,33 @@ Select.setMethod(function addField(path) {
1442
1480
  *
1443
1481
  * @author Jelle De Loecker <jelle@develry.be>
1444
1482
  * @since 1.1.0
1445
- * @version 1.1.0
1483
+ * @version 1.2.0
1446
1484
  *
1447
- * @param {String} path
1485
+ * @param {String|Object} path
1448
1486
  *
1449
1487
  * @return {Criteria|Null} A criteria object if the context has changed
1450
1488
  */
1451
1489
  Select.setMethod(function parse(path) {
1452
1490
 
1453
- var context,
1491
+ let context,
1454
1492
  select = this,
1455
- 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);
1456
1510
 
1457
1511
  // Associations were found,
1458
1512
  // like "Comment._id" or "Comment.User"
@@ -1467,7 +1521,7 @@ Select.setMethod(function parse(path) {
1467
1521
  continue;
1468
1522
  }
1469
1523
 
1470
- select = this.addAssociation(name);
1524
+ select = select.addAssociation(name);
1471
1525
  }
1472
1526
  }
1473
1527
 
@@ -1486,7 +1540,7 @@ Select.setMethod(function parse(path) {
1486
1540
  *
1487
1541
  * @author Jelle De Loecker <jelle@develry.be>
1488
1542
  * @since 1.1.0
1489
- * @version 1.1.0
1543
+ * @version 1.2.0
1490
1544
  *
1491
1545
  * @param {Criteria} criteria
1492
1546
  *
@@ -1504,6 +1558,10 @@ Select.setMethod(function cloneForCriteria(criteria) {
1504
1558
  clone.fields = this.fields.slice(0);
1505
1559
  }
1506
1560
 
1561
+ if (this.query_fields && this.query_fields.length) {
1562
+ clone.query_fields = this.query_fields.slice(0);
1563
+ }
1564
+
1507
1565
  if (this.associations) {
1508
1566
  let key;
1509
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');
@@ -1058,6 +1081,26 @@ Model.setMethod(function addAssociatedDataToRecord(criteria, item, callback) {
1058
1081
  return;
1059
1082
  }
1060
1083
 
1084
+ // SchemaFields use a dummy Model name, so get the root_model in that case
1085
+ let root_model = (that.options && that.options.root_model) || that.name;
1086
+
1087
+ // Make sure references to the same model don't cause a deadlock
1088
+ // (Can happen when a model refers to itself)
1089
+ if (association.modelName == root_model && criteria.options._root_data) {
1090
+ let assoc_key = assoc_crit.options.assoc_key,
1091
+ assoc_value = assoc_crit.options.assoc_value,
1092
+ root = criteria.options._root_data[root_model];
1093
+
1094
+ if (root && root[assoc_key] && Object.alike(root[assoc_key], assoc_value)) {
1095
+
1096
+ aliases[alias] = nextAlias => {
1097
+ nextAlias(null, root[assoc_key]);
1098
+ };
1099
+
1100
+ return;
1101
+ }
1102
+ }
1103
+
1061
1104
  aliases[alias] = function aliasRecordTask(nextAlias) {
1062
1105
 
1063
1106
  var key;
@@ -1381,15 +1424,30 @@ Model.setProperty(function associations() {
1381
1424
  *
1382
1425
  * @author Jelle De Loecker <jelle@develry.be>
1383
1426
  * @since 1.0.0
1384
- * @version 1.1.0
1427
+ * @version 1.2.0
1385
1428
  *
1386
- * @param {String} name The name of the field
1429
+ * @param {string} path The path to the field
1387
1430
  *
1388
1431
  * @return {Object}
1389
1432
  */
1390
- Model.setMethod(function getField(name) {
1391
- return this.schema.getField(name);
1392
- }, 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
+ });
1393
1451
 
1394
1452
  // Make this class easily available
1395
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,