alchemymvc 1.4.0-alpha.6 → 1.4.0-alpha.8

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.
@@ -680,6 +680,11 @@ Model.setMethod(function getAliasModel(alias) {
680
680
  config = this.schema.associations[alias];
681
681
  }
682
682
 
683
+ // @TODO: associated fields with an alias name inside a nested schema
684
+ // somehow breaks! Should be looked in to?
685
+ // Workaround: set `recursive: 0` on the root schema field
686
+ // console.log('Alias:', alias, 'config:', config, this.schema);
687
+
683
688
  if (config) {
684
689
  result = this.getModel(config.modelName);
685
690
  } else {
@@ -144,11 +144,19 @@ SystemSetting.setDocumentMethod(function applySetting(do_actions = true) {
144
144
 
145
145
  if (!existing_value) {
146
146
  existing_value = setting.generateValue();
147
+ alchemy.system_settings.forceValueInstanceAtPath(this.setting_id, existing_value);
147
148
  }
148
149
 
150
+ let result;
151
+
149
152
  if (do_actions) {
150
- return existing_value.setValue(this.configuration.value);
153
+ result = existing_value.setValue(this.configuration.value);
151
154
  } else {
152
- return existing_value.setValueSilently(this.configuration.value);
155
+ result = existing_value.setValueSilently(this.configuration.value);
153
156
  }
157
+
158
+ // And now force the settings to update!
159
+ alchemy.refreshSettingsObject();
160
+
161
+ return result;
154
162
  });
@@ -17,7 +17,7 @@ let shared_objects = {},
17
17
  *
18
18
  * @author Jelle De Loecker <jelle@elevenways.be>
19
19
  * @since 0.0.1
20
- * @version 1.3.21
20
+ * @version 1.4.0
21
21
  */
22
22
  global.Alchemy = Function.inherits('Alchemy.Base', function Alchemy() {
23
23
 
@@ -147,7 +147,6 @@ global.Alchemy = Function.inherits('Alchemy.Base', function Alchemy() {
147
147
  this.text_body = this.use('body');
148
148
  this.formidable = this.use('formidable');
149
149
  this.body_parser = this.use('body-parser');
150
- this.url_form_body = this.body_parser.urlencoded({extended: true});
151
150
  });
152
151
 
153
152
  /**
@@ -581,10 +580,21 @@ Alchemy.setMethod(function setSetting(path, value) {
581
580
  this.system_settings.setPathSilently(path, value);
582
581
 
583
582
  if (this.started) {
584
- this.settings = this.system_settings.toObject();
583
+ this.refreshSettingsObject();
585
584
  }
586
585
  });
587
586
 
587
+ /**
588
+ * Refresh the settings object
589
+ *
590
+ * @author Jelle De Loecker <jelle@elevenways.be>
591
+ * @since 1.4.0
592
+ * @version 1.4.0
593
+ */
594
+ Alchemy.setMethod(function refreshSettingsObject() {
595
+ this.settings = this.system_settings.toObject();
596
+ });
597
+
588
598
  /**
589
599
  * Get a setting value
590
600
  *
@@ -1745,12 +1755,43 @@ Alchemy.setMethod(function addAppcacheEntry(entry) {
1745
1755
  ac_entries[entry.type].push(entry);
1746
1756
  });
1747
1757
 
1758
+ /**
1759
+ * Get a size limit for the given route
1760
+ *
1761
+ * @author Jelle De Loecker <jelle@elevenways.be>
1762
+ * @since 1.4.0
1763
+ * @version 1.4.0
1764
+ *
1765
+ * @param {Route?} route
1766
+ * @param {string} name
1767
+ */
1768
+ function getRouteSizeLimit(route, name) {
1769
+
1770
+ let global_size = alchemy.settings.network[name];
1771
+
1772
+ if (!route) {
1773
+ return global_size;
1774
+ }
1775
+
1776
+ let route_value = route.options[name];
1777
+
1778
+ if (route_value == null || typeof route_value != 'number') {
1779
+ return global_size;
1780
+ }
1781
+
1782
+ if (route_value <= 0) {
1783
+ return Infinity;
1784
+ }
1785
+
1786
+ return route_value;
1787
+ }
1788
+
1748
1789
  /**
1749
1790
  * Get the body of an IncomingMessage
1750
1791
  *
1751
1792
  * @author Jelle De Loecker <jelle@elevenways.be>
1752
1793
  * @since 1.1.0
1753
- * @version 1.3.18
1794
+ * @version 1.4.0
1754
1795
  *
1755
1796
  * @param {IncomingMessage} req
1756
1797
  * @param {OutgoingMessage} res Optional
@@ -1777,24 +1818,44 @@ Alchemy.setMethod(function parseRequestBody(req, res, callback) {
1777
1818
 
1778
1819
  let content_type = req.headers['content-type'];
1779
1820
 
1821
+ let request_body_size_limit = getRouteSizeLimit(conduit?.route, 'request_body_size_limit');
1822
+
1780
1823
  // Multipart data is handled by "formidable"
1781
1824
  if (content_type && content_type.startsWith('multipart/form-data')) {
1782
1825
 
1826
+ let request_individual_file_size_limit = getRouteSizeLimit(conduit?.route, 'request_individual_file_size_limit'),
1827
+ request_total_file_size_limit = getRouteSizeLimit(conduit?.route, 'request_total_file_size_limit');
1828
+
1783
1829
  let form = new this.formidable.IncomingForm({
1784
1830
  multiples : true,
1785
1831
  hashAlgorithm : this.settings.data_management.file_hash_algorithm || 'sha1',
1832
+ minFileSize : 0,
1833
+ allowEmptyFiles : true,
1834
+ maxFileSize : request_individual_file_size_limit,
1835
+ maxFieldsSize : request_body_size_limit,
1836
+ maxTotalFileSize : request_total_file_size_limit,
1786
1837
  });
1787
1838
 
1788
1839
  form.parse(req, function parsedMultipart(err, form_fields, form_files) {
1789
1840
 
1790
- var fields = {},
1791
- files = {},
1792
- key;
1841
+ if (err) {
1842
+
1843
+ // Ignore the error if the request was already aborted
1844
+ if (conduit?.aborted) {
1845
+ return callback(null);
1846
+ }
1793
1847
 
1794
- if (err && req.conduit && req.conduit.aborted) {
1795
- return callback(null);
1848
+ if (conduit) {
1849
+ return conduit.error(err);
1850
+ }
1851
+
1852
+ return callback(err);
1796
1853
  }
1797
1854
 
1855
+ let fields = {},
1856
+ files = {},
1857
+ key;
1858
+
1798
1859
  // Since formidable v3, all the fields are now arrays.
1799
1860
  // We already had a lot of logic to deal with this,
1800
1861
  // so we just have to un-array everything
@@ -1811,18 +1872,12 @@ Alchemy.setMethod(function parseRequestBody(req, res, callback) {
1811
1872
  Object.setFormPath(files, key, form_files[key]);
1812
1873
  }
1813
1874
 
1814
- if (err) {
1815
- log.error('Error parsing multipart POST', {err: err});
1816
- req.body = {};
1817
- req.files = {};
1818
- } else {
1819
- req.body = fields;
1820
- req.files = files;
1875
+ req.body = fields;
1876
+ req.files = files;
1821
1877
 
1822
- if (conduit) {
1823
- conduit.setRequestBody(fields);
1824
- conduit.setRequestFiles(files);
1825
- }
1878
+ if (conduit) {
1879
+ conduit.setRequestBody(fields);
1880
+ conduit.setRequestFiles(files);
1826
1881
  }
1827
1882
 
1828
1883
  callback(null, fields);
@@ -1834,24 +1889,34 @@ Alchemy.setMethod(function parseRequestBody(req, res, callback) {
1834
1889
  // Regular form-encoded data
1835
1890
  if (content_type && content_type.indexOf('form-urlencoded') > -1) {
1836
1891
 
1837
- this.url_form_body(req, res, function parsedBody(err) {
1892
+ let url_form_body = this.body_parser.urlencoded({
1893
+ limit: request_body_size_limit,
1894
+ extended: true,
1895
+ });
1838
1896
 
1839
- if (err && req.conduit && req.conduit.aborted) {
1840
- return callback(null);
1897
+ url_form_body(req, res, function parsedBody(err) {
1898
+
1899
+ if (err) {
1900
+
1901
+ // Ignore the error if the request was already aborted
1902
+ if (conduit?.aborted) {
1903
+ return callback(null);
1904
+ }
1905
+
1906
+ if (conduit) {
1907
+ return conduit.error(err);
1908
+ }
1909
+
1910
+ return callback(err);
1841
1911
  }
1842
1912
 
1843
1913
  // You can't send files using a regular post
1844
1914
  req.files = {};
1845
1915
 
1846
- if (err) {
1847
- log.error('Error parsing x-www-form-urlencoded body data', {err: err});
1848
- req.body = {};
1849
- } else {
1850
- req.body = req.body;
1916
+ req.body = req.body;
1851
1917
 
1852
- if (conduit) {
1853
- conduit.setRequestBody(req.body);
1854
- }
1918
+ if (conduit) {
1919
+ conduit.setRequestBody(req.body);
1855
1920
  }
1856
1921
 
1857
1922
  callback(null, req.body);
@@ -1861,25 +1926,30 @@ Alchemy.setMethod(function parseRequestBody(req, res, callback) {
1861
1926
  }
1862
1927
 
1863
1928
  // Any other encoded data (like JSON)
1864
- this.any_body(req, function parsedBody(err, body) {
1929
+ this.any_body(req, res, {limit: request_body_size_limit}, function parsedBody(err, body) {
1865
1930
 
1866
1931
  function handleResponse(err, body) {
1867
- if (err && req.conduit && req.conduit.aborted) {
1868
- return callback(null);
1932
+
1933
+ if (err) {
1934
+
1935
+ // Ignore the error if the request was already aborted
1936
+ if (conduit?.aborted) {
1937
+ return callback(null);
1938
+ }
1939
+
1940
+ if (conduit) {
1941
+ return conduit.error(err);
1942
+ }
1943
+
1944
+ return callback(err);
1869
1945
  }
1870
-
1946
+
1871
1947
  // You can't send files using a regular post
1872
1948
  req.files = {};
1949
+ req.body = body;
1873
1950
 
1874
- if (err) {
1875
- log.error('Error parsing body data', {err: err});
1876
- req.body = {};
1877
- } else {
1878
- req.body = body;
1879
-
1880
- if (conduit) {
1881
- conduit.setRequestBody(body);
1882
- }
1951
+ if (conduit) {
1952
+ conduit.setRequestBody(body);
1883
1953
  }
1884
1954
 
1885
1955
  callback(null, req.body);
@@ -1,6 +1,7 @@
1
1
  let mkdirp = alchemy.use('mkdirp')?.mkdirp,
2
2
  ncp = alchemy.use('ncp').ncp,
3
3
  fs = alchemy.use('fs'),
4
+ fsp = fs.promises,
4
5
  libpath = alchemy.use('path'),
5
6
  child = alchemy.use('child_process'),
6
7
  crypto = alchemy.use('crypto'),
@@ -1106,6 +1107,53 @@ Alchemy.setMethod(function request(url, options, callback) {
1106
1107
  return Blast.fetch(options, callback);
1107
1108
  });
1108
1109
 
1110
+ /**
1111
+ * Turn a `data:` uri into a file
1112
+ *
1113
+ * @author Jelle De Loecker <jelle@elevenways.be>
1114
+ * @since 1.4.0
1115
+ * @version 1.4.0
1116
+ *
1117
+ * @param {string} data_uri
1118
+ *
1119
+ * @return {Pledge<File>}
1120
+ */
1121
+ function convertDataUriToFile(data_uri) {
1122
+
1123
+ // Split the data uri into its 2 parts
1124
+ let pieces = data_uri.slice(5).split(',');
1125
+
1126
+ // Split the info bit
1127
+ let info = pieces[0].split(';');
1128
+
1129
+ // Get the expected mime type
1130
+ let mime_type = info[0];
1131
+
1132
+ // And is it base64?
1133
+ let is_b64 = info[1] && info[1].toLowerCase() == 'base64';
1134
+
1135
+ let buffer;
1136
+
1137
+ if (is_b64) {
1138
+ buffer = Buffer.from(pieces[1], 'base64');
1139
+ } else {
1140
+ buffer = Buffer.from(String.decodeURI(pieces[1]));
1141
+ }
1142
+
1143
+ return Pledge.Swift.waterfall(
1144
+ () => Blast.createTempDir({prefix: 'aldl'}),
1145
+ async (temp_dir) => {
1146
+ let full_path = libpath.resolve(temp_dir, 'buffer_' + alchemy.ObjectId());
1147
+
1148
+ await fsp.writeFile(full_path, buffer);
1149
+
1150
+ return full_path;
1151
+ },
1152
+ (full_path) => Classes.Alchemy.Inode.Inode.from(full_path)
1153
+ );
1154
+
1155
+ }
1156
+
1109
1157
  /**
1110
1158
  * Download a file
1111
1159
  *
@@ -1119,6 +1167,14 @@ Alchemy.setMethod(function request(url, options, callback) {
1119
1167
  */
1120
1168
  Alchemy.setMethod(function download(url, options) {
1121
1169
 
1170
+ if (url.startsWith('data:')) {
1171
+ try {
1172
+ return convertDataUriToFile(url, options);
1173
+ } catch (err) {
1174
+ return Pledge.reject(err);
1175
+ }
1176
+ }
1177
+
1122
1178
  const pledge = new Pledge();
1123
1179
 
1124
1180
  // Get the file
package/lib/core/base.js CHANGED
@@ -564,7 +564,7 @@ Base.setMethod(function attachConduit(conduit) {
564
564
  *
565
565
  * @author Jelle De Loecker <jelle@elevenways.be>
566
566
  * @since 0.3.0
567
- * @version 1.1.8
567
+ * @version 1.4.0
568
568
  *
569
569
  * @param {string} name The name of the model to get
570
570
  * @param {boolean} init Initialize the class [true]
@@ -597,9 +597,9 @@ Base.setMethod(function getModel(name, init, options) {
597
597
 
598
598
  if (options.cache !== false) {
599
599
  if (!this._modelInstances) {
600
- this._modelInstances = {};
600
+ this._modelInstances = new Map();
601
601
  } else {
602
- instance = this._modelInstances[name];
602
+ instance = this._modelInstances.get(name);
603
603
  }
604
604
  }
605
605
 
@@ -622,7 +622,7 @@ Base.setMethod(function getModel(name, init, options) {
622
622
  }
623
623
 
624
624
  if (options.cache !== false) {
625
- this._modelInstances[name] = instance;
625
+ this._modelInstances.set(name, instance);
626
626
  }
627
627
 
628
628
  return instance;
@@ -452,6 +452,9 @@ const Group = Function.inherits('Alchemy.Setting.Base', function Group(name, con
452
452
 
453
453
  // All the children
454
454
  this.children = new Map();
455
+
456
+ // Weak references to existing values
457
+ this.weak_values = new Blast.Classes.WeakValueSet();
455
458
  });
456
459
 
457
460
  /**
@@ -583,6 +586,14 @@ Group.setMethod(function createGroup(name) {
583
586
  let group = new Group(name, this);
584
587
  this.children.set(name, group);
585
588
 
589
+ if (this.weak_values.size) {
590
+ let group_value = group.generateValue();
591
+
592
+ for (let existing of this.weak_values) {
593
+ this.setDefaultValue(existing, {[name]: group_value});
594
+ }
595
+ }
596
+
586
597
  return group;
587
598
  });
588
599
 
@@ -633,6 +644,8 @@ Group.setMethod(function generateValue() {
633
644
 
634
645
  this.setDefaultValue(result, object);
635
646
 
647
+ this.weak_values.add(result);
648
+
636
649
  return result;
637
650
  });
638
651
 
@@ -1344,6 +1357,38 @@ GroupValue.setMethod(function _setPath(silent, path, raw_value) {
1344
1357
  return this.getPath(path);
1345
1358
  });
1346
1359
 
1360
+ /**
1361
+ * Force a value at the given path
1362
+ *
1363
+ * @author Jelle De Loecker <jelle@elevenways.be>
1364
+ * @since 1.4.0
1365
+ * @version 1.4.0
1366
+ *
1367
+ * @param {string|Array} path
1368
+ * @param {Value}
1369
+ */
1370
+ GroupValue.setMethod(function forceValueInstanceAtPath(path, value) {
1371
+
1372
+ if (typeof path == 'string') {
1373
+ path = path.split('.');
1374
+ }
1375
+
1376
+ if (this.definition.group == null && path[0] == this.definition.name) {
1377
+ path.shift();
1378
+ }
1379
+
1380
+ let last = path.pop();
1381
+
1382
+ let current = this;
1383
+
1384
+ while (path.length && current) {
1385
+ let next = path.shift();
1386
+ current = current.get(next);
1387
+ }
1388
+
1389
+ current[VALUE][last] = value;
1390
+ });
1391
+
1347
1392
  /**
1348
1393
  * Convert to a datasource array
1349
1394
  *
@@ -1584,6 +1629,20 @@ Value.setMethod(function getPath(path) {
1584
1629
  return current;
1585
1630
  });
1586
1631
 
1632
+ /**
1633
+ * Force a value at the given path
1634
+ *
1635
+ * @author Jelle De Loecker <jelle@elevenways.be>
1636
+ * @since 1.4.0
1637
+ * @version 1.4.0
1638
+ *
1639
+ * @param {string|Array} path
1640
+ * @param {Value}
1641
+ */
1642
+ Value.setMethod(function forceValueInstanceAtPath(path, value) {
1643
+ throw new Error('Unable to perform on a simple Value instance');
1644
+ });
1645
+
1587
1646
  if (Blast.isBrowser) {
1588
1647
  return;
1589
1648
  }
@@ -1675,15 +1734,61 @@ const MagicGroupValue = Function.inherits('Magic', 'Alchemy.Setting', function M
1675
1734
  */
1676
1735
  MagicGroupValue.setMethod(function __get(key) {
1677
1736
 
1678
- let result = this[VALUE].get(key);
1737
+ let result = this[key];
1738
+
1739
+ if (result != null) {
1740
+ return result;
1741
+ }
1742
+
1743
+ result = this[VALUE].get(key);
1744
+
1745
+ if (result == null) {
1746
+ return this[VALUE][key];
1747
+ }
1679
1748
 
1680
1749
  if (!result) {
1681
- return;
1750
+ return result;
1682
1751
  }
1683
1752
 
1684
1753
  if (result.is_group) {
1685
1754
  return result.toProxyObject();
1686
1755
  }
1687
1756
 
1757
+ if (!result.get) {
1758
+ return result;
1759
+ }
1760
+
1688
1761
  return result.get();
1762
+ });
1763
+
1764
+ /**
1765
+ * The magic getter
1766
+ *
1767
+ * @author Jelle De Loecker <jelle@elevenways.be>
1768
+ * @since 1.4.0
1769
+ * @version 1.4.0
1770
+ *
1771
+ * @param {string} key
1772
+ */
1773
+ MagicGroupValue.setMethod(function __ownKeys() {
1774
+ return Object.keys(this[VALUE].toObject())
1775
+ });
1776
+
1777
+ /**
1778
+ * The magic getter
1779
+ *
1780
+ * @author Jelle De Loecker <jelle@elevenways.be>
1781
+ * @since 1.4.0
1782
+ * @version 1.4.0
1783
+ *
1784
+ * @param {string} key
1785
+ */
1786
+ MagicGroupValue.setMethod(function __describe(key) {
1787
+ let result = Object.getOwnPropertyDescriptor(this[VALUE], key);
1788
+
1789
+ if (result == null) {
1790
+ result = Object.getOwnPropertyDescriptor(this, key);
1791
+ }
1792
+
1793
+ return result;
1689
1794
  });
@@ -73,6 +73,24 @@ network.addSetting('use_compression', {
73
73
  description : 'Compress responses using gzip/deflate',
74
74
  });
75
75
 
76
+ network.addSetting('request_body_size_limit', {
77
+ type : 'number',
78
+ default : 20 * 1024 * 1024,
79
+ description : 'Maximum allowed size in bytes for the request body, excluding files',
80
+ });
81
+
82
+ network.addSetting('request_individual_file_size_limit', {
83
+ type : 'number',
84
+ default : 200 * 1024 * 1024,
85
+ description : 'Maximum allowed size in bytes for individual uploaded files',
86
+ });
87
+
88
+ network.addSetting('request_total_file_size_limit', {
89
+ type : 'number',
90
+ default : 200 * 1024 * 1024,
91
+ description : 'Maximum allowed size in bytes for all uploaded files',
92
+ });
93
+
76
94
  network.addSetting('use_json_dry_responses', {
77
95
  type : 'boolean',
78
96
  default : true,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "alchemymvc",
3
3
  "description": "MVC framework for Node.js",
4
- "version": "1.4.0-alpha.6",
4
+ "version": "1.4.0-alpha.8",
5
5
  "author": "Jelle De Loecker <jelle@elevenways.be>",
6
6
  "keywords": [
7
7
  "alchemy",