alchemymvc 1.2.8 → 1.3.1

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 (45) hide show
  1. package/lib/app/behaviour/sluggable_behaviour.js +4 -2
  2. package/lib/app/conduit/http_conduit.js +7 -2
  3. package/lib/app/conduit/loopback_conduit.js +2 -2
  4. package/lib/app/conduit/socket_conduit.js +20 -5
  5. package/lib/app/controller/alchemy_info_controller.js +4 -8
  6. package/lib/app/helper/backed_map.js +2 -2
  7. package/lib/app/helper/router_helper.js +98 -24
  8. package/lib/app/helper_controller/controller.js +45 -30
  9. package/lib/app/helper_datasource/00-nosql_datasource.js +44 -10
  10. package/lib/app/helper_field/enum_field.js +4 -4
  11. package/lib/app/helper_field/schema_field.js +50 -36
  12. package/lib/app/helper_model/document.js +81 -46
  13. package/lib/app/helper_model/field_set.js +11 -0
  14. package/lib/app/helper_model/model.js +107 -53
  15. package/lib/app/helper_validator/00_validator.js +38 -6
  16. package/lib/app/helper_validator/not_empty_validator.js +1 -3
  17. package/lib/app/routes.js +7 -1
  18. package/lib/bootstrap.js +1 -0
  19. package/lib/class/conduit.js +438 -290
  20. package/lib/class/controller.js +18 -15
  21. package/lib/class/datasource.js +19 -8
  22. package/lib/class/document.js +3 -3
  23. package/lib/class/field.js +34 -3
  24. package/lib/class/inode.js +27 -0
  25. package/lib/class/inode_file.js +204 -4
  26. package/lib/class/migration.js +2 -1
  27. package/lib/class/model.js +16 -5
  28. package/lib/class/path_definition.js +76 -120
  29. package/lib/class/path_param_definition.js +202 -0
  30. package/lib/class/postponement.js +573 -0
  31. package/lib/class/route.js +193 -33
  32. package/lib/class/router.js +22 -4
  33. package/lib/class/schema.js +47 -11
  34. package/lib/class/schema_client.js +65 -35
  35. package/lib/class/session.js +138 -12
  36. package/lib/class/sitemap.js +341 -0
  37. package/lib/core/base.js +13 -3
  38. package/lib/core/client_alchemy.js +78 -7
  39. package/lib/core/client_base.js +16 -10
  40. package/lib/core/middleware.js +56 -45
  41. package/lib/init/alchemy.js +124 -11
  42. package/lib/init/constants.js +11 -0
  43. package/lib/init/functions.js +163 -86
  44. package/lib/stages.js +18 -3
  45. package/package.json +6 -6
@@ -79,7 +79,7 @@ Sluggable.setStatic(function getSource(source, schema) {
79
79
  *
80
80
  * @author Jelle De Loecker <jelle@develry.be>
81
81
  * @since 0.2.0
82
- * @version 0.2.0
82
+ * @version 1.3.0
83
83
  *
84
84
  * @param {Schema} schema
85
85
  * @param {Object} options
@@ -113,7 +113,9 @@ Sluggable.setStatic(function attached(schema, new_options) {
113
113
  // Create the target if it doesn't exist yet
114
114
  if (!target) {
115
115
 
116
- field_options = {};
116
+ field_options = {
117
+ description: 'A human-readable yet unique identifier'
118
+ };
117
119
 
118
120
  // See if the target field we're adding needs to be translatable
119
121
  if (options.translatable || (options.translatable == null && source && source.is_translatable)) {
@@ -105,9 +105,9 @@ HttpConduit.enforceProperty(function fingerprint() {
105
105
  /**
106
106
  * Init
107
107
  *
108
- * @author Jelle De Loecker <jelle@kipdola.be>
108
+ * @author Jelle De Loecker <jelle@elevenways.be>
109
109
  * @since 0.3.3
110
- * @version 1.2.0
110
+ * @version 1.3.1
111
111
  *
112
112
  * @param {IncomingMessage} req
113
113
  * @param {ServerResponse} res
@@ -139,6 +139,11 @@ HttpConduit.setMethod(async function initHttp(req, res, router) {
139
139
 
140
140
  this.debugMark(false);
141
141
 
142
+ if (this.shouldBePostponed()) {
143
+ this.postponeAndQueue();
144
+ return;
145
+ }
146
+
142
147
  // Call the middleware, which will call the handler afterwards
143
148
  this.callMiddleware();
144
149
  });
@@ -78,7 +78,7 @@ LoopConduit.setMethod(function copyParentProperties(conduit) {
78
78
  *
79
79
  * @author Jelle De Loecker <jelle@elevenways.be>
80
80
  * @since 1.1.3
81
- * @version 1.2.7
81
+ * @version 1.3.0
82
82
  *
83
83
  * @param {Object} options
84
84
  * @param {Function} callback
@@ -165,7 +165,7 @@ LoopConduit.setMethod(function setOptions(options, callback) {
165
165
  }
166
166
 
167
167
  if (options.params) {
168
- this.params = options.params;
168
+ this.setRouteParameters(options.params);
169
169
  }
170
170
 
171
171
  if (options.arguments) {
@@ -102,19 +102,34 @@ var SocketConduit = Function.inherits('Alchemy.Conduit', function Socket(socket,
102
102
  /**
103
103
  * Return the client IP address
104
104
  *
105
- * @author Jelle De Loecker <jelle@develry.be>
105
+ * @author Jelle De Loecker <jelle@elevenways.be>
106
106
  * @since 0.2.1
107
- * @version 0.2.1
107
+ * @version 1.3.1
108
108
  */
109
109
  SocketConduit.setProperty(function ip() {
110
110
 
111
- var sock = this.socket;
111
+ let handshake = this.socket?.handshake;
112
112
 
113
- if (!sock) {
113
+ if (!handshake) {
114
114
  return null;
115
115
  }
116
116
 
117
- return sock.conn.remoteAddress || null;
117
+ if (handshake.headers) {
118
+ let forwarded_for = handshake.headers['x-forwarded-for'] || handshake.headers['x-real-ip'];
119
+
120
+ if (forwarded_for) {
121
+
122
+ // Forwarded for can contain multiple ip addresses,
123
+ // return the first one
124
+ if (forwarded_for.indexOf(',') > -1) {
125
+ forwarded_for = forwarded_for.before(',');
126
+ }
127
+
128
+ return forwarded_for;
129
+ }
130
+ }
131
+
132
+ return handshake.address || null;
118
133
  });
119
134
 
120
135
  /**
@@ -130,20 +130,16 @@ Info.setAction(async function appcache(conduit) {
130
130
  *
131
131
  * @author Jelle De Loecker <jelle@elevenways.be>
132
132
  * @since 1.1.0
133
- * @version 1.1.0
133
+ * @version 1.3.1
134
134
  */
135
135
  Info.setAction(function postponed(conduit, id) {
136
136
 
137
137
  var session = conduit.getSession(),
138
- postponed_conduit = session.postponed.get(id);
138
+ postponement = session.getPostponement(id);
139
139
 
140
- if (!postponed_conduit) {
140
+ if (!postponement) {
141
141
  return conduit.notFound();
142
142
  }
143
143
 
144
- postponed_conduit.response = conduit.response;
145
-
146
- postponed_conduit._end(...postponed_conduit._end_arguments);
147
-
148
- session.postponed.remove(id);
144
+ postponement.handleRequest(conduit);
149
145
  });
@@ -77,12 +77,12 @@ Backed.enforceProperty(function backing(new_value, old_value) {
77
77
  *
78
78
  * @author Jelle De Loecker <jelle@elevenways.be>
79
79
  * @since 1.2.1
80
- * @version 1.2.1
80
+ * @version 1.3.1
81
81
  *
82
82
  * @return {Backed}
83
83
  */
84
84
  Backed.setMethod(function clone() {
85
- let result = new Backed(this.backing);
85
+ let result = new this.constructor(this.backing);
86
86
  result.local = new Map(this.local);
87
87
  return result;
88
88
  });
@@ -46,12 +46,49 @@ Router.setProperty(function current_url() {
46
46
  }
47
47
  });
48
48
 
49
+ /**
50
+ * Check if the given URL is a local one
51
+ *
52
+ * @author Jelle De Loecker <jelle@elevenways.be>
53
+ * @since 1.3.0
54
+ * @version 1.3.0
55
+ *
56
+ * @param {String|RURL} url
57
+ *
58
+ * @return {Boolean}
59
+ */
60
+ Router.setMethod(function isLocalUrl(url) {
61
+
62
+ url = RURL.parse(url);
63
+
64
+ if (!url.hostname) {
65
+ return true;
66
+ }
67
+
68
+ if (Blast.isNode && alchemy.settings.url) {
69
+ // @TODO: Would be nice to not have to parse this every time
70
+ let server_url = RURL.parse(alchemy.settings.url);
71
+
72
+ if (server_url.hostname == url.hostname) {
73
+ return true;
74
+ }
75
+ }
76
+
77
+ let current_url = this.current_url;
78
+
79
+ if (current_url && current_url.hostname == url.hostname) {
80
+ return true;
81
+ }
82
+
83
+ return false;
84
+ });
85
+
49
86
  /**
50
87
  * Apply directive to an element
51
88
  *
52
- * @author Jelle De Loecker <jelle@develry.be>
89
+ * @author Jelle De Loecker <jelle@elevenways.be>
53
90
  * @since 1.1.0
54
- * @version 1.2.7
91
+ * @version 1.3.1
55
92
  *
56
93
  * @param {Element} element The element to apply to
57
94
  * @param {String} name The route name
@@ -83,14 +120,23 @@ Router.setMethod(function applyDirective(element, name, options) {
83
120
  if (config.keys && config.keys.length) {
84
121
 
85
122
  let key,
86
- val,
87
- i;
123
+ val;
88
124
 
89
- for (i = 0; i < config.keys.length; i++) {
90
- key = config.keys[i];
125
+ for (key of config.keys) {
91
126
 
92
127
  if (params[key] == null) {
93
- if ((val = element['route_' + key]) == null) {
128
+
129
+ val = element['route_' + key];
130
+
131
+ if (val == null) {
132
+ let variables = element[Hawkejs.VARIABLES];
133
+
134
+ if (variables) {
135
+ val = variables[key];
136
+ }
137
+ }
138
+
139
+ if (val == null) {
94
140
  val = element[key];
95
141
  }
96
142
 
@@ -284,14 +330,14 @@ Router.setMethod(function routeConfig(name, socket_route) {
284
330
  *
285
331
  * @author Jelle De Loecker <jelle@develry.be>
286
332
  * @since 0.2.0
287
- * @version 1.2.1
333
+ * @version 1.3.0
288
334
  *
289
335
  * @param {String} name
290
336
  * @param {Object} parameters
291
337
  *
292
338
  * @return {RURL}
293
339
  */
294
- Router.setMethod(function routeUrl(name, _parameters, options) {
340
+ Router.setMethod(function routeUrl(name, parameters, options) {
295
341
 
296
342
  var parameters,
297
343
  base_url,
@@ -307,10 +353,9 @@ Router.setMethod(function routeUrl(name, _parameters, options) {
307
353
 
308
354
  url = '';
309
355
  config = options.config || this.routeConfig(name);
310
- parameters = Blast.Bound.Object.assign({}, _parameters);
311
356
 
312
- if (options.locale) {
313
- locales = [options.locale];
357
+ if (options.locale || options.prefix) {
358
+ locales = [options.prefix || options.locale];
314
359
  } else if (options.locales) {
315
360
  locales = options.locales;
316
361
  } else if (this.view) {
@@ -393,11 +438,24 @@ Router.setMethod(function routeUrl(name, _parameters, options) {
393
438
  }
394
439
 
395
440
  if (options.full || options.absolute) {
396
- base_url = this.view.internal('url') || RURL.parse(window.location);
397
- url.protocol = base_url.protocol;
398
- url.host = base_url.host;
399
- url.hostname = base_url.hostname;
400
- url.port = base_url.port;
441
+ if (this.view) {
442
+ base_url = this.view.internal('url');
443
+ }
444
+
445
+ if (!base_url) {
446
+ if (Blast.isBrowser) {
447
+ base_url = RURL.parse(window.location);
448
+ } else {
449
+ base_url = RURL.parse(alchemy.settings.url);
450
+ }
451
+ }
452
+
453
+ if (base_url) {
454
+ url.protocol = base_url.protocol;
455
+ url.host = base_url.host;
456
+ url.hostname = base_url.hostname;
457
+ url.port = base_url.port;
458
+ }
401
459
  }
402
460
 
403
461
  url._locales = locales;
@@ -631,11 +689,12 @@ Router.setMethod(function getRouteVariables() {
631
689
  *
632
690
  * @author Jelle De Loecker <jelle@elevenways.be>
633
691
  * @since 1.2.5
634
- * @version 1.2.5
692
+ * @version 1.3.0
635
693
  *
636
694
  * @param {Element} element The element to apply to
695
+ * @param {Object} variables
637
696
  */
638
- Router.setMethod(function updateLanguageSwitcher(element) {
697
+ Router.setMethod(function updateLanguageSwitcher(element, variables) {
639
698
 
640
699
  let language = element.getAttribute('data-alchemy-language-switch');
641
700
 
@@ -643,7 +702,7 @@ Router.setMethod(function updateLanguageSwitcher(element) {
643
702
  return;
644
703
  }
645
704
 
646
- let url = this.translateCurrentRoute(language);
705
+ let url = this.translateCurrentRoute(language, variables);
647
706
 
648
707
  if (!url) {
649
708
  url = '/' + language;
@@ -657,11 +716,21 @@ Router.setMethod(function updateLanguageSwitcher(element) {
657
716
  *
658
717
  * @author Jelle De Loecker <jelle@elevenways.be>
659
718
  * @since 1.2.5
660
- * @version 1.2.5
719
+ * @version 1.3.0
661
720
  *
662
721
  * @param {String} prefix The prefix to use
722
+ * @param {Object} variables
663
723
  */
664
- Router.setMethod(function translateCurrentRoute(prefix) {
724
+ Router.setMethod(function translateCurrentRoute(prefix, variables) {
725
+
726
+ let current_route_translations = variables?.__current_route_translations,
727
+ current_route_translation = current_route_translations?.[prefix] ?? null;
728
+
729
+ if (current_route_translation) {
730
+ return current_route_translation;
731
+ } else if (current_route_translation === false) {
732
+ return false;
733
+ }
665
734
 
666
735
  let info = this.getRouteVariables();
667
736
 
@@ -685,6 +754,11 @@ Router.setMethod(function translateCurrentRoute(prefix) {
685
754
  url.pathname = '/' + prefix + url.pathname;
686
755
  }
687
756
 
757
+ // Don't return urls with missing parameters
758
+ if (url.pathname.indexOf('{') > -1) {
759
+ return false;
760
+ }
761
+
688
762
  // Add the get queries
689
763
  if (info.url && info.url.search) {
690
764
  for (key in info.url.query) {
@@ -705,7 +779,7 @@ Router.setMethod(function translateCurrentRoute(prefix) {
705
779
  *
706
780
  * @author Jelle De Loecker <jelle@elevenways.be>
707
781
  * @since 1.2.5
708
- * @version 1.2.5
782
+ * @version 1.3.0
709
783
  *
710
784
  * @param {Element} element The element to apply to
711
785
  * @param {String} language The actual language
@@ -718,7 +792,7 @@ Router.setMethod(function languageSwitcherDirective(element, language, options)
718
792
  element.setAttribute('data-alchemy-language-switch', language);
719
793
  element.setAttribute('rel', 'nofollow');
720
794
 
721
- this.updateLanguageSwitcher(element);
795
+ this.updateLanguageSwitcher(element, element.hawkejs_renderer?.variables);
722
796
  });
723
797
 
724
798
  /**
@@ -159,6 +159,22 @@ Controller.setMethod(function addComponent(name, options) {
159
159
  this.components[underscored] = new Blast.Classes.Alchemy.Client.Component[name](this, options);
160
160
  }, false);
161
161
 
162
+ /**
163
+ * Change the response URL (or disable it)
164
+ *
165
+ * @author Jelle De Loecker <jelle@elevenways.be>
166
+ * @since 1.3.0
167
+ * @version 1.3.0
168
+ *
169
+ * @param {String|RURL|Boolean} new_url
170
+ */
171
+ Controller.setMethod(function setResponseUrl(new_url) {
172
+
173
+ // @TODO: will only work when called on the server-side
174
+ if (this.conduit) {
175
+ this.conduit.setResponseUrl(new_url);
176
+ }
177
+ });
162
178
 
163
179
  /**
164
180
  * Set a variable for ViewRender, through conduit
@@ -308,12 +324,12 @@ Controller.setMethod(function end(message) {
308
324
  *
309
325
  * @author Jelle De Loecker <jelle@develry.be>
310
326
  * @since 0.2.0
311
- * @version 1.1.0
327
+ * @version 1.3.0
312
328
  *
313
329
  * @param {String} name The name of the action to execute
314
330
  * @param {Array} args Arguments to apply to the action
315
331
  */
316
- Controller.setMethod(function doAction(name, args) {
332
+ Controller.setMethod(async function doAction(name, args) {
317
333
 
318
334
  var that = this;
319
335
 
@@ -338,43 +354,42 @@ Controller.setMethod(function doAction(name, args) {
338
354
  this.initial_action_arguments = args;
339
355
  }
340
356
 
341
- if (this.conduit && this.conduit.route && this.conduit.route.options && this.conduit.route.options.title) {
357
+ const route = this.conduit?.route;
342
358
 
343
- let title = this.conduit.route.options.title;
359
+ if (route) {
344
360
 
345
- if (alchemy.settings && alchemy.settings.title_suffix) {
346
- title += alchemy.settings.title_suffix;
347
- }
361
+ if (route.options?.title) {
362
+ let title = route.options.title;
348
363
 
349
- this.setTitle(title);
350
- }
364
+ if (alchemy.settings && alchemy.settings.title_suffix) {
365
+ title += alchemy.settings.title_suffix;
366
+ }
351
367
 
352
- Function.series(function initializing(next) {
353
- that.issueEvent('initializing', next);
354
- }, function filtering(next) {
355
- that.issueEvent('filtering', next);
356
- }, function starting(next) {
357
- that.issueEvent('starting', [name], next);
358
- }, function actioning(next) {
359
-
360
- var result = that.constructor.actions[name].apply(that, args);
361
-
362
- if (result && typeof result.then == 'function') {
363
- result.then(function() {
364
- next();
365
- }).catch(next);
366
- } else {
367
- next();
368
+ this.setTitle(title);
368
369
  }
369
370
 
370
- }, function done(err) {
371
+ if (route.requires_data_for_translation && route.visible_location !== false && this.conduit.prefix) {
372
+ let route_translations = route.getRouteTranslations(this, this.conduit);
371
373
 
372
- if (err != null) {
373
- that.conduit.error(err);
374
- return;
374
+ if (route_translations) {
375
+ route_translations = await route_translations;
376
+ this.internal('current_route_translations', route_translations);
377
+ }
375
378
  }
376
379
 
377
- });
380
+ if (route.visible_location !== true) {
381
+ this.setResponseUrl(route.visible_location);
382
+ }
383
+ }
384
+
385
+ try {
386
+ await this.issueEvent('initializing');
387
+ await this.issueEvent('filtering');
388
+ await this.issueEvent('starting', [name]);
389
+ await this.constructor.actions[name].apply(this, args);
390
+ } catch (err) {
391
+ this.conduit.error(err);
392
+ }
378
393
  });
379
394
 
380
395
  /**
@@ -590,7 +590,7 @@ NoSQL.setStatic(function areComparable(a, b) {
590
590
  *
591
591
  * @author Jelle De Loecker <jelle@develry.be>
592
592
  * @since 1.1.0
593
- * @version 1.2.2
593
+ * @version 1.3.0
594
594
  *
595
595
  * @param {Criteria} criteria
596
596
  * @param {Group} group
@@ -710,12 +710,14 @@ NoSQL.setMethod(function compileCriteria(criteria, group) {
710
710
  obj = {};
711
711
 
712
712
  let field_entry = {},
713
- name = entry.target_path;
713
+ name = entry.target_path,
714
+ queries_property = name.indexOf('.') > -1;
714
715
 
715
716
  // Do we need to look into an object itself?
716
717
  // (Like the "timestamp" property of a date field when stored with units)
717
- if (entry.db_property) {
718
+ if (entry.db_property && !queries_property) {
718
719
  name += '.' + entry.db_property;
720
+ queries_property = true;
719
721
  }
720
722
 
721
723
  if (entry.association) {
@@ -815,15 +817,46 @@ NoSQL.setMethod(function compileCriteria(criteria, group) {
815
817
  throw new Error('Unknown criteria expression: "' + item.type + '"');
816
818
  }
817
819
 
820
+ let multiple_fields,
821
+ prefixed_name = name;
822
+
823
+ // Temporary fix to actually query translatable field contents
824
+ // (entry.field can be undefined if trying to query a path)
825
+ if (entry.field && entry.field.is_translatable) {
826
+
827
+ let prefix = criteria.options.locale;
828
+
829
+ // If a prefix is specified, only query that translation
830
+ if (prefix) {
831
+ prefixed_name = name + '.' + prefix;
832
+ } else {
833
+ multiple_fields = [];
834
+
835
+ // No prefixes specified, so look through all translations
836
+ for (let key in Prefix.all()) {
837
+ multiple_fields.push(name + '.' + key);
838
+ }
839
+ }
840
+ }
841
+
818
842
  if (obj && obj.$or) {
819
843
 
820
844
  let $or = [],
821
845
  i;
822
846
 
823
847
  for (i = 0; i < obj.$or.length; i++) {
824
- $or.push({
825
- [name] : obj.$or[i]
826
- });
848
+
849
+ if (multiple_fields) {
850
+ for (let name of multiple_fields) {
851
+ $or.push({
852
+ [name] : obj.$or[i]
853
+ });
854
+ }
855
+ } else {
856
+ $or.push({
857
+ [prefixed_name] : obj.$or[i]
858
+ });
859
+ }
827
860
  }
828
861
 
829
862
  if (not) {
@@ -861,18 +894,19 @@ NoSQL.setMethod(function compileCriteria(criteria, group) {
861
894
 
862
895
  // Temporary fix to actually query translatable field contents
863
896
  // (entry.field can be undefined if trying to query a path)
864
- if (entry.field && entry.field.is_translatable) {
897
+ if (multiple_fields) {
898
+
865
899
  let $or = [];
866
900
 
867
- for (let key in Prefix.all()) {
901
+ for (let name of multiple_fields) {
868
902
  let temp = {};
869
- temp[name + '.' + key] = obj;
903
+ temp[name] = obj;
870
904
  $or.push(temp);
871
905
  }
872
906
 
873
907
  field_entry.$or = $or;
874
908
  } else {
875
- field_entry[name] = obj;
909
+ field_entry[prefixed_name] = obj;
876
910
  }
877
911
  }
878
912
 
@@ -45,7 +45,7 @@ Enum.setMethod(function cast(value) {
45
45
  *
46
46
  * @author Jelle De Loecker <jelle@develry.be>
47
47
  * @since 0.2.0
48
- * @version 1.2.1
48
+ * @version 1.3.0
49
49
  *
50
50
  * @return {EnumValues}
51
51
  */
@@ -55,10 +55,10 @@ Enum.setMethod(function getValues() {
55
55
  // or its pluralized name, if nothing was found
56
56
  if (this.options && this.options.values) {
57
57
  return this.options.values;
58
- } else if (this.schema.enumValues[this.name]) {
59
- return this.schema.enumValues[this.name];
58
+ } else if (this.schema.enum_values[this.name]) {
59
+ return this.schema.enum_values[this.name];
60
60
  } else {
61
- return this.schema.enumValues[this.name.pluralize()];
61
+ return this.schema.enum_values[this.name.pluralize()];
62
62
  }
63
63
  });
64
64