alchemymvc 1.4.0-alpha.5 → 1.4.0-alpha.7

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.
@@ -131,7 +131,11 @@ Mongo.setMethod(function castToBigInt(value) {
131
131
  }
132
132
 
133
133
  if (typeof value == 'object') {
134
- return value.toBigInt();
134
+ if (value.toBigInt) {
135
+ return value.toBigInt();
136
+ }
137
+
138
+ return null;
135
139
  }
136
140
 
137
141
  return BigInt(value);
@@ -487,9 +491,21 @@ Mongo.setMethod(function _create(context) {
487
491
  this.collection(model.table),
488
492
  collection => {
489
493
 
490
- const data = context.getConvertedData();
494
+ const converted_data = context.getConvertedData();
491
495
  const pledge = new Swift();
492
496
 
497
+ let data = {};
498
+
499
+ for (let key in converted_data) {
500
+ let val = converted_data[key];
501
+
502
+ if (val == null) {
503
+ continue;
504
+ }
505
+
506
+ data[key] = val;
507
+ }
508
+
493
509
  Pledge.done(collection.insertOne(data, {w: 1, fullResult: true}), function afterInsert(err, result) {
494
510
 
495
511
  // Clear the cache
@@ -541,110 +557,125 @@ Mongo.setMethod(function _create(context) {
541
557
  */
542
558
  Mongo.setMethod(function _update(context) {
543
559
 
544
- const model = context.getModel(),
545
- options = context.getSaveOptions();
560
+ const model = context.getModel();
546
561
 
547
562
  return Swift.waterfall(
548
563
  this.collection(model.table),
549
564
  collection => {
565
+ return performUpdate(collection, model, context);
566
+ }
567
+ );
568
+ });
550
569
 
551
- let key;
570
+ /**
571
+ * Perform the update
572
+ *
573
+ * @author Jelle De Loecker <jelle@elevenways.be>
574
+ * @since 1.4.0
575
+ * @version 1.4.0
576
+ *
577
+ * @return {Pledge}
578
+ */
579
+ const performUpdate = (collection, model, context) => {
552
580
 
553
- // Get the converted data
554
- const data = context.getConvertedData();
581
+ const options = context.getSaveOptions();
555
582
 
556
- // Get the id to update, it should always be inside the given data
557
- let id = data._id;
583
+ let key;
558
584
 
559
- // Clone the data object
560
- let doc = {...data};
585
+ // Get the converted data
586
+ const data = context.getConvertedData();
561
587
 
562
- // Values that will get flattened
563
- let to_flatten = {};
588
+ // Get the id to update, it should always be inside the given data
589
+ let id = data._id;
564
590
 
565
- // Field names that won't get flattened
566
- let no_flatten = {};
591
+ // Clone the data object
592
+ let doc = {...data};
567
593
 
568
- // Remove the id
569
- delete doc._id;
594
+ // Values that will get flattened
595
+ let to_flatten = {};
570
596
 
571
- if (!options.override_created) {
572
- delete doc.created;
573
- }
597
+ // Field names that won't get flattened
598
+ let no_flatten = {};
574
599
 
575
- // Iterate over the fields
576
- for (key in doc) {
577
- let field = model.getField(key);
600
+ // Remove the id
601
+ delete doc._id;
578
602
 
579
- if (field && (field.is_self_contained || field.is_translatable)) {
580
- no_flatten[key] = doc[key];
581
- } else {
582
- to_flatten[key] = doc[key];
583
- }
584
- }
603
+ if (!options.override_created) {
604
+ delete doc.created;
605
+ }
585
606
 
586
- // Flatten the object, using periods & NOT flattening arrays
587
- let flat = Object.flatten(to_flatten, '.', false);
607
+ // Iterate over the fields
608
+ for (key in doc) {
609
+ let field = model.getField(key);
588
610
 
589
- // Assign the no-flatten values, too
590
- Object.assign(flat, no_flatten);
611
+ if (field && (field.is_self_contained || field.is_translatable || typeof doc[key] == 'object')) {
612
+ no_flatten[key] = doc[key];
613
+ } else {
614
+ to_flatten[key] = doc[key];
615
+ }
616
+ }
591
617
 
592
- let unset = {};
618
+ // Flatten the object, using periods & NOT flattening arrays
619
+ let flat = Object.flatten(to_flatten, '.', false);
593
620
 
594
- for (key in flat) {
595
- // Undefined or null means we want to delete the value
596
- // We can't set null, because that could interfere with dot notation updates
597
- if (flat[key] == null) {
621
+ // Assign the no-flatten values, too
622
+ Object.assign(flat, no_flatten);
598
623
 
599
- // Add the key to the unset object
600
- unset[key] = '';
624
+ let unset = {};
601
625
 
602
- // Remove it from the flat object
603
- delete flat[key];
604
- }
605
- }
626
+ for (key in flat) {
627
+ // Undefined or null means we want to delete the value
628
+ // We can't set null, because that could interfere with dot notation updates
629
+ if (flat[key] == null) {
606
630
 
607
- let update_object = {
608
- $set: flat
609
- };
631
+ // Add the key to the unset object
632
+ unset[key] = '';
610
633
 
611
- if (!Object.isEmpty(unset)) {
612
- update_object.$unset = unset;
613
- }
634
+ // Remove it from the flat object
635
+ delete flat[key];
636
+ }
637
+ }
614
638
 
615
- if (options.debug) {
616
- console.log('Updating with obj', id, update_object);
617
- }
639
+ let update_object = {
640
+ $set: flat
641
+ };
618
642
 
619
- let promise;
643
+ if (!Object.isEmpty(unset)) {
644
+ update_object.$unset = unset;
645
+ }
620
646
 
621
- if (collection.findOneAndUpdate) {
622
- promise = collection.findOneAndUpdate({_id: id}, update_object, {upsert: true});
623
- } else if (collection.findAndModify) {
624
- promise = collection.findAndModify({_id: id}, [['_id', 1]], update_object, {upsert: true});
625
- } else {
626
- // If it's not available (like nedb)
627
- promise = collection.update({_id: ''+id}, update_object, {upsert: true});
628
- }
647
+ if (options.debug) {
648
+ console.log('Updating with obj', id, update_object);
649
+ }
629
650
 
630
- let pledge = new Swift();
651
+ let promise;
631
652
 
632
- Pledge.done(promise, function afterUpdate(err, result) {
653
+ if (collection.findOneAndUpdate) {
654
+ promise = collection.findOneAndUpdate({_id: id}, update_object, {upsert: true});
655
+ } else if (collection.findAndModify) {
656
+ promise = collection.findAndModify({_id: id}, [['_id', 1]], update_object, {upsert: true});
657
+ } else {
658
+ // If it's not available (like nedb)
659
+ promise = collection.update({_id: ''+id}, update_object, {upsert: true});
660
+ }
633
661
 
634
- // Clear the cache
635
- model.nukeCache();
662
+ let pledge = new Swift();
636
663
 
637
- if (err != null) {
638
- return pledge.reject(err);
639
- }
664
+ Pledge.done(promise, function afterUpdate(err, result) {
640
665
 
641
- pledge.resolve(Object.assign({}, data));
642
- });
666
+ // Clear the cache
667
+ model.nukeCache();
643
668
 
644
- return pledge;
669
+ if (err != null) {
670
+ return pledge.reject(err);
645
671
  }
646
- );
647
- });
672
+
673
+ pledge.resolve(Object.assign({}, data));
674
+ });
675
+
676
+ return pledge;
677
+
678
+ };
648
679
 
649
680
  /**
650
681
  * Remove a record from the database
@@ -1745,12 +1745,43 @@ Alchemy.setMethod(function addAppcacheEntry(entry) {
1745
1745
  ac_entries[entry.type].push(entry);
1746
1746
  });
1747
1747
 
1748
+ /**
1749
+ * Get a size limit for the given route
1750
+ *
1751
+ * @author Jelle De Loecker <jelle@elevenways.be>
1752
+ * @since 1.4.0
1753
+ * @version 1.4.0
1754
+ *
1755
+ * @param {Route?} route
1756
+ * @param {string} name
1757
+ */
1758
+ function getRouteSizeLimit(route, name) {
1759
+
1760
+ let global_size = alchemy.settings.network[name];
1761
+
1762
+ if (!route) {
1763
+ return global_size;
1764
+ }
1765
+
1766
+ let route_value = route.options[name];
1767
+
1768
+ if (route_value == null || typeof route_value != 'number') {
1769
+ return global_size;
1770
+ }
1771
+
1772
+ if (route_value <= 0) {
1773
+ return Infinity;
1774
+ }
1775
+
1776
+ return route_value;
1777
+ }
1778
+
1748
1779
  /**
1749
1780
  * Get the body of an IncomingMessage
1750
1781
  *
1751
1782
  * @author Jelle De Loecker <jelle@elevenways.be>
1752
1783
  * @since 1.1.0
1753
- * @version 1.3.18
1784
+ * @version 1.4.0
1754
1785
  *
1755
1786
  * @param {IncomingMessage} req
1756
1787
  * @param {OutgoingMessage} res Optional
@@ -1777,24 +1808,44 @@ Alchemy.setMethod(function parseRequestBody(req, res, callback) {
1777
1808
 
1778
1809
  let content_type = req.headers['content-type'];
1779
1810
 
1811
+ let request_body_size_limit = getRouteSizeLimit(conduit?.route, 'request_body_size_limit');
1812
+
1780
1813
  // Multipart data is handled by "formidable"
1781
1814
  if (content_type && content_type.startsWith('multipart/form-data')) {
1782
1815
 
1816
+ let request_individual_file_size_limit = getRouteSizeLimit(conduit?.route, 'request_individual_file_size_limit'),
1817
+ request_total_file_size_limit = getRouteSizeLimit(conduit?.route, 'request_total_file_size_limit');
1818
+
1783
1819
  let form = new this.formidable.IncomingForm({
1784
1820
  multiples : true,
1785
1821
  hashAlgorithm : this.settings.data_management.file_hash_algorithm || 'sha1',
1822
+ minFileSize : 0,
1823
+ allowEmptyFiles : true,
1824
+ maxFileSize : request_individual_file_size_limit,
1825
+ maxFieldsSize : request_body_size_limit,
1826
+ maxTotalFileSize : request_total_file_size_limit,
1786
1827
  });
1787
1828
 
1788
1829
  form.parse(req, function parsedMultipart(err, form_fields, form_files) {
1789
1830
 
1790
- var fields = {},
1791
- files = {},
1792
- key;
1831
+ if (err) {
1832
+
1833
+ // Ignore the error if the request was already aborted
1834
+ if (conduit?.aborted) {
1835
+ return callback(null);
1836
+ }
1793
1837
 
1794
- if (err && req.conduit && req.conduit.aborted) {
1795
- return callback(null);
1838
+ if (conduit) {
1839
+ return conduit.error(err);
1840
+ }
1841
+
1842
+ return callback(err);
1796
1843
  }
1797
1844
 
1845
+ let fields = {},
1846
+ files = {},
1847
+ key;
1848
+
1798
1849
  // Since formidable v3, all the fields are now arrays.
1799
1850
  // We already had a lot of logic to deal with this,
1800
1851
  // so we just have to un-array everything
@@ -1811,18 +1862,12 @@ Alchemy.setMethod(function parseRequestBody(req, res, callback) {
1811
1862
  Object.setFormPath(files, key, form_files[key]);
1812
1863
  }
1813
1864
 
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;
1865
+ req.body = fields;
1866
+ req.files = files;
1821
1867
 
1822
- if (conduit) {
1823
- conduit.setRequestBody(fields);
1824
- conduit.setRequestFiles(files);
1825
- }
1868
+ if (conduit) {
1869
+ conduit.setRequestBody(fields);
1870
+ conduit.setRequestFiles(files);
1826
1871
  }
1827
1872
 
1828
1873
  callback(null, fields);
@@ -1834,24 +1879,29 @@ Alchemy.setMethod(function parseRequestBody(req, res, callback) {
1834
1879
  // Regular form-encoded data
1835
1880
  if (content_type && content_type.indexOf('form-urlencoded') > -1) {
1836
1881
 
1837
- this.url_form_body(req, res, function parsedBody(err) {
1882
+ this.url_form_body(req, res, {limit: request_body_size_limit}, function parsedBody(err) {
1883
+
1884
+ if (err) {
1885
+
1886
+ // Ignore the error if the request was already aborted
1887
+ if (conduit?.aborted) {
1888
+ return callback(null);
1889
+ }
1890
+
1891
+ if (conduit) {
1892
+ return conduit.error(err);
1893
+ }
1838
1894
 
1839
- if (err && req.conduit && req.conduit.aborted) {
1840
- return callback(null);
1895
+ return callback(err);
1841
1896
  }
1842
1897
 
1843
1898
  // You can't send files using a regular post
1844
1899
  req.files = {};
1845
1900
 
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;
1901
+ req.body = req.body;
1851
1902
 
1852
- if (conduit) {
1853
- conduit.setRequestBody(req.body);
1854
- }
1903
+ if (conduit) {
1904
+ conduit.setRequestBody(req.body);
1855
1905
  }
1856
1906
 
1857
1907
  callback(null, req.body);
@@ -1861,25 +1911,30 @@ Alchemy.setMethod(function parseRequestBody(req, res, callback) {
1861
1911
  }
1862
1912
 
1863
1913
  // Any other encoded data (like JSON)
1864
- this.any_body(req, function parsedBody(err, body) {
1914
+ this.any_body(req, res, {limit: request_body_size_limit}, function parsedBody(err, body) {
1865
1915
 
1866
1916
  function handleResponse(err, body) {
1867
- if (err && req.conduit && req.conduit.aborted) {
1868
- return callback(null);
1917
+
1918
+ if (err) {
1919
+
1920
+ // Ignore the error if the request was already aborted
1921
+ if (conduit?.aborted) {
1922
+ return callback(null);
1923
+ }
1924
+
1925
+ if (conduit) {
1926
+ return conduit.error(err);
1927
+ }
1928
+
1929
+ return callback(err);
1869
1930
  }
1870
-
1931
+
1871
1932
  // You can't send files using a regular post
1872
1933
  req.files = {};
1934
+ req.body = body;
1873
1935
 
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
- }
1936
+ if (conduit) {
1937
+ conduit.setRequestBody(body);
1883
1938
  }
1884
1939
 
1885
1940
  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
@@ -477,6 +477,11 @@ Group.setStatic(function unDry(obj, cloned) {
477
477
  child;
478
478
 
479
479
  for (child of children) {
480
+
481
+ if (!child) {
482
+ continue;
483
+ }
484
+
480
485
  result.children.set(child.name, child);
481
486
  child.group = result;
482
487
  }
@@ -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.5",
4
+ "version": "1.4.0-alpha.7",
5
5
  "author": "Jelle De Loecker <jelle@elevenways.be>",
6
6
  "keywords": [
7
7
  "alchemy",