alchemymvc 1.4.0 → 1.4.2
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 +1 -1
- package/lib/app/behaviour/sluggable_behaviour.js +2 -2
- package/lib/app/datasource/mongo_datasource.js +19 -3
- package/lib/app/helper/cron.js +2 -2
- package/lib/app/helper_datasource/00-nosql_datasource.js +9 -3
- package/lib/app/helper_datasource/05-fallback_datasource.js +10 -13
- package/lib/app/helper_datasource/idb_datasource.js +7 -5
- package/lib/app/helper_datasource/remote_datasource.js +1 -1
- package/lib/app/helper_field/password_field.js +4 -2
- package/lib/app/helper_field/schema_field.js +3 -2
- package/lib/app/helper_field/time_field.js +1 -1
- package/lib/app/helper_model/00-base_criteria.js +14 -0
- package/lib/app/helper_model/05-criteria_expressions.js +30 -7
- package/lib/app/helper_model/10-model_criteria.js +47 -8
- package/lib/app/helper_model/document.js +11 -2
- package/lib/app/helper_model/model.js +6 -3
- package/lib/app/model/system_task_history_model.js +134 -0
- package/lib/class/conduit.js +5 -2
- package/lib/class/controller.js +1 -0
- package/lib/class/datasource.js +14 -2
- package/lib/class/document.js +40 -12
- package/lib/class/import_stream_parser.js +299 -0
- package/lib/class/inode_file.js +2 -0
- package/lib/class/migration.js +5 -2
- package/lib/class/model.js +12 -142
- package/lib/class/plugin.js +32 -3
- package/lib/class/postponement.js +1 -1
- package/lib/class/router.js +26 -28
- package/lib/class/schema_client.js +39 -8
- package/lib/class/sitemap.js +2 -2
- package/lib/class/task.js +42 -24
- package/lib/core/alchemy.js +110 -162
- package/lib/core/alchemy_load_functions.js +64 -5
- package/lib/core/base.js +2 -2
- package/lib/core/middleware.js +31 -5
- package/lib/core/prefix.js +1 -1
- package/lib/core/setting.js +12 -9
- package/lib/scripts/create_constants.js +5 -1
- package/lib/stages/00-load_core.js +8 -2
- package/lib/testing/browser.js +1164 -0
- package/lib/testing/harness.js +922 -0
- package/package.json +13 -6
- package/testing/browser.js +27 -0
- package/testing.js +37 -0
|
@@ -116,4 +116,138 @@ SystemTaskHistory.constitute(function chimeraConfig() {
|
|
|
116
116
|
route : 'Chimera.Editor#taskMonitor',
|
|
117
117
|
route_params : {task_history_id: '$pk'},
|
|
118
118
|
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Derive a single-word status from the recorded fields.
|
|
123
|
+
*
|
|
124
|
+
* - 'running': is_running is still true
|
|
125
|
+
* - 'failed': had_error is true
|
|
126
|
+
* - 'done': started AND ended cleanly
|
|
127
|
+
* - 'aborted': started but never ended (pre-1.4.2 zombies, or a process
|
|
128
|
+
* that died mid-task without unwinding the try/finally)
|
|
129
|
+
*
|
|
130
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
131
|
+
* @since 1.4.2
|
|
132
|
+
*
|
|
133
|
+
* @return {string}
|
|
134
|
+
*/
|
|
135
|
+
SystemTaskHistory.setDocumentMethod(function getStatus() {
|
|
136
|
+
|
|
137
|
+
if (this.had_error) {
|
|
138
|
+
return 'failed';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (this.is_running) {
|
|
142
|
+
return 'running';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (this.ended_at) {
|
|
146
|
+
return 'done';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (this.started_at) {
|
|
150
|
+
return 'aborted';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return 'scheduled';
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Return how long this run took (or has been running so far), in
|
|
158
|
+
* milliseconds. Null when nothing has started yet.
|
|
159
|
+
*
|
|
160
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
161
|
+
* @since 1.4.2
|
|
162
|
+
*
|
|
163
|
+
* @return {number|null}
|
|
164
|
+
*/
|
|
165
|
+
SystemTaskHistory.setDocumentMethod(function getDuration() {
|
|
166
|
+
|
|
167
|
+
if (!this.started_at) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let end = this.ended_at || (this.is_running ? new Date() : null);
|
|
172
|
+
|
|
173
|
+
if (!end) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return end.getTime() - this.started_at.getTime();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Query the most recent task runs with optional filters. Centralized
|
|
182
|
+
* here so that both Chimera-side dashboards and MCP / API consumers can
|
|
183
|
+
* use the same paginated query - avoids forking the criteria.
|
|
184
|
+
*
|
|
185
|
+
* Options:
|
|
186
|
+
* type {string} Filter by task type_path (e.g. 'arcana.task.sync_harvest_clients')
|
|
187
|
+
* status {string} 'running' | 'done' | 'failed' | 'aborted'
|
|
188
|
+
* since {Date} Earliest started_at to include
|
|
189
|
+
* limit {number} Page size (1-100, default 25)
|
|
190
|
+
* offset {number} Skip count
|
|
191
|
+
*
|
|
192
|
+
* Returns `{rows, total}` where `total` is the unpaginated count.
|
|
193
|
+
*
|
|
194
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
195
|
+
* @since 1.4.2
|
|
196
|
+
*
|
|
197
|
+
* @param {Object} [options]
|
|
198
|
+
*
|
|
199
|
+
* @return {Promise<{rows: DocumentList, total: number}>}
|
|
200
|
+
*/
|
|
201
|
+
SystemTaskHistory.setMethod(async function findRecent(options) {
|
|
202
|
+
|
|
203
|
+
options = options || {};
|
|
204
|
+
|
|
205
|
+
let crit = this.find();
|
|
206
|
+
|
|
207
|
+
if (options.type) {
|
|
208
|
+
crit.where('type').equals(options.type);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (options.status) {
|
|
212
|
+
switch (options.status) {
|
|
213
|
+
case 'running':
|
|
214
|
+
crit.where('is_running').equals(true);
|
|
215
|
+
break;
|
|
216
|
+
|
|
217
|
+
case 'failed':
|
|
218
|
+
crit.where('had_error').equals(true);
|
|
219
|
+
break;
|
|
220
|
+
|
|
221
|
+
case 'done':
|
|
222
|
+
crit.where('is_running').equals(false);
|
|
223
|
+
crit.where('had_error').not().equals(true);
|
|
224
|
+
crit.where('ended_at').exists(true);
|
|
225
|
+
break;
|
|
226
|
+
|
|
227
|
+
case 'aborted':
|
|
228
|
+
crit.where('is_running').equals(false);
|
|
229
|
+
crit.where('had_error').not().equals(true);
|
|
230
|
+
crit.where('ended_at').isEmpty();
|
|
231
|
+
crit.where('started_at').exists(true);
|
|
232
|
+
break;
|
|
233
|
+
|
|
234
|
+
default:
|
|
235
|
+
throw new Error('Unknown task-run status filter: ' + options.status);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (options.since) {
|
|
240
|
+
crit.where('started_at').gte(options.since);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let limit = Math.min(Math.max(options.limit || 25, 1), 100);
|
|
244
|
+
let offset = Math.max(options.offset || 0, 0);
|
|
245
|
+
|
|
246
|
+
crit.sort({started_at: -1});
|
|
247
|
+
crit.setOption('available', true);
|
|
248
|
+
crit.limit(limit);
|
|
249
|
+
if (offset > 0) crit.skip(offset);
|
|
250
|
+
|
|
251
|
+
let rows = await this.find('all', crit);
|
|
252
|
+
return {rows, total: rows.available || 0};
|
|
119
253
|
});
|
package/lib/class/conduit.js
CHANGED
|
@@ -1825,7 +1825,7 @@ Conduit.setMethod(function end(message) {
|
|
|
1825
1825
|
}
|
|
1826
1826
|
|
|
1827
1827
|
// Use regular JSON if DRY has been disabled in settings
|
|
1828
|
-
if (alchemy.settings.network.
|
|
1828
|
+
if (alchemy.settings.network.use_json_dry_responses === false || this.json_dry === false) {
|
|
1829
1829
|
json_type = 'json';
|
|
1830
1830
|
json_fnc = JSON.stringify;
|
|
1831
1831
|
} else {
|
|
@@ -2153,7 +2153,7 @@ Conduit.setTypedMethod([Types.String, Types.Object.optional()], function serveFi
|
|
|
2153
2153
|
*
|
|
2154
2154
|
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
2155
2155
|
* @since 0.2.0
|
|
2156
|
-
* @version 1.4.
|
|
2156
|
+
* @version 1.4.1
|
|
2157
2157
|
*
|
|
2158
2158
|
* @param {Alchemy.Inode.File} file The file to serve
|
|
2159
2159
|
* @param {Object} options Options, including headers
|
|
@@ -2171,6 +2171,9 @@ Conduit.setTypedMethod([Types.Alchemy.Inode.File, Types.Object.optional()], asyn
|
|
|
2171
2171
|
} catch (err) {
|
|
2172
2172
|
|
|
2173
2173
|
if (err.code == 'ENOENT') {
|
|
2174
|
+
if (options.onError) {
|
|
2175
|
+
return options.onError(err);
|
|
2176
|
+
}
|
|
2174
2177
|
return this.notFound(err);
|
|
2175
2178
|
}
|
|
2176
2179
|
|
package/lib/class/controller.js
CHANGED
|
@@ -432,6 +432,7 @@ Controller.setAction(async function readDatasource(conduit) {
|
|
|
432
432
|
user_id = this.getUserId();
|
|
433
433
|
|
|
434
434
|
// @TODO: non-users should also be able to send criteria :/
|
|
435
|
+
// We allow everyone to do so for now, fix later.
|
|
435
436
|
if (true || user_id) {
|
|
436
437
|
conduit.body = JSON.undry(conduit.body);
|
|
437
438
|
}
|
package/lib/class/datasource.js
CHANGED
|
@@ -508,7 +508,7 @@ Datasource.setMethod(function read(context) {
|
|
|
508
508
|
|
|
509
509
|
model.emit('reading_datasource', criteria);
|
|
510
510
|
|
|
511
|
-
|
|
511
|
+
let read_pledge = Pledge.Swift.waterfall(
|
|
512
512
|
that._read(context),
|
|
513
513
|
_result => {
|
|
514
514
|
|
|
@@ -548,6 +548,18 @@ Datasource.setMethod(function read(context) {
|
|
|
548
548
|
}
|
|
549
549
|
);
|
|
550
550
|
});
|
|
551
|
+
|
|
552
|
+
// If a cache_pledge was created, make sure it gets rejected on error
|
|
553
|
+
// so that subsequent queries with the same hash don't hang forever
|
|
554
|
+
if (cache_pledge) {
|
|
555
|
+
Pledge.Swift.done(read_pledge, (err) => {
|
|
556
|
+
if (err) {
|
|
557
|
+
cache_pledge.reject(err);
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return read_pledge;
|
|
551
563
|
});
|
|
552
564
|
|
|
553
565
|
/**
|
|
@@ -603,7 +615,7 @@ Datasource.setMethod(function update(context) {
|
|
|
603
615
|
|
|
604
616
|
let result = Swift.waterfall(
|
|
605
617
|
// Convert the data into something the datasource will understand
|
|
606
|
-
this.toDatasource(context),
|
|
618
|
+
() => this.toDatasource(context),
|
|
607
619
|
|
|
608
620
|
// Actually create the data
|
|
609
621
|
converted_data => this._update(context.setConvertedData(converted_data)),
|
package/lib/class/document.js
CHANGED
|
@@ -427,7 +427,7 @@ Document.setMethod(function toHawkejs(wm) {
|
|
|
427
427
|
|
|
428
428
|
result.setDataRecord(record, options);
|
|
429
429
|
} else {
|
|
430
|
-
|
|
430
|
+
result.$record = record;
|
|
431
431
|
}
|
|
432
432
|
|
|
433
433
|
// Clone $hold values if they are available
|
|
@@ -908,35 +908,63 @@ Document.setMethod(function exportToStream(output) {
|
|
|
908
908
|
*
|
|
909
909
|
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
910
910
|
* @since 1.0.5
|
|
911
|
-
* @version 1.
|
|
911
|
+
* @version 1.4.1
|
|
912
912
|
*
|
|
913
913
|
* @param {Buffer} buffer
|
|
914
914
|
*
|
|
915
915
|
* @return {Pledge}
|
|
916
916
|
*/
|
|
917
|
-
Document.setMethod(function importFromBuffer(buffer) {
|
|
917
|
+
Document.setMethod(function importFromBuffer(buffer, options) {
|
|
918
918
|
|
|
919
919
|
var that = this,
|
|
920
920
|
pledge = new Pledge();
|
|
921
921
|
|
|
922
|
+
if (!options) {
|
|
923
|
+
options = {};
|
|
924
|
+
}
|
|
925
|
+
|
|
922
926
|
zlib.gunzip(buffer, async function unzipped(err, data) {
|
|
923
927
|
|
|
924
928
|
if (err) {
|
|
925
929
|
return pledge.reject(err);
|
|
926
930
|
}
|
|
927
931
|
|
|
928
|
-
|
|
932
|
+
try {
|
|
933
|
+
data = JSON.undry(data.toString());
|
|
929
934
|
|
|
930
|
-
|
|
935
|
+
that.$main = data;
|
|
931
936
|
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
937
|
+
let save_options = {
|
|
938
|
+
validate : false,
|
|
939
|
+
override_created : true,
|
|
940
|
+
set_updated : false,
|
|
941
|
+
importing : true,
|
|
942
|
+
};
|
|
938
943
|
|
|
939
|
-
|
|
944
|
+
// If replace_existing is true, don't force create (allow update)
|
|
945
|
+
// Otherwise, force INSERT
|
|
946
|
+
if (!options.replace_existing) {
|
|
947
|
+
save_options.create = true;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
await that.save(null, save_options);
|
|
951
|
+
|
|
952
|
+
pledge.resolve();
|
|
953
|
+
} catch (save_err) {
|
|
954
|
+
// Ensure we have a proper Error object with a message
|
|
955
|
+
let error = save_err;
|
|
956
|
+
|
|
957
|
+
if (!error) {
|
|
958
|
+
error = new Error('Unknown error during document import');
|
|
959
|
+
} else if (!(error instanceof Error)) {
|
|
960
|
+
// If it's not an Error object, wrap it
|
|
961
|
+
let message = error.message || error.errmsg || String(error);
|
|
962
|
+
error = new Error('Import save failed: ' + message);
|
|
963
|
+
error.originalError = save_err;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
pledge.reject(error);
|
|
967
|
+
}
|
|
940
968
|
});
|
|
941
969
|
|
|
942
970
|
return pledge;
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
const libstream = require('stream');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The ImportStreamParser class:
|
|
5
|
+
* Handles parsing of Alchemy's binary import stream format.
|
|
6
|
+
*
|
|
7
|
+
* Stream format:
|
|
8
|
+
* - 0x01 [1-byte size] [model name]: Model header
|
|
9
|
+
* - 0x02 [4-byte size BE] [document data]: Document data
|
|
10
|
+
* - 0xFF [4-byte size BE] [extra data]: Extra import data for current document
|
|
11
|
+
*
|
|
12
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
13
|
+
* @since 1.4.1
|
|
14
|
+
* @version 1.4.1
|
|
15
|
+
*/
|
|
16
|
+
const ImportStreamParser = Function.inherits('Alchemy.Base', function ImportStreamParser(input, options) {
|
|
17
|
+
|
|
18
|
+
// The input stream to parse
|
|
19
|
+
this.input = input;
|
|
20
|
+
|
|
21
|
+
// Options
|
|
22
|
+
this.options = options || {};
|
|
23
|
+
|
|
24
|
+
// The model resolver function - receives model name, returns Model instance
|
|
25
|
+
// Can throw an error to abort parsing
|
|
26
|
+
this.model_resolver = null;
|
|
27
|
+
|
|
28
|
+
// State machine variables
|
|
29
|
+
this.current_type = null;
|
|
30
|
+
this.extra_stream = null;
|
|
31
|
+
this.stopped = false;
|
|
32
|
+
this.paused = false;
|
|
33
|
+
this.buffer = null;
|
|
34
|
+
this.model = null;
|
|
35
|
+
this.value = null;
|
|
36
|
+
this.seen = 0;
|
|
37
|
+
this.left = 0;
|
|
38
|
+
this.size = 0;
|
|
39
|
+
this.doc = null;
|
|
40
|
+
|
|
41
|
+
// Track stream end and pending imports
|
|
42
|
+
this.stream_ended = false;
|
|
43
|
+
this.pending_import = false;
|
|
44
|
+
|
|
45
|
+
// The pledge that resolves when parsing is complete
|
|
46
|
+
this.pledge = new Pledge();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Set the model resolver function.
|
|
51
|
+
* Called when a 0x01 model header is encountered.
|
|
52
|
+
*
|
|
53
|
+
* The resolver receives (model_name, current_model) and should:
|
|
54
|
+
* - Return a Model instance to use for subsequent documents
|
|
55
|
+
* - Throw an error to abort parsing
|
|
56
|
+
*
|
|
57
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
58
|
+
* @since 1.4.1
|
|
59
|
+
* @version 1.4.1
|
|
60
|
+
*
|
|
61
|
+
* @param {Function} resolver (model_name, current_model) => Model
|
|
62
|
+
*/
|
|
63
|
+
ImportStreamParser.setMethod(function setModelResolver(resolver) {
|
|
64
|
+
this.model_resolver = resolver;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Start parsing the input stream.
|
|
69
|
+
* Returns a Pledge that resolves when parsing is complete.
|
|
70
|
+
*
|
|
71
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
72
|
+
* @since 1.4.1
|
|
73
|
+
* @version 1.4.1
|
|
74
|
+
*
|
|
75
|
+
* @return {Pledge}
|
|
76
|
+
*/
|
|
77
|
+
ImportStreamParser.setMethod(function parse() {
|
|
78
|
+
|
|
79
|
+
let that = this;
|
|
80
|
+
|
|
81
|
+
if (!this.model_resolver) {
|
|
82
|
+
return Pledge.reject(new Error('No model resolver has been set'));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.input.on('data', function onData(data) {
|
|
86
|
+
|
|
87
|
+
if (that.stopped) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (that.buffer) {
|
|
92
|
+
that.buffer = Buffer.concat([that.buffer, data]);
|
|
93
|
+
} else {
|
|
94
|
+
that.buffer = data;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
that.handleBuffer();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
this.input.on('end', function onEnd() {
|
|
101
|
+
that.stream_ended = true;
|
|
102
|
+
|
|
103
|
+
// Only resolve if we're not in the middle of importing a document
|
|
104
|
+
if (!that.stopped && !that.pending_import) {
|
|
105
|
+
that.pledge.resolve();
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
this.input.on('error', function onError(err) {
|
|
110
|
+
that.stopped = true;
|
|
111
|
+
that.pledge.reject(err);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return this.pledge;
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Handle the current buffer data.
|
|
119
|
+
* Implements the state machine for parsing packet headers.
|
|
120
|
+
*
|
|
121
|
+
* State machine:
|
|
122
|
+
* - current_type = null: waiting for a new packet header
|
|
123
|
+
* - current_type = 0x01/0x02/0xFF: header parsed, processing payload
|
|
124
|
+
*
|
|
125
|
+
* We must NOT set current_type until we have the FULL header,
|
|
126
|
+
* otherwise a TCP chunk boundary could leave us in an invalid state.
|
|
127
|
+
*
|
|
128
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
129
|
+
* @since 1.4.1
|
|
130
|
+
* @version 1.4.1
|
|
131
|
+
*/
|
|
132
|
+
ImportStreamParser.setMethod(function handleBuffer() {
|
|
133
|
+
|
|
134
|
+
if (this.paused) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!this.current_type) {
|
|
139
|
+
// Need at least 1 byte to peek at the marker type
|
|
140
|
+
if (this.buffer.length < 1) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let marker = this.buffer.readUInt8(0);
|
|
145
|
+
|
|
146
|
+
if (marker == 0x01) {
|
|
147
|
+
// Type 0x01: 1-byte marker + 1-byte size = 2 bytes header
|
|
148
|
+
if (this.buffer.length < 2) {
|
|
149
|
+
return; // Wait for more data (don't consume the marker yet)
|
|
150
|
+
}
|
|
151
|
+
this.current_type = marker;
|
|
152
|
+
this.size = this.buffer.readUInt8(1);
|
|
153
|
+
this.buffer = this.buffer.slice(2);
|
|
154
|
+
} else if (marker == 0x02) {
|
|
155
|
+
// Type 0x02: 1-byte marker + 4-byte size = 5 bytes header
|
|
156
|
+
if (this.buffer.length < 5) {
|
|
157
|
+
return; // Wait for more data (don't consume the marker yet)
|
|
158
|
+
}
|
|
159
|
+
this.current_type = marker;
|
|
160
|
+
this.size = this.buffer.readUInt32BE(1);
|
|
161
|
+
this.buffer = this.buffer.slice(5);
|
|
162
|
+
} else if (marker == 0xFF) {
|
|
163
|
+
// Type 0xFF: 1-byte marker + 4-byte size = 5 bytes header
|
|
164
|
+
if (this.buffer.length < 5) {
|
|
165
|
+
return; // Wait for more data (don't consume the marker yet)
|
|
166
|
+
}
|
|
167
|
+
this.current_type = marker;
|
|
168
|
+
this.size = this.buffer.readUInt32BE(1);
|
|
169
|
+
this.buffer = this.buffer.slice(5);
|
|
170
|
+
this.seen = 0;
|
|
171
|
+
|
|
172
|
+
if (!this.doc) {
|
|
173
|
+
this.stopped = true;
|
|
174
|
+
this.pledge.reject(new Error('Found extra import data, but no active document'));
|
|
175
|
+
} else {
|
|
176
|
+
this.extra_stream = new libstream.PassThrough();
|
|
177
|
+
this.doc.extraImportFromStream(this.extra_stream);
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
// Unknown marker - this shouldn't happen in valid data
|
|
181
|
+
this.stopped = true;
|
|
182
|
+
this.pledge.reject(new Error('Unknown marker byte: 0x' + marker.toString(16)));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
this.handlePayload();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Handle the payload data after a header has been parsed.
|
|
192
|
+
*
|
|
193
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
194
|
+
* @since 1.4.1
|
|
195
|
+
* @version 1.4.1
|
|
196
|
+
*/
|
|
197
|
+
ImportStreamParser.setMethod(function handlePayload() {
|
|
198
|
+
|
|
199
|
+
let that = this;
|
|
200
|
+
|
|
201
|
+
// Handle extra data streaming (0xFF)
|
|
202
|
+
if (this.current_type == 0xFF) {
|
|
203
|
+
this.left = this.size - this.seen;
|
|
204
|
+
this.value = this.buffer.slice(0, this.left);
|
|
205
|
+
|
|
206
|
+
this.seen += this.value.length;
|
|
207
|
+
|
|
208
|
+
if (this.value.length == this.buffer.length) {
|
|
209
|
+
this.buffer = null;
|
|
210
|
+
} else if (this.value.length < this.buffer.length) {
|
|
211
|
+
this.buffer = this.buffer.slice(this.left);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
this.extra_stream.write(this.value);
|
|
215
|
+
|
|
216
|
+
if (this.value.length == this.left) {
|
|
217
|
+
this.extra_stream.end();
|
|
218
|
+
this.current_type = null;
|
|
219
|
+
|
|
220
|
+
if (this.buffer) {
|
|
221
|
+
this.handleBuffer();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Wait for full payload
|
|
229
|
+
if (this.buffer.length >= this.size) {
|
|
230
|
+
this.value = this.buffer.slice(0, this.size);
|
|
231
|
+
this.buffer = this.buffer.slice(this.size);
|
|
232
|
+
} else {
|
|
233
|
+
// Wait for next call
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Handle model header (0x01)
|
|
238
|
+
if (this.current_type == 0x01) {
|
|
239
|
+
let model_name = this.value.toString();
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
this.model = this.model_resolver(model_name, this.model);
|
|
243
|
+
this.doc = null;
|
|
244
|
+
} catch (err) {
|
|
245
|
+
this.stopped = true;
|
|
246
|
+
return this.pledge.reject(err);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (!this.model) {
|
|
250
|
+
this.stopped = true;
|
|
251
|
+
return this.pledge.reject(new Error('Model resolver returned no model for "' + model_name + '"'));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
this.current_type = null;
|
|
255
|
+
this.size = 0;
|
|
256
|
+
}
|
|
257
|
+
// Handle document data (0x02)
|
|
258
|
+
else if (this.current_type == 0x02) {
|
|
259
|
+
this.doc = this.model.createDocument();
|
|
260
|
+
this.input.pause();
|
|
261
|
+
this.paused = true;
|
|
262
|
+
this.pending_import = true;
|
|
263
|
+
|
|
264
|
+
this.doc.importFromBuffer(this.value, this.options).done(function done(err, result) {
|
|
265
|
+
|
|
266
|
+
that.pending_import = false;
|
|
267
|
+
|
|
268
|
+
if (err) {
|
|
269
|
+
that.stopped = true;
|
|
270
|
+
return that.pledge.reject(err);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
that.current_type = null;
|
|
274
|
+
that.paused = false;
|
|
275
|
+
|
|
276
|
+
// Check if there's more data to process
|
|
277
|
+
if (that.buffer && that.buffer.length > 0) {
|
|
278
|
+
that.input.resume();
|
|
279
|
+
that.handleBuffer();
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// No more data in buffer
|
|
284
|
+
that.input.resume();
|
|
285
|
+
|
|
286
|
+
// If the stream has ended and there's no more data, resolve
|
|
287
|
+
if (that.stream_ended && !that.stopped) {
|
|
288
|
+
that.pledge.resolve();
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Continue processing remaining buffer
|
|
296
|
+
if (this.buffer && this.buffer.length) {
|
|
297
|
+
this.handleBuffer();
|
|
298
|
+
}
|
|
299
|
+
});
|
package/lib/class/inode_file.js
CHANGED
package/lib/class/migration.js
CHANGED
|
@@ -20,7 +20,7 @@ const Migration = Function.inherits('Alchemy.Base', function Migration(document)
|
|
|
20
20
|
*
|
|
21
21
|
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
22
22
|
* @since 1.2.0
|
|
23
|
-
* @version 1.
|
|
23
|
+
* @version 1.4.1
|
|
24
24
|
*/
|
|
25
25
|
Migration.setStatic(async function start() {
|
|
26
26
|
|
|
@@ -32,7 +32,10 @@ Migration.setStatic(async function start() {
|
|
|
32
32
|
|
|
33
33
|
await dir.loadContents();
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
// Sort entries alphabetically so numbered prefixes (001_, 002_) run in order
|
|
36
|
+
let entries = [...dir].sort((a, b) => a.name.localeCompare(b.name));
|
|
37
|
+
|
|
38
|
+
for (let entry of entries) {
|
|
36
39
|
|
|
37
40
|
let name = entry.name.beforeLast('.js');
|
|
38
41
|
|