alchemymvc 1.4.4 → 1.4.5
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.
- package/lib/app/behaviour/revision_behaviour.js +15 -5
- package/lib/app/helper_datasource/05-fallback_datasource.js +31 -4
- package/lib/app/helper_model/document.js +47 -1
- package/lib/app/helper_model/model.js +0 -14
- package/lib/class/model.js +7 -2
- package/lib/core/client_base.js +7 -0
- package/lib/stages/50-routes.js +11 -1
- package/lib/testing/browser.js +74 -1
- package/package.json +4 -4
|
@@ -285,6 +285,16 @@ Revision.setMethod(function afterSave(record, options, created) {
|
|
|
285
285
|
// Find the complete saved item
|
|
286
286
|
Model.get(this.model.model_name).findByPk(doc.$pk, async function gotRecord(err, result) {
|
|
287
287
|
|
|
288
|
+
try {
|
|
289
|
+
await createRevision(result);
|
|
290
|
+
pledge.resolve();
|
|
291
|
+
} catch (err) {
|
|
292
|
+
pledge.reject(err);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
async function createRevision(result) {
|
|
297
|
+
|
|
288
298
|
if (result) {
|
|
289
299
|
|
|
290
300
|
// Get the new data
|
|
@@ -320,13 +330,13 @@ Revision.setMethod(function afterSave(record, options, created) {
|
|
|
320
330
|
[that.revision_model.model_name] : revision_data
|
|
321
331
|
};
|
|
322
332
|
|
|
323
|
-
// Save the
|
|
324
|
-
|
|
333
|
+
// Save the revision. This IS awaited: a `revert()` right after
|
|
334
|
+
// a save has to see this revision, and a fire-and-forget save
|
|
335
|
+
// would also swallow any insert error.
|
|
336
|
+
await that.revision_model.save(revision_data, {allowFields: true});
|
|
325
337
|
}
|
|
326
338
|
}
|
|
327
|
-
|
|
328
|
-
pledge.resolve();
|
|
329
|
-
});
|
|
339
|
+
}
|
|
330
340
|
|
|
331
341
|
return pledge;
|
|
332
342
|
});
|
|
@@ -165,7 +165,7 @@ Fallback.setMethod(function storeInUpperDatasource(model, data, options) {
|
|
|
165
165
|
*
|
|
166
166
|
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
167
167
|
* @since 1.1.0
|
|
168
|
-
* @version 1.4.
|
|
168
|
+
* @version 1.4.5
|
|
169
169
|
*
|
|
170
170
|
* @param {Alchemy.OperationalContext.ReadDocumentFromDatasource} context
|
|
171
171
|
*
|
|
@@ -183,7 +183,28 @@ Fallback.setMethod(function read(context) {
|
|
|
183
183
|
let lower_context = context.createChild();
|
|
184
184
|
lower_context.setCriteria(lower_criteria);
|
|
185
185
|
|
|
186
|
-
|
|
186
|
+
// A rejected lower (remote) read must not reject the whole query:
|
|
187
|
+
// resolve to null instead, so the waterfall falls through to the upper
|
|
188
|
+
// (local cache) read. Without this, being offline made every find fail
|
|
189
|
+
// even though the cache had the data.
|
|
190
|
+
tasks.push(() => {
|
|
191
|
+
let attempt = new Swift();
|
|
192
|
+
|
|
193
|
+
Swift.done(this.lower.read(lower_context), (err, result) => {
|
|
194
|
+
|
|
195
|
+
if (err) {
|
|
196
|
+
if (Blast.isBrowser && typeof alchemy != 'undefined' && alchemy.distinctProblem) {
|
|
197
|
+
alchemy.distinctProblem('fallback-lower-read', 'Remote read failed, using local cache', {repeat_after: 60000});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return attempt.resolve(null);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
attempt.resolve(result);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
return attempt;
|
|
207
|
+
});
|
|
187
208
|
}
|
|
188
209
|
|
|
189
210
|
let upper_criteria = criteria.clone();
|
|
@@ -202,7 +223,7 @@ Fallback.setMethod(function read(context) {
|
|
|
202
223
|
*
|
|
203
224
|
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
204
225
|
* @since 1.1.0
|
|
205
|
-
* @version 1.4.
|
|
226
|
+
* @version 1.4.5
|
|
206
227
|
*
|
|
207
228
|
* @param {Model} model
|
|
208
229
|
*
|
|
@@ -220,7 +241,13 @@ Fallback.setMethod(function getRecordsToSync(model) {
|
|
|
220
241
|
context.setModel(model);
|
|
221
242
|
context.setCriteria(criteria);
|
|
222
243
|
|
|
223
|
-
|
|
244
|
+
// Datasource#read resolves with `{items, available}` since 1.4.2, but the
|
|
245
|
+
// consumers of this method (getRecordsToBeSavedRemotely) iterate the result
|
|
246
|
+
// directly - so unwrap to the items array, or offline saves never sync.
|
|
247
|
+
return Swift.waterfall(
|
|
248
|
+
this.upper.read(context),
|
|
249
|
+
result => (result && result.items) ? result.items : (result || [])
|
|
250
|
+
);
|
|
224
251
|
});
|
|
225
252
|
|
|
226
253
|
/**
|
|
@@ -1170,6 +1170,31 @@ Document.setMethod(function save(data, options, callback) {
|
|
|
1170
1170
|
|
|
1171
1171
|
save_result = save_result[0];
|
|
1172
1172
|
|
|
1173
|
+
// Fields edited while the save was in flight would be silently
|
|
1174
|
+
// reverted by adopting the response wholesale (the response only
|
|
1175
|
+
// reflects the record as it was sent), so detect and keep them.
|
|
1176
|
+
let kept_fields;
|
|
1177
|
+
|
|
1178
|
+
if (in_flight_snapshot && that.$main) {
|
|
1179
|
+
let key;
|
|
1180
|
+
|
|
1181
|
+
for (key in that.$main) {
|
|
1182
|
+
|
|
1183
|
+
if (key == pk_name) {
|
|
1184
|
+
continue;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
if (!that.alikeWhenStored(that.$main[key], in_flight_snapshot[key])) {
|
|
1188
|
+
|
|
1189
|
+
if (!kept_fields) {
|
|
1190
|
+
kept_fields = {};
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
kept_fields[key] = that.$main[key];
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1173
1198
|
// Use the saved data from now on
|
|
1174
1199
|
that.$main = save_result.$main;
|
|
1175
1200
|
|
|
@@ -1177,10 +1202,21 @@ Document.setMethod(function save(data, options, callback) {
|
|
|
1177
1202
|
that.$attributes.original_record = undefined;
|
|
1178
1203
|
that.markUnchanged();
|
|
1179
1204
|
|
|
1180
|
-
if (that.hasObjectFields()) {
|
|
1205
|
+
if (kept_fields || that.hasObjectFields()) {
|
|
1181
1206
|
that.storeCurrentDataAsOriginalRecord();
|
|
1182
1207
|
}
|
|
1183
1208
|
|
|
1209
|
+
if (kept_fields) {
|
|
1210
|
+
let key;
|
|
1211
|
+
|
|
1212
|
+
// Put the newer, in-flight edits back on top of the saved state
|
|
1213
|
+
// and mark them as changed, so a follow-up save persists them.
|
|
1214
|
+
for (key in kept_fields) {
|
|
1215
|
+
that.markChangedField(key, kept_fields[key]);
|
|
1216
|
+
that.$main[key] = kept_fields[key];
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1184
1220
|
pledge.resolve(that);
|
|
1185
1221
|
}
|
|
1186
1222
|
|
|
@@ -1218,6 +1254,16 @@ Document.setMethod(function save(data, options, callback) {
|
|
|
1218
1254
|
}
|
|
1219
1255
|
}
|
|
1220
1256
|
|
|
1257
|
+
// Snapshot the record as it is being sent, so `updateDoc` can tell which
|
|
1258
|
+
// fields were edited while the save was in flight
|
|
1259
|
+
let in_flight_snapshot = null;
|
|
1260
|
+
|
|
1261
|
+
try {
|
|
1262
|
+
in_flight_snapshot = JSON.clone(main);
|
|
1263
|
+
} catch (err) {
|
|
1264
|
+
// Without a snapshot the response is simply adopted as-is
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1221
1267
|
sub_pledge = this.$model.save(this, options, updateDoc);
|
|
1222
1268
|
|
|
1223
1269
|
return pledge;
|
|
@@ -1892,20 +1892,6 @@ Model.setMethod(function saveRecord(document, options, callback) {
|
|
|
1892
1892
|
|
|
1893
1893
|
creating = options.create || document.$pk == null;
|
|
1894
1894
|
next();
|
|
1895
|
-
|
|
1896
|
-
return;
|
|
1897
|
-
|
|
1898
|
-
// Look through unique indexes if no _id is present
|
|
1899
|
-
that.auditRecord(document, options, function afterAudit(err, doc) {
|
|
1900
|
-
|
|
1901
|
-
if (err) {
|
|
1902
|
-
return next(err);
|
|
1903
|
-
}
|
|
1904
|
-
|
|
1905
|
-
// Is a new record being created?
|
|
1906
|
-
creating = options.create || doc.$pk == null;
|
|
1907
|
-
next();
|
|
1908
|
-
});
|
|
1909
1895
|
}, function doBeforeNormalize(next) {
|
|
1910
1896
|
// @TODO: make "beforeSave" only use promises
|
|
1911
1897
|
that.issueDataEvent('beforeNormalize', [document, options], next);
|
package/lib/class/model.js
CHANGED
|
@@ -1402,7 +1402,7 @@ Model.setMethod(function remove(id, callback) {
|
|
|
1402
1402
|
*
|
|
1403
1403
|
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
1404
1404
|
* @since 0.5.0
|
|
1405
|
-
* @version 1.
|
|
1405
|
+
* @version 1.4.5
|
|
1406
1406
|
*
|
|
1407
1407
|
* @param {Object} options Find options
|
|
1408
1408
|
* @param {Function} task Task to perform on each record
|
|
@@ -1425,7 +1425,12 @@ Model.setMethod(function eachRecord(options, task, callback) {
|
|
|
1425
1425
|
options = {};
|
|
1426
1426
|
}
|
|
1427
1427
|
|
|
1428
|
-
if (
|
|
1428
|
+
if (callback) {
|
|
1429
|
+
// Errors are also delivered through the callback, and callers that
|
|
1430
|
+
// pass one routinely discard the returned pledge - its rejection
|
|
1431
|
+
// must not be reported as an uncaught error on top of the callback
|
|
1432
|
+
pledge.warn_uncaught_errors = false;
|
|
1433
|
+
} else {
|
|
1429
1434
|
callback = Function.thrower;
|
|
1430
1435
|
}
|
|
1431
1436
|
|
package/lib/core/client_base.js
CHANGED
|
@@ -291,6 +291,13 @@ ClientBase.setMethod(function issueEvent(name, args, next) {
|
|
|
291
291
|
args = [];
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
+
if (next) {
|
|
295
|
+
// Errors are also delivered through `next`, and callers that pass a
|
|
296
|
+
// callback routinely discard the returned pledge - its rejection
|
|
297
|
+
// must not be reported as an uncaught error on top of the callback.
|
|
298
|
+
pledge.warn_uncaught_errors = false;
|
|
299
|
+
}
|
|
300
|
+
|
|
294
301
|
if (this.constructor.event_to_method_map) {
|
|
295
302
|
method_name = this.constructor.event_to_method_map.get(name);
|
|
296
303
|
}
|
package/lib/stages/50-routes.js
CHANGED
|
@@ -118,7 +118,17 @@ const hawkejs = routes.createStage('hawkejs', () => {
|
|
|
118
118
|
return req.conduit.notFound('Could not find any of the given templates');
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
|
|
121
|
+
// Only allow long-lived caching when the request is versioned
|
|
122
|
+
// with the CURRENT app version: unversioned (or stale-versioned)
|
|
123
|
+
// urls would keep serving an old template for up to an hour
|
|
124
|
+
// after a deploy.
|
|
125
|
+
let requested_version = req.conduit.param('v');
|
|
126
|
+
|
|
127
|
+
if (requested_version && String(requested_version) === String(alchemy.hawkejs.app_version)) {
|
|
128
|
+
req.conduit.setHeader('cache-control', 'public, max-age=3600, must-revalidate');
|
|
129
|
+
} else {
|
|
130
|
+
req.conduit.setHeader('cache-control', 'no-cache');
|
|
131
|
+
}
|
|
122
132
|
|
|
123
133
|
// Don't use json dry, hawkejs expects regular json
|
|
124
134
|
req.conduit.json_dry = false;
|
package/lib/testing/browser.js
CHANGED
|
@@ -24,6 +24,78 @@
|
|
|
24
24
|
*/
|
|
25
25
|
'use strict';
|
|
26
26
|
|
|
27
|
+
const fs = require('fs'),
|
|
28
|
+
child_process = require('child_process');
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Check if a browser executable actually works in headless mode.
|
|
32
|
+
* Puppeteer's own bundled-Chrome download can be missing/corrupt in some
|
|
33
|
+
* sandboxes (e.g. a pinned old revision missing its ICU data file). Such a
|
|
34
|
+
* binary still exits 0 for `--version`, but hangs and never emits a CDP
|
|
35
|
+
* handshake when actually launched headless - which is what makes
|
|
36
|
+
* `puppeteer.launch()` hang until its connect-timeout instead of failing
|
|
37
|
+
* fast. So a real headless run is checked synchronously up front instead.
|
|
38
|
+
*
|
|
39
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
40
|
+
* @since 1.4.5
|
|
41
|
+
* @version 1.4.5
|
|
42
|
+
*
|
|
43
|
+
* @param {string} executable_path
|
|
44
|
+
*
|
|
45
|
+
* @return {boolean}
|
|
46
|
+
*/
|
|
47
|
+
function isUsableBrowser(executable_path) {
|
|
48
|
+
|
|
49
|
+
if (!executable_path || !fs.existsSync(executable_path)) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let result = child_process.spawnSync(executable_path, [
|
|
54
|
+
'--headless',
|
|
55
|
+
'--disable-gpu',
|
|
56
|
+
'--no-sandbox',
|
|
57
|
+
'--dump-dom',
|
|
58
|
+
'about:blank',
|
|
59
|
+
], {timeout: 8000});
|
|
60
|
+
|
|
61
|
+
return result.status === 0 && !result.error;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Resolve which Chrome/Chromium executable to launch.
|
|
66
|
+
* Prefers `PUPPETEER_EXECUTABLE_PATH` (puppeteer already honors it, but we
|
|
67
|
+
* validate it here too) and puppeteer's own bundled download; falls back to
|
|
68
|
+
* common system-installed locations when neither actually works.
|
|
69
|
+
*
|
|
70
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
71
|
+
* @since 1.4.5
|
|
72
|
+
* @version 1.4.5
|
|
73
|
+
*
|
|
74
|
+
* @param {Object} puppeteer
|
|
75
|
+
*
|
|
76
|
+
* @return {string|undefined}
|
|
77
|
+
*/
|
|
78
|
+
function resolveExecutablePath(puppeteer) {
|
|
79
|
+
|
|
80
|
+
let candidates = [
|
|
81
|
+
process.env.PUPPETEER_EXECUTABLE_PATH,
|
|
82
|
+
puppeteer.executablePath(),
|
|
83
|
+
'/usr/bin/chromium',
|
|
84
|
+
'/usr/bin/chromium-browser',
|
|
85
|
+
'/usr/bin/google-chrome',
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
for (let candidate of candidates) {
|
|
89
|
+
if (isUsableBrowser(candidate)) {
|
|
90
|
+
return candidate;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Nothing usable found: let puppeteer try its own default and produce
|
|
95
|
+
// its normal error/timeout, rather than silently launching with none
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
27
99
|
/**
|
|
28
100
|
* The BrowserHelper class
|
|
29
101
|
*
|
|
@@ -133,7 +205,8 @@ BrowserHelper.setMethod(async function load() {
|
|
|
133
205
|
} else {
|
|
134
206
|
// Launch new browser
|
|
135
207
|
this.browser = await puppeteer.launch({
|
|
136
|
-
headless: this.options.headless,
|
|
208
|
+
headless : this.options.headless,
|
|
209
|
+
executablePath : resolveExecutablePath(puppeteer),
|
|
137
210
|
});
|
|
138
211
|
}
|
|
139
212
|
|
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.
|
|
4
|
+
"version": "1.4.5",
|
|
5
5
|
"author": "Jelle De Loecker <jelle@elevenways.be>",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"alchemy",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"chokidar" : "~4.0.3",
|
|
23
23
|
"formidable" : "~3.5.4",
|
|
24
24
|
"graceful-fs" : "~4.2.11",
|
|
25
|
-
"hawkejs" : "~2.4.
|
|
25
|
+
"hawkejs" : "~2.4.3",
|
|
26
26
|
"jsondiffpatch" : "~0.5.0",
|
|
27
27
|
"mime-types" : "~3.0.2",
|
|
28
28
|
"minimist" : "~1.2.8",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"ncp" : "~2.0.0",
|
|
33
33
|
"postcss" : "~8.5.6",
|
|
34
34
|
"postcss-prune-var": "~1.1.2",
|
|
35
|
-
"protoblast" : "~0.9.
|
|
35
|
+
"protoblast" : "~0.9.9",
|
|
36
36
|
"semver" : "~7.7.2",
|
|
37
37
|
"socket.io" : "~4.7.5",
|
|
38
38
|
"@11ways/socket.io-stream" : "~0.9.2",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"mocha" : "^11.7.5",
|
|
67
67
|
"mongo-unit" : "^3.4.0",
|
|
68
68
|
"nyc" : "^15.1.0",
|
|
69
|
-
"puppeteer" : "
|
|
69
|
+
"puppeteer" : "^24.43.1",
|
|
70
70
|
"source-map" : "~0.7.3"
|
|
71
71
|
},
|
|
72
72
|
"scripts": {
|