alchemymvc 1.3.20 → 1.3.21

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.
@@ -30,7 +30,7 @@ Alchemy.setStatic(function onScene(scene, options) {
30
30
  *
31
31
  * @author Jelle De Loecker <jelle@elevenways.be>
32
32
  * @since 0.0.1
33
- * @version 1.2.7
33
+ * @version 1.3.21
34
34
  *
35
35
  * @param {String|Object} options
36
36
  * @param {Object} data
@@ -122,10 +122,14 @@ Alchemy.setMethod(function getResource(options, data, callback) {
122
122
 
123
123
  config = this.view.helpers.Router.routeConfig(options.name);
124
124
 
125
- if (config && config.methods) {
126
- if (config.methods.indexOf('get') == -1) {
125
+ if (config) {
126
+ if (config.methods && config.methods.indexOf('get') == -1) {
127
127
  method = config.methods[0];
128
128
  }
129
+
130
+ if (config.cache != null && options.cache == null) {
131
+ options.cache = config.cache;
132
+ }
129
133
  }
130
134
 
131
135
  // Get the url to the resource
@@ -150,7 +154,7 @@ Alchemy.setMethod(function getResource(options, data, callback) {
150
154
  options[method] = true;
151
155
  }
152
156
 
153
- pledge = hawkejs.scene.fetch(options);
157
+ pledge = this.view.fetch(options);
154
158
  pledge.done(callback);
155
159
 
156
160
  return pledge;
@@ -359,6 +359,7 @@ Controller.setMethod(async function doAction(name, args) {
359
359
  if (route) {
360
360
 
361
361
  if (route.options?.title) {
362
+ // @TODO: Add support for objects with language keys
362
363
  let title = route.options.title;
363
364
 
364
365
  if (alchemy.settings && alchemy.settings.title_suffix) {
@@ -50,7 +50,7 @@ BigIntField.setMethod(function cast(value) {
50
50
  *
51
51
  * @author Jelle De Loecker <jelle@elevenways.be>
52
52
  * @since 1.3.6
53
- * @version 1.3.20
53
+ * @version 1.3.21
54
54
  *
55
55
  * @param {Mixed} value
56
56
  * @param {Array} field_paths The path to the field
@@ -60,7 +60,7 @@ BigIntField.setMethod(function cast(value) {
60
60
  BigIntField.setMethod(function _castCondition(value, field_paths) {
61
61
 
62
62
  value = this.cast(value);
63
- value = this.convertBigIntForDatasource(value);
63
+ value = this.datasource.convertBigIntForDatasource(value);
64
64
 
65
65
  return value;
66
66
  });
@@ -0,0 +1,73 @@
1
+ /**
2
+ * The Mixed Field class
3
+ *
4
+ * @constructor
5
+ *
6
+ * @author Jelle De Loecker <jelle@elevenways.be>
7
+ * @since 1.3.21
8
+ * @version 1.3.21
9
+ */
10
+ const MixedField = Function.inherits('Alchemy.Field', 'Mixed');
11
+
12
+ /**
13
+ * Set the datatype name
14
+ *
15
+ * @author Jelle De Loecker <jelle@elevenways.be>
16
+ * @since 1.3.21
17
+ * @version 1.3.21
18
+ */
19
+ MixedField.setDatatype('object');
20
+
21
+ /**
22
+ * This field value is self-contained
23
+ *
24
+ * @author Jelle De Loecker <jelle@elevenways.be>
25
+ * @since 1.3.21
26
+ * @version 1.3.21
27
+ */
28
+ MixedField.setSelfContained(true);
29
+
30
+ /**
31
+ * Store objects as strings, if wanted
32
+ *
33
+ * @author Jelle De Loecker <jelle@elevenways.be>
34
+ * @since 1.3.21
35
+ * @version 1.3.21
36
+ *
37
+ * @param {Mixed} value The field's own value
38
+ * @param {Object} data The main record
39
+ * @param {Datasource} datasource The datasource instance
40
+ *
41
+ * @return {Mixed}
42
+ */
43
+ MixedField.setMethod(function _toDatasource(value, data, datasource, callback) {
44
+
45
+ if (value && typeof value == 'object' && !Object.isPlainObject(value)) {
46
+ if (!(value instanceof Date)) {
47
+ value = JSON.toDryObject(value);
48
+ }
49
+ }
50
+
51
+ Blast.nextTick(callback, null, null, value);
52
+ });
53
+
54
+ /**
55
+ * Convert from database to app
56
+ *
57
+ * @author Jelle De Loecker <jelle@elevenways.be>
58
+ * @since 1.3.21
59
+ * @version 1.3.21
60
+ *
61
+ * @param {Object} query The original query
62
+ * @param {Object} options The original query options
63
+ * @param {Mixed} value The field value, as stored in the DB
64
+ * @param {Function} callback
65
+ */
66
+ MixedField.setMethod(function _toApp(query, options, value, callback) {
67
+
68
+ if (value && typeof value == 'object' && typeof value.dry == 'string') {
69
+ value = JSON.unDry(value);
70
+ }
71
+
72
+ callback(null, value);
73
+ });
@@ -110,7 +110,7 @@ SchemaField.setProperty(function requires_translating() {
110
110
  *
111
111
  * @author Jelle De Loecker <jelle@elevenways.be>
112
112
  * @since 0.2.0
113
- * @version 1.3.7
113
+ * @version 1.3.21
114
114
  *
115
115
  * @param {Object} record This *should* be the schema context (might not be the root)
116
116
  * @param {String} some_path Some path to a field in the wanted schema
@@ -124,56 +124,131 @@ SchemaField.setMethod(function getSubschema(record, some_path) {
124
124
  // If schema is a string,
125
125
  // it needs to be extracted from another field's value
126
126
  if (typeof schema == 'string') {
127
+ schema = this.resolveSchemaPath(this.schema, record, some_path, schema);
128
+ }
127
129
 
128
- // When there are 2 pieces, the second piece is the property name
129
- let pieces = schema.split('.');
130
+ return schema;
131
+ });
130
132
 
131
- // The first piece is the external field name
132
- let external_field_name = pieces[0];
133
+ /**
134
+ * Get the subschema of a given schema
135
+ *
136
+ * @author Jelle De Loecker <jelle@elevenways.be>
137
+ * @since 1.3.21
138
+ * @version 1.3.21
139
+ *
140
+ * @param {Schema} context_schema The schema context (probably the schema this field is in)
141
+ * @param {Object} record This *should* be the schema context (might not be the root)
142
+ * @param {String} some_path Some path to a field in the wanted schema
143
+ * @param {String} schema The schema path to resolve
144
+ *
145
+ * @return {Schema}
146
+ */
147
+ SchemaField.setMethod(function resolveSchemaPath(context_schema, record, field_path, schema_path) {
133
148
 
134
- // The second piece is the property name
135
- let property_name = pieces[1] || 'schema';
149
+ if (typeof schema_path != 'string') {
150
+ return schema_path;
151
+ }
136
152
 
137
- // Get that other field by its name
138
- let field = this.schema.getField(external_field_name);
153
+ let schema;
139
154
 
140
- if (!field) {
141
- console.error('Failed to get subschema', external_field_name, 'of', this.schema, some_path);
142
- return null;
143
- }
155
+ // When there are 2 pieces, the second piece is the property name
156
+ let pieces = schema_path.split('.');
144
157
 
145
- // Get the values that field can have (probably an enum)
146
- let values = field.getValues();
158
+ // The first piece is the external field name
159
+ let external_field_name = pieces.shift();
147
160
 
148
- if (values == null) {
149
- return null;
150
- }
161
+ // The rest of the pieces is the property path
162
+ let property_name = pieces.join('.') || 'schema';
151
163
 
152
- // Now get the actual external value from the record
153
- let record_value = field.getRecordValue(record);
164
+ // Get that other field by its name
165
+ let field = context_schema.getField(external_field_name);
154
166
 
155
- // I'm not sure if this will help
156
- if (record_value == null) {
157
- record_value = field.getRecordValue(record, some_path);
158
- }
167
+ if (!field) {
168
+ let association = context_schema.getAssociation(external_field_name);
169
+ return this.getSubschemaFromAssociation(record, property_name, association);
170
+ }
159
171
 
160
- // Get the correct field value
161
- let enum_value = values.get(record_value);
172
+ if (!field) {
173
+ console.error('Failed to get subschema', external_field_name, 'of', context_schema, field_path);
174
+ return null;
175
+ }
162
176
 
163
- if (!enum_value) {
164
- schema = null;
165
- } else if (enum_value.schema) {
166
- schema = enum_value.schema;
167
- } else if (enum_value.value) {
168
- schema = enum_value.value[property_name] || enum_value.value.schema;
169
- } else {
170
- console.log('Could not find', schema, 'in', record, 'enum values:', enum_value, 'of field', field)
171
- }
177
+ // Get the values that field can have (probably an enum)
178
+ let values = field.getValues();
179
+
180
+ if (values == null) {
181
+ return null;
182
+ }
183
+
184
+ // Now get the actual external value from the record
185
+ let record_value = field.getRecordValue(record);
186
+
187
+ // I'm not sure if this will help
188
+ if (record_value == null) {
189
+ record_value = field.getRecordValue(record, field_path);
190
+ }
191
+
192
+ // Get the correct field value
193
+ let enum_value = values.get(record_value);
194
+
195
+ if (!enum_value) {
196
+ schema = null;
197
+ } else if (enum_value.schema) {
198
+ schema = enum_value.schema;
199
+ } else if (enum_value.value) {
200
+ schema = enum_value.value[property_name] || enum_value.value.schema;
201
+ } else {
202
+ console.log('Could not find', schema_path, 'in', record, 'enum values:', enum_value, 'of field', field)
172
203
  }
173
204
 
174
205
  return schema;
175
206
  });
176
207
 
208
+ /**
209
+ * Get the subschema via an association
210
+ * @TODO: Work in progress!
211
+ *
212
+ * @author Jelle De Loecker <jelle@elevenways.be>
213
+ * @since 0.2.0
214
+ * @version 1.3.21
215
+ *
216
+ * @param {Object} record This *should* be the schema context (might not be the root)
217
+ * @param {String} path The path inside the associated schema
218
+ * @param {Object} association The association object
219
+ *
220
+ * @return {Schema} Response might be a promise
221
+ */
222
+ SchemaField.setMethod(function getSubschemaFromAssociation(record, path, association) {
223
+
224
+ let local_value = record[association.options.local_key];
225
+
226
+ if (!local_value) {
227
+ return false;
228
+ }
229
+
230
+ const associated_model = alchemy.getModel(association.model_name);
231
+
232
+ if (!associated_model) {
233
+ return false;
234
+ }
235
+
236
+ return Pledge.Swift.execute(async () => {
237
+
238
+ let result = await associated_model.findByValues({
239
+ [association.options.foreign_key]: local_value,
240
+ });
241
+
242
+ if (!result) {
243
+ return null;
244
+ }
245
+
246
+ let found_schema = this.resolveSchemaPath(associated_model.schema, result, path, path);
247
+
248
+ return found_schema;
249
+ });
250
+ });
251
+
177
252
  /**
178
253
  * Cast all the subschema values using their _toDatasource method
179
254
  *
@@ -1116,6 +1116,19 @@ Criteria.setMethod(function and(value) {
1116
1116
  return this._applyGroup('and');
1117
1117
  });
1118
1118
 
1119
+ /**
1120
+ * Alias for `ne`: Not equals check
1121
+ *
1122
+ * @author Jelle De Loecker <jelle@elevenways.be>
1123
+ * @since 1.3.21
1124
+ * @version 1.3.2
1125
+ *
1126
+ * @param {*} value
1127
+ */
1128
+ Criteria.setMethod(function notEquals(value) {
1129
+ return this.ne(value);
1130
+ });
1131
+
1119
1132
  Criteria.addValueExpression('equals');
1120
1133
  Criteria.addValueExpression('gte');
1121
1134
  Criteria.addValueExpression('gt');
@@ -131,6 +131,32 @@ Document.setStatic(function setMethod(key, method, on_server) {
131
131
  return Blast.Collection.Function.setMethod(this, key, method);
132
132
  });
133
133
 
134
+ /**
135
+ * Set a getter for this computed field
136
+ *
137
+ * @author Jelle De Loecker <jelle@elevenways.be>
138
+ * @since 1.3.21
139
+ * @version 1.3.21
140
+ *
141
+ * @param {String} name Name of the property
142
+ * @param {Boolean} on_server Also set on the server implementation
143
+ */
144
+ Document.setStatic(function setComputedFieldGetter(name, on_server) {
145
+ this.setProperty(name, function getComputedFieldValue() {
146
+ this.recomputeFieldIfNecessary(name);
147
+ return this.$main[name];
148
+ }, function setComputedFieldValue(value) {
149
+
150
+ const field = this.$model.schema.getField(name);
151
+
152
+ if (field?.options?.allow_manual_set) {
153
+ return this.$main[name] = value;
154
+ }
155
+
156
+ console.error('Can not set computed field "' + name + '" to', value);
157
+ }, on_server);
158
+ });
159
+
134
160
  /**
135
161
  * Set a getter for this field
136
162
  *
@@ -858,6 +884,209 @@ Document.setMethod(function setValues(values) {
858
884
 
859
885
  });
860
886
 
887
+ /**
888
+ * Recompute the given field if required
889
+ *
890
+ * @author Jelle De Loecker <jelle@elevenways.be>
891
+ * @since 1.3.21
892
+ * @version 1.3.21
893
+ *
894
+ * @param {string|Alchemy.Field} name
895
+ * @param {boolean} force
896
+ *
897
+ * @return {Pledge|undefined}
898
+ */
899
+ Document.setMethod(function recomputeFieldIfNecessary(name, force = false) {
900
+
901
+ const field = this.$model.schema.getField(name),
902
+ original = this.$main[name];
903
+
904
+ if (!field) {
905
+ return original;
906
+ }
907
+
908
+ let options = field.options;
909
+
910
+ if (!options.compute_method) {
911
+ return original;
912
+ }
913
+
914
+ let required_field_count = options.required_fields?.length || 0,
915
+ optional_field_count = options.optional_fields?.length || 0;
916
+
917
+ if (!force && original == null) {
918
+ // If the value is null, it hasn't been computed yet
919
+ // and we need to compute it
920
+ force = true;
921
+ }
922
+
923
+ if (!force) {
924
+ let has_changed = false;
925
+
926
+ if (required_field_count) {
927
+ for (let required_field of options.required_fields) {
928
+ if (this.hasChanged(required_field)) {
929
+ has_changed = true;
930
+ break;
931
+ }
932
+ }
933
+ }
934
+
935
+ if (!has_changed && optional_field_count) {
936
+ for (let optional_field of options.optional_fields) {
937
+ if (this.hasChanged(optional_field)) {
938
+ has_changed = true;
939
+ break;
940
+ }
941
+ }
942
+ }
943
+
944
+ if (!has_changed) {
945
+ return original;
946
+ }
947
+ }
948
+
949
+ // Make sure the required fields are set
950
+ if (required_field_count) {
951
+ let has_value = true;
952
+
953
+ for (let required_field of options.required_fields) {
954
+ if (this[required_field] == null) {
955
+ has_value = false;
956
+ break;
957
+ }
958
+ }
959
+
960
+ if (!has_value) {
961
+ // If not all required field values are set,
962
+ // the result will also be undefined
963
+ return this._setComputedFieldValue(name, undefined);
964
+ }
965
+ }
966
+
967
+ let compute_method = options.compute_method;
968
+
969
+ if (typeof compute_method == 'string') {
970
+ let fnc = this[compute_method];
971
+
972
+ if (typeof fnc == 'function') {
973
+ compute_method = fnc;
974
+ } else {
975
+ // Handle special cases in the browser
976
+ compute_method = alchemy.getCustomHandler('recompute_field');
977
+ }
978
+ }
979
+
980
+ if (!compute_method) {
981
+ return original;
982
+ }
983
+
984
+ let result = compute_method.call(this, this, field);
985
+
986
+ return this._setComputedFieldValue(name, result);
987
+ });
988
+
989
+ /**
990
+ * Set a computed field to a specific value
991
+ *
992
+ * @author Jelle De Loecker <jelle@elevenways.be>
993
+ * @since 1.3.21
994
+ * @version 1.3.21
995
+ *
996
+ * @param {string|Alchemy.Field} name
997
+ * @param {*} value
998
+ *
999
+ * @return {Pledge|undefined}
1000
+ */
1001
+ Document.setMethod(function _setComputedFieldValue(name, value) {
1002
+
1003
+ const field = this.$model.schema.getField(name);
1004
+
1005
+ if (value == null && field?.options?.allow_manual_set) {
1006
+ return this.$main[name];
1007
+ }
1008
+
1009
+ if (Pledge.isThenable(value)) {
1010
+ value.then(value => {
1011
+ this.$main[name] = value;
1012
+ });
1013
+ } else {
1014
+ this.$main[name] = value;
1015
+ }
1016
+
1017
+ return value;
1018
+ });
1019
+
1020
+ /**
1021
+ * Recompute values of computed fields.
1022
+ * This might be async. If it is, a pledge will be returned.
1023
+ *
1024
+ * @author Jelle De Loecker <jelle@elevenways.be>
1025
+ * @since 1.3.21
1026
+ * @version 1.3.21
1027
+ *
1028
+ * @return {Pledge|undefined}
1029
+ */
1030
+ Document.setMethod(function recomputeValues() {
1031
+
1032
+ const schema = this.$model.schema;
1033
+
1034
+ if (!schema.has_computed_fields) {
1035
+ return;
1036
+ }
1037
+
1038
+ let promises = [];
1039
+
1040
+ for (let key in schema.computed_fields) {
1041
+ let field = schema.computed_fields[key];
1042
+ let options = field.options;
1043
+
1044
+ // Make sure the required fields are set
1045
+ if (options.required_fields?.length) {
1046
+ let has_value = true;
1047
+
1048
+ for (let required_field of options.required_fields) {
1049
+ if (this[required_field] == null) {
1050
+ has_value = false;
1051
+ break;
1052
+ }
1053
+ }
1054
+
1055
+ if (!has_value) {
1056
+ // If not all required field values are set,
1057
+ // the result will also be undefined
1058
+ this._setComputedFieldValue(key, undefined);
1059
+ continue;
1060
+ }
1061
+ }
1062
+
1063
+ let compute_method = options.compute_method;
1064
+
1065
+ if (typeof compute_method == 'string') {
1066
+ let fnc = this[compute_method];
1067
+
1068
+ if (typeof fnc == 'function') {
1069
+ compute_method = fnc;
1070
+ } else {
1071
+ // Handle special cases in the browser
1072
+ compute_method = alchemy.getCustomHandler('recompute_field');
1073
+ }
1074
+ }
1075
+
1076
+ if (!compute_method) {
1077
+ continue;
1078
+ }
1079
+
1080
+ let result = compute_method.call(this, this, field);
1081
+
1082
+ this._setComputedFieldValue(key, result);
1083
+ }
1084
+
1085
+ if (promises.length) {
1086
+ return Pledge.all(promises);
1087
+ }
1088
+ });
1089
+
861
1090
  /**
862
1091
  * Get the clean options
863
1092
  *
@@ -629,7 +629,7 @@ Model.setMethod(function getAliasModel(alias) {
629
629
  *
630
630
  * @author Jelle De Loecker <jelle@elevenways.be>
631
631
  * @since 0.0.1
632
- * @version 1.3.8
632
+ * @version 1.3.21
633
633
  *
634
634
  * @param {String} type The type of find (first, all)
635
635
  * @param {Criteria} criteria The criteria object
@@ -797,6 +797,27 @@ Model.setMethod(function find(type, criteria, callback) {
797
797
  next();
798
798
  }
799
799
  });
800
+ }, function recomputeAfterFind(next) {
801
+
802
+ if (!that.schema.has_recompute_after_find || criteria.options.document === false) {
803
+ return next();
804
+ }
805
+
806
+ let tasks = [];
807
+
808
+ for (let document of records) {
809
+ tasks.push((next) => {
810
+ let promise = document.recomputeValues();
811
+
812
+ if (!promise) {
813
+ return next();
814
+ }
815
+
816
+ Pledge.done(promise, next);
817
+ });
818
+ }
819
+
820
+ Function.parallel(8, tasks, next);
800
821
  }, function done(err) {
801
822
 
802
823
  var i;
@@ -1773,7 +1794,7 @@ Model.setMethod(function compose(data, options) {
1773
1794
  *
1774
1795
  * @author Jelle De Loecker <jelle@elevenways.be>
1775
1796
  * @since 0.2.0
1776
- * @version 1.3.20
1797
+ * @version 1.3.21
1777
1798
  *
1778
1799
  * @param {Document} document
1779
1800
  * @param {Object} options
@@ -1786,10 +1807,17 @@ Model.setMethod(function createRecord(document, options, callback) {
1786
1807
  // Normalize & clone the data, set default values, ...
1787
1808
  let data = this.compose(document, options);
1788
1809
 
1789
- // Turn it into a new document
1790
- document = this.createDocument(data);
1810
+ document = createDocumentForSaving(this, document, data);
1811
+
1812
+ Function.series(function recompute(next) {
1813
+ let result = document.recomputeValues();
1791
1814
 
1792
- Function.series(function doBeforeValidate(next) {
1815
+ if (result) {
1816
+ result.done(next);
1817
+ } else {
1818
+ next();
1819
+ }
1820
+ }, function doBeforeValidate(next) {
1793
1821
  that.callOrNext('beforeValidate', [document, options], next);
1794
1822
  }, function validate(next) {
1795
1823
 
@@ -1823,12 +1851,35 @@ Model.setMethod(function createRecord(document, options, callback) {
1823
1851
  })
1824
1852
  });
1825
1853
 
1854
+ /**
1855
+ * Create a document used for saving
1856
+ *
1857
+ * @author Jelle De Loecker <jelle@elevenways.be>
1858
+ * @since 1.3.21
1859
+ * @version 1.3.21
1860
+ *
1861
+ * @param {Model} model
1862
+ * @param {Document|Object} original_input
1863
+ * @param {Object} data
1864
+ */
1865
+ function createDocumentForSaving(model, original_input, data) {
1866
+
1867
+ // Turn it into a new document
1868
+ let document = model.createDocument(data);
1869
+
1870
+ if (original_input?.$attributes.original_record) {
1871
+ document.$attributes.original_record = original_input.$attributes.original_record;
1872
+ }
1873
+
1874
+ return document;
1875
+ }
1876
+
1826
1877
  /**
1827
1878
  * Update a record in the database
1828
1879
  *
1829
1880
  * @author Jelle De Loecker <jelle@elevenways.be>
1830
1881
  * @since 0.2.0
1831
- * @version 1.3.20
1882
+ * @version 1.3.21
1832
1883
  *
1833
1884
  * @param {Document} document
1834
1885
  * @param {Object} options
@@ -1842,9 +1893,17 @@ Model.setMethod(function updateRecord(document, options, callback) {
1842
1893
  let data = this.compose(document, Object.assign({update: true}, options));
1843
1894
 
1844
1895
  // Turn it into a new document
1845
- document = this.createDocument(data);
1896
+ document = createDocumentForSaving(this, document, data);
1897
+
1898
+ Function.series(function recompute(next) {
1899
+ let result = document.recomputeValues();
1846
1900
 
1847
- Function.series(function doBeforeValidate(next) {
1901
+ if (result) {
1902
+ result.done(next);
1903
+ } else {
1904
+ next();
1905
+ }
1906
+ }, function doBeforeValidate(next) {
1848
1907
  that.callOrNext('beforeValidate', [document, options], next);
1849
1908
  }, function validate(next) {
1850
1909