@sasjs/cli 6.8.4 → 6.9.0
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/build/commands/build/internal/mockSite/assets/runtime-core.js +68 -1
- package/build/commands/build/internal/mockSite/mockSite.d.ts +18 -0
- package/build/commands/build/internal/mockSite/mockSite.js +35 -2
- package/build/commands/build/internal/mockSite/spec/mockSite.spec.js +42 -8
- package/build/commands/build/internal/mockSite/spec/runtime-core.spec.js +68 -1
- package/build/commands/shared/fetchLogFileContent.js +40 -10
- package/build/commands/shared/spec/fetchLogFileContent.spec.d.ts +1 -0
- package/build/commands/shared/spec/fetchLogFileContent.spec.js +134 -0
- package/package.json +2 -2
|
@@ -731,6 +731,15 @@ const FS = {
|
|
|
731
731
|
else throw e
|
|
732
732
|
}
|
|
733
733
|
},
|
|
734
|
+
/**
|
|
735
|
+
* Node's appendFileSync. The webout() block streams its members with it, and
|
|
736
|
+
* writeFileSync already honours the appending flag, so this is the same write
|
|
737
|
+
* with that flag set - text stays text and bytes stay bytes, as in node.
|
|
738
|
+
*/
|
|
739
|
+
appendFileSync(p, data, options) {
|
|
740
|
+
const { encoding } = writeOptions(options)
|
|
741
|
+
FS.writeFileSync(p, data, { encoding, flag: 'a' })
|
|
742
|
+
},
|
|
734
743
|
mkdirSync(p) {
|
|
735
744
|
mkdirp(P.resolve(String(p)))
|
|
736
745
|
},
|
|
@@ -858,6 +867,46 @@ const loadMockConfig = async () => {
|
|
|
858
867
|
}
|
|
859
868
|
}
|
|
860
869
|
|
|
870
|
+
/**
|
|
871
|
+
* The webout() block, generated by the mock build from @sasjs/utils with
|
|
872
|
+
* sentinel values (see WEBOUT_SENTINELS in mockSite.ts). Filled in per run, so
|
|
873
|
+
* a service that sends data back behaves exactly as it does on a server -
|
|
874
|
+
* including the content type, which is what the block sets.
|
|
875
|
+
*/
|
|
876
|
+
let weboutPreludeTemplate = ''
|
|
877
|
+
let weboutSentinels = null
|
|
878
|
+
|
|
879
|
+
const loadWeboutPrelude = async () => {
|
|
880
|
+
try {
|
|
881
|
+
const res = await fetch('/host/webout-prelude.json', { cache: 'no-store' })
|
|
882
|
+
if (!res.ok) return
|
|
883
|
+
const cfg = await res.json()
|
|
884
|
+
if (cfg && typeof cfg.template === 'string') {
|
|
885
|
+
weboutPreludeTemplate = cfg.template
|
|
886
|
+
weboutSentinels = cfg.sentinels || null
|
|
887
|
+
}
|
|
888
|
+
} catch (e) {
|
|
889
|
+
/* a site built before the block existed keeps the _webout-only path */
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* One run's webout() source: each sentinel replaced by this run's value, quoted
|
|
895
|
+
* exactly as the server's generator quotes it. The sentinel names come from the
|
|
896
|
+
* build (WEBOUT_SENTINELS), so the two sides cannot disagree about them.
|
|
897
|
+
*/
|
|
898
|
+
const fillWeboutPrelude = (values) => {
|
|
899
|
+
if (!weboutPreludeTemplate || !weboutSentinels) return ''
|
|
900
|
+
let text = weboutPreludeTemplate
|
|
901
|
+
for (const [key, sentinel] of Object.entries(weboutSentinels)) {
|
|
902
|
+
if (!(key in values)) continue
|
|
903
|
+
text = text
|
|
904
|
+
.split('"' + sentinel + '"')
|
|
905
|
+
.join(JSON.stringify(String(values[key])))
|
|
906
|
+
}
|
|
907
|
+
return text
|
|
908
|
+
}
|
|
909
|
+
|
|
861
910
|
let sessionSeq = 0
|
|
862
911
|
|
|
863
912
|
const formatArg = (a) => {
|
|
@@ -951,6 +1000,18 @@ const runProgram = ({ program, code, params = {}, tables = [] }) => {
|
|
|
951
1000
|
})
|
|
952
1001
|
webinStatements.push(`const _WEBIN_FILE_COUNT = ${tables.length};`)
|
|
953
1002
|
|
|
1003
|
+
// The webout() block, filled in for this run. Empty only for a site whose
|
|
1004
|
+
// build predates it, where the _webout-only tail below still works.
|
|
1005
|
+
const weboutPrelude = fillWeboutPrelude({
|
|
1006
|
+
weboutPath,
|
|
1007
|
+
headersPath,
|
|
1008
|
+
program: codePath || program || '',
|
|
1009
|
+
debug: params._debug || '',
|
|
1010
|
+
username: 'DEMO',
|
|
1011
|
+
userId: '0',
|
|
1012
|
+
apiServerUrl: typeof location !== 'undefined' ? location.origin : ''
|
|
1013
|
+
})
|
|
1014
|
+
|
|
954
1015
|
const preProgram = `
|
|
955
1016
|
let _webout = '';
|
|
956
1017
|
const weboutPath = ${JSON.stringify(weboutPath)};
|
|
@@ -969,8 +1030,13 @@ const fs = require('fs');
|
|
|
969
1030
|
${varStatements}
|
|
970
1031
|
${webinStatements.join('\n')}
|
|
971
1032
|
${preProgram}
|
|
1033
|
+
${weboutPrelude}
|
|
972
1034
|
${payloadCode || FILES.get(codePath)}
|
|
973
|
-
|
|
1035
|
+
${
|
|
1036
|
+
weboutPrelude
|
|
1037
|
+
? '_sasjs_webout.finalise(_webout);'
|
|
1038
|
+
: 'if (_webout) { fs.writeFile(weboutPath, _webout, function () {}); }'
|
|
1039
|
+
}
|
|
974
1040
|
return _webout;
|
|
975
1041
|
`
|
|
976
1042
|
|
|
@@ -1180,6 +1246,7 @@ const fetchDrive = async () => {
|
|
|
1180
1246
|
|
|
1181
1247
|
const boot = async () => {
|
|
1182
1248
|
await loadMockConfig()
|
|
1249
|
+
await loadWeboutPrelude()
|
|
1183
1250
|
const { source, files: seed } = await fetchDrive()
|
|
1184
1251
|
mkdirp(DRIVE_APP)
|
|
1185
1252
|
mkdirp(`${DRIVE_ROOT}/files`)
|
|
@@ -10,6 +10,24 @@ export interface MockSiteResult {
|
|
|
10
10
|
/** True when a captured drive snapshot was shipped with the site. */
|
|
11
11
|
snapshot: boolean;
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* The webout() block is the one @sasjs/utils generates for the server, emitted
|
|
15
|
+
* here with sentinel values and filled in per run by runtime-core.js. That is
|
|
16
|
+
* what makes a service behave identically as a stored program and in a mock
|
|
17
|
+
* build: one copy of the block, one set of paths, one envelope.
|
|
18
|
+
*
|
|
19
|
+
* The sentinels are quoted in the emitted text (they are JSON string literals),
|
|
20
|
+
* so the substitution replaces the whole literal and never a fragment.
|
|
21
|
+
*/
|
|
22
|
+
export declare const WEBOUT_SENTINELS: {
|
|
23
|
+
weboutPath: string;
|
|
24
|
+
headersPath: string;
|
|
25
|
+
program: string;
|
|
26
|
+
debug: string;
|
|
27
|
+
username: string;
|
|
28
|
+
userId: string;
|
|
29
|
+
apiServerUrl: string;
|
|
30
|
+
};
|
|
13
31
|
/**
|
|
14
32
|
* The bounds on what the browser drive seeds. A byte entry is carried as base64
|
|
15
33
|
* in vfs-seed.json and held in the worker's memory, so a large artefact - an
|
|
@@ -75,12 +75,30 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
75
75
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
76
76
|
};
|
|
77
77
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
78
|
-
exports.DRIVE_FORMAT = exports.MAX_SEED_BYTES = exports.MAX_BINARY_SEED_BYTES = void 0;
|
|
78
|
+
exports.DRIVE_FORMAT = exports.MAX_SEED_BYTES = exports.MAX_BINARY_SEED_BYTES = exports.WEBOUT_SENTINELS = void 0;
|
|
79
79
|
exports.buildMockSite = buildMockSite;
|
|
80
80
|
var crypto_1 = require("crypto");
|
|
81
81
|
var fs_1 = __importDefault(require("fs"));
|
|
82
82
|
var path_1 = __importDefault(require("path"));
|
|
83
83
|
var utils_1 = require("@sasjs/utils");
|
|
84
|
+
/**
|
|
85
|
+
* The webout() block is the one @sasjs/utils generates for the server, emitted
|
|
86
|
+
* here with sentinel values and filled in per run by runtime-core.js. That is
|
|
87
|
+
* what makes a service behave identically as a stored program and in a mock
|
|
88
|
+
* build: one copy of the block, one set of paths, one envelope.
|
|
89
|
+
*
|
|
90
|
+
* The sentinels are quoted in the emitted text (they are JSON string literals),
|
|
91
|
+
* so the substitution replaces the whole literal and never a fragment.
|
|
92
|
+
*/
|
|
93
|
+
exports.WEBOUT_SENTINELS = {
|
|
94
|
+
weboutPath: '__SASJS_WEBOUT_PATH__',
|
|
95
|
+
headersPath: '__SASJS_WEBOUT_HEADERS__',
|
|
96
|
+
program: '__SASJS_WEBOUT_PROGRAM__',
|
|
97
|
+
debug: '__SASJS_WEBOUT_DEBUG__',
|
|
98
|
+
username: '__SASJS_WEBOUT_USERNAME__',
|
|
99
|
+
userId: '__SASJS_WEBOUT_USERID__',
|
|
100
|
+
apiServerUrl: '__SASJS_WEBOUT_APIURL__'
|
|
101
|
+
};
|
|
84
102
|
var ASSETS_FOLDER = path_1.default.join(__dirname, 'assets');
|
|
85
103
|
var toPosix = function (value) { return value.split(path_1.default.sep).join('/'); };
|
|
86
104
|
var listFiles = function (folder, base) {
|
|
@@ -439,8 +457,23 @@ function buildMockSite(target) {
|
|
|
439
457
|
// sw.js sits at the site root: a worker can only control the folder it is
|
|
440
458
|
// served from, and the app has to be controlled from /
|
|
441
459
|
fs_1.default.copyFileSync(path_1.default.join(ASSETS_FOLDER, 'sw.js'), path_1.default.join(outputFolder, 'sw.js'));
|
|
460
|
+
// 2a. the webout() block, generated once with sentinels that runtime-core.js
|
|
461
|
+
// fills in per run. It is the same text the server emits, from @sasjs/utils,
|
|
462
|
+
// so a service cannot behave differently in a mock build.
|
|
463
|
+
fs_1.default.writeFileSync(path_1.default.join(hostFolder, 'webout-prelude.json'), JSON.stringify({
|
|
464
|
+
sentinels: exports.WEBOUT_SENTINELS,
|
|
465
|
+
template: (0, utils_1.getWeboutPrelude)({
|
|
466
|
+
weboutPath: exports.WEBOUT_SENTINELS.weboutPath,
|
|
467
|
+
headersPath: exports.WEBOUT_SENTINELS.headersPath,
|
|
468
|
+
program: exports.WEBOUT_SENTINELS.program,
|
|
469
|
+
debug: exports.WEBOUT_SENTINELS.debug,
|
|
470
|
+
username: exports.WEBOUT_SENTINELS.username,
|
|
471
|
+
userId: exports.WEBOUT_SENTINELS.userId,
|
|
472
|
+
apiServerUrl: exports.WEBOUT_SENTINELS.apiServerUrl
|
|
473
|
+
})
|
|
474
|
+
}));
|
|
442
475
|
hostVersion = (0, crypto_1.createHash)('md5')
|
|
443
|
-
.update(['runtime-core.js', 'persist.js', 'intercept.js']
|
|
476
|
+
.update(['runtime-core.js', 'persist.js', 'intercept.js', 'webout-prelude.json']
|
|
444
477
|
.map(function (asset) { return fs_1.default.readFileSync(path_1.default.join(hostFolder, asset)); })
|
|
445
478
|
.reduce(function (all, buf) { return Buffer.concat([all, buf]); }, Buffer.alloc(0)))
|
|
446
479
|
.digest('hex')
|
|
@@ -189,6 +189,40 @@ describe('browser-hosted build', function () {
|
|
|
189
189
|
}
|
|
190
190
|
});
|
|
191
191
|
}); });
|
|
192
|
+
it('ships the webout() block with sentinels the runtime fills in', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
193
|
+
var prelude, _a, _b, sentinel;
|
|
194
|
+
var e_2, _c;
|
|
195
|
+
return __generator(this, function (_d) {
|
|
196
|
+
switch (_d.label) {
|
|
197
|
+
case 0: return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
|
|
198
|
+
case 1:
|
|
199
|
+
_d.sent();
|
|
200
|
+
prelude = JSON.parse(fs_1.default.readFileSync(path_1.default.join(out(), 'host', 'webout-prelude.json'), 'utf8'));
|
|
201
|
+
// The block is the one @sasjs/utils generates for the server. It travels
|
|
202
|
+
// with sentinel values because the run's paths and user are not known at
|
|
203
|
+
// build time, and the sentinel names travel with it so the two sides cannot
|
|
204
|
+
// disagree about them.
|
|
205
|
+
expect(prelude.template).toContain('webout(');
|
|
206
|
+
expect(prelude.template).toContain('_sasjs_webout.finalise');
|
|
207
|
+
expect(prelude.template).toContain('content-type: application/json');
|
|
208
|
+
expect(prelude.sentinels.weboutPath).toEqual('__SASJS_WEBOUT_PATH__');
|
|
209
|
+
try {
|
|
210
|
+
for (_a = __values(Object.values(prelude.sentinels)), _b = _a.next(); !_b.done; _b = _a.next()) {
|
|
211
|
+
sentinel = _b.value;
|
|
212
|
+
expect(prelude.template).toContain(JSON.stringify(sentinel));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
catch (e_2_1) { e_2 = { error: e_2_1 }; }
|
|
216
|
+
finally {
|
|
217
|
+
try {
|
|
218
|
+
if (_b && !_b.done && (_c = _a.return)) _c.call(_a);
|
|
219
|
+
}
|
|
220
|
+
finally { if (e_2) throw e_2.error; }
|
|
221
|
+
}
|
|
222
|
+
return [2 /*return*/];
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
}); });
|
|
192
226
|
it('points the app at itself and installs the boot loader', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
193
227
|
var html;
|
|
194
228
|
return __generator(this, function (_a) {
|
|
@@ -293,7 +327,7 @@ describe('browser-hosted build', function () {
|
|
|
293
327
|
}); });
|
|
294
328
|
it('stops seeding bytes once the drive budget is spent, and names the file', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
295
329
|
var chunk, _a, _b, name_1, seed, seeded;
|
|
296
|
-
var
|
|
330
|
+
var e_3, _c;
|
|
297
331
|
var _d;
|
|
298
332
|
return __generator(this, function (_e) {
|
|
299
333
|
switch (_e.label) {
|
|
@@ -306,12 +340,12 @@ describe('browser-hosted build', function () {
|
|
|
306
340
|
fs_1.default.writeFileSync(path_1.default.join(root, 'sasjsbuild', 'mock-storage', name_1), chunk);
|
|
307
341
|
}
|
|
308
342
|
}
|
|
309
|
-
catch (
|
|
343
|
+
catch (e_3_1) { e_3 = { error: e_3_1 }; }
|
|
310
344
|
finally {
|
|
311
345
|
try {
|
|
312
346
|
if (_b && !_b.done && (_c = _a.return)) _c.call(_a);
|
|
313
347
|
}
|
|
314
|
-
finally { if (
|
|
348
|
+
finally { if (e_3) throw e_3.error; }
|
|
315
349
|
}
|
|
316
350
|
return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())
|
|
317
351
|
// the budget is spent in path order, so the drive is the same on every host:
|
|
@@ -465,8 +499,8 @@ describe('browser-hosted build', function () {
|
|
|
465
499
|
});
|
|
466
500
|
}); });
|
|
467
501
|
it('refuses a mockWebFolder that would delete the project or its inputs', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
468
|
-
var _a, _b, mockWebFolder,
|
|
469
|
-
var
|
|
502
|
+
var _a, _b, mockWebFolder, e_4_1;
|
|
503
|
+
var e_4, _c;
|
|
470
504
|
return __generator(this, function (_d) {
|
|
471
505
|
switch (_d.label) {
|
|
472
506
|
case 0:
|
|
@@ -497,14 +531,14 @@ describe('browser-hosted build', function () {
|
|
|
497
531
|
return [3 /*break*/, 1];
|
|
498
532
|
case 4: return [3 /*break*/, 7];
|
|
499
533
|
case 5:
|
|
500
|
-
|
|
501
|
-
|
|
534
|
+
e_4_1 = _d.sent();
|
|
535
|
+
e_4 = { error: e_4_1 };
|
|
502
536
|
return [3 /*break*/, 7];
|
|
503
537
|
case 6:
|
|
504
538
|
try {
|
|
505
539
|
if (_b && !_b.done && (_c = _a.return)) _c.call(_a);
|
|
506
540
|
}
|
|
507
|
-
finally { if (
|
|
541
|
+
finally { if (e_4) throw e_4.error; }
|
|
508
542
|
return [7 /*endfinally*/];
|
|
509
543
|
case 7:
|
|
510
544
|
expect(fs_1.default.existsSync(path_1.default.join(root, 'client', 'dist', 'index.html'))).toBe(true);
|
|
@@ -36,6 +36,8 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
|
36
36
|
}
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
var utils_1 = require("@sasjs/utils");
|
|
40
|
+
var mockSite_1 = require("../mockSite");
|
|
39
41
|
/**
|
|
40
42
|
* The parity claim of a browser-hosted build is that a service runs in the
|
|
41
43
|
* browser exactly as it runs as a stored program. That claim lives in
|
|
@@ -59,7 +61,7 @@ var jsonResponse = function (body) { return ({
|
|
|
59
61
|
return [2 /*return*/, body];
|
|
60
62
|
}); }); }
|
|
61
63
|
}); };
|
|
62
|
-
var seedDrive = function (runtime, files) { return __awaiter(void 0, void 0, void 0, function () {
|
|
64
|
+
var seedDrive = function (runtime, files, prelude) { return __awaiter(void 0, void 0, void 0, function () {
|
|
63
65
|
return __generator(this, function (_a) {
|
|
64
66
|
switch (_a.label) {
|
|
65
67
|
case 0:
|
|
@@ -70,6 +72,12 @@ var seedDrive = function (runtime, files) { return __awaiter(void 0, void 0, voi
|
|
|
70
72
|
return [2 /*return*/, jsonResponse({ appLoc: appLoc })];
|
|
71
73
|
if (url.includes('vfs-seed'))
|
|
72
74
|
return [2 /*return*/, jsonResponse({ files: files })];
|
|
75
|
+
if (url.includes('webout-prelude'))
|
|
76
|
+
return [2 /*return*/, prelude
|
|
77
|
+
? jsonResponse(prelude)
|
|
78
|
+
: { ok: false, json: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
|
|
79
|
+
return [2 /*return*/, ({})];
|
|
80
|
+
}); }); } }];
|
|
73
81
|
return [2 /*return*/, { ok: false, json: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
|
|
74
82
|
return [2 /*return*/, ({})];
|
|
75
83
|
}); }); } }];
|
|
@@ -128,6 +136,65 @@ describe('browser runtime', function () {
|
|
|
128
136
|
}
|
|
129
137
|
});
|
|
130
138
|
}); });
|
|
139
|
+
it('sends data back the way the server does, through webout()', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
140
|
+
var runtime, result, parsed;
|
|
141
|
+
return __generator(this, function (_a) {
|
|
142
|
+
switch (_a.label) {
|
|
143
|
+
case 0:
|
|
144
|
+
runtime = loadRuntime();
|
|
145
|
+
// A mock build ships the same block the server generates, with sentinel
|
|
146
|
+
// values runtime-core fills per run. Without it a service written with
|
|
147
|
+
// webout() fails in a mock build with "webout is not defined", which is the
|
|
148
|
+
// gap this closes.
|
|
149
|
+
return [4 /*yield*/, seedDrive(runtime, {}, {
|
|
150
|
+
sentinels: mockSite_1.WEBOUT_SENTINELS,
|
|
151
|
+
template: (0, utils_1.getWeboutPrelude)({
|
|
152
|
+
weboutPath: mockSite_1.WEBOUT_SENTINELS.weboutPath,
|
|
153
|
+
headersPath: mockSite_1.WEBOUT_SENTINELS.headersPath,
|
|
154
|
+
program: mockSite_1.WEBOUT_SENTINELS.program,
|
|
155
|
+
debug: mockSite_1.WEBOUT_SENTINELS.debug,
|
|
156
|
+
username: mockSite_1.WEBOUT_SENTINELS.username,
|
|
157
|
+
userId: mockSite_1.WEBOUT_SENTINELS.userId,
|
|
158
|
+
apiServerUrl: mockSite_1.WEBOUT_SENTINELS.apiServerUrl
|
|
159
|
+
})
|
|
160
|
+
})];
|
|
161
|
+
case 1:
|
|
162
|
+
// A mock build ships the same block the server generates, with sentinel
|
|
163
|
+
// values runtime-core fills per run. Without it a service written with
|
|
164
|
+
// webout() fails in a mock build with "webout is not defined", which is the
|
|
165
|
+
// gap this closes.
|
|
166
|
+
_a.sent();
|
|
167
|
+
result = runtime.run({
|
|
168
|
+
code: "\nwebout('OPEN')\nwebout('OBJ', 'users', [{ name: 'ann' }, { name: 'bob' }])\nwebout('CLOSE')"
|
|
169
|
+
});
|
|
170
|
+
expect(result.error).toBeNull();
|
|
171
|
+
parsed = JSON.parse(result.webout);
|
|
172
|
+
expect(parsed.users).toEqual([{ name: 'ann' }, { name: 'bob' }]);
|
|
173
|
+
expect(parsed.SYSDATE).toMatch(/^\d{2}[A-Z]{3}\d{4}$/);
|
|
174
|
+
expect(parsed.MF_GETUSER).toEqual('DEMO');
|
|
175
|
+
expect(parsed.SYSPROCESSNAME).toEqual('Browser');
|
|
176
|
+
expect(parsed.SYSSCPL).toEqual('browser');
|
|
177
|
+
// The content type is the point: the block sets it, so the worker hands the
|
|
178
|
+
// app JSON rather than a string. The mock seeds the file empty (the server
|
|
179
|
+
// seeds text/html), so the block's record is the only one here.
|
|
180
|
+
expect(result.headers).toContain('content-type: application/json');
|
|
181
|
+
return [2 /*return*/];
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
}); });
|
|
185
|
+
it('leaves the headers alone for a service that only assigns _webout', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
186
|
+
var runtime, result;
|
|
187
|
+
return __generator(this, function (_a) {
|
|
188
|
+
runtime = loadRuntime();
|
|
189
|
+
result = runtime.run({ code: "_webout = '<h1>raw</h1>'" });
|
|
190
|
+
expect(result.error).toBeNull();
|
|
191
|
+
expect(result.webout).toEqual('<h1>raw</h1>');
|
|
192
|
+
// Unchanged behaviour: a raw assignment declares no type here, and the
|
|
193
|
+
// worker's binary fallback depends on this file staying empty.
|
|
194
|
+
expect(result.headers).toEqual('');
|
|
195
|
+
return [2 /*return*/];
|
|
196
|
+
});
|
|
197
|
+
}); });
|
|
131
198
|
it('exposes request parameters as variables, with JSON escaping', function () {
|
|
132
199
|
var runtime = loadRuntime();
|
|
133
200
|
var result = runtime.run({
|
|
@@ -62,6 +62,31 @@ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
|
|
62
62
|
};
|
|
63
63
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
64
64
|
exports.fetchLogFileContent = void 0;
|
|
65
|
+
/**
|
|
66
|
+
* Parses a log response into the text of the log, or into the paged shape Viya
|
|
67
|
+
* returns.
|
|
68
|
+
*
|
|
69
|
+
* Viya answers a log request with `{ items: [{ line }] }` and pages it with
|
|
70
|
+
* `start` and `limit`. A server that answers with the log itself has no chunks
|
|
71
|
+
* to parse, and the body must not be fed to `JSON.parse` blindly: a log whose
|
|
72
|
+
* first character happens to be a digit parses as a number, and reading `.items`
|
|
73
|
+
* off it throws.
|
|
74
|
+
*
|
|
75
|
+
* The adapter hands back `JSON.stringify(result)` whatever the server sent, so
|
|
76
|
+
* the log arrives here quoted and escaped - parsing once gives the text back.
|
|
77
|
+
*/
|
|
78
|
+
var parseLogChunk = function (body) {
|
|
79
|
+
var parsed;
|
|
80
|
+
try {
|
|
81
|
+
parsed = JSON.parse(body);
|
|
82
|
+
}
|
|
83
|
+
catch (_) {
|
|
84
|
+
return { text: body !== null && body !== void 0 ? body : '' };
|
|
85
|
+
}
|
|
86
|
+
if (parsed && Array.isArray(parsed.items))
|
|
87
|
+
return parsed;
|
|
88
|
+
return { text: typeof parsed === 'string' ? parsed : (body !== null && body !== void 0 ? body : '') };
|
|
89
|
+
};
|
|
65
90
|
/**
|
|
66
91
|
* Fetches content of the log file
|
|
67
92
|
* @param {object} sasjs - configuration object of SAS adapter.
|
|
@@ -71,27 +96,32 @@ exports.fetchLogFileContent = void 0;
|
|
|
71
96
|
* @returns an object containing log lines in 'items' array.
|
|
72
97
|
*/
|
|
73
98
|
var fetchLogFileContent = function (sasjs, accessToken, logUrl, logCount) { return __awaiter(void 0, void 0, void 0, function () {
|
|
74
|
-
var logJson, loglimit, start,
|
|
75
|
-
return __generator(this, function (
|
|
76
|
-
switch (
|
|
99
|
+
var logJson, loglimit, start, body, parsed, logChunkJson;
|
|
100
|
+
return __generator(this, function (_a) {
|
|
101
|
+
switch (_a.label) {
|
|
77
102
|
case 0:
|
|
78
103
|
logJson = { items: [] };
|
|
79
104
|
loglimit = logCount < 10000 ? logCount : 10000;
|
|
80
105
|
start = 0;
|
|
81
|
-
|
|
82
|
-
case 1:
|
|
83
|
-
_b = (_a = JSON).parse;
|
|
84
|
-
return [4 /*yield*/, sasjs.fetchLogFileContent("".concat(logUrl, "?start=").concat(start, "&limit=").concat(loglimit), accessToken)];
|
|
106
|
+
_a.label = 1;
|
|
107
|
+
case 1: return [4 /*yield*/, sasjs.fetchLogFileContent("".concat(logUrl, "?start=").concat(start, "&limit=").concat(loglimit), accessToken)];
|
|
85
108
|
case 2:
|
|
86
|
-
|
|
109
|
+
body = (_a.sent());
|
|
110
|
+
parsed = parseLogChunk(body);
|
|
111
|
+
// Not Viya's paged shape, so the body is the log: SASjs Server answers with
|
|
112
|
+
// the log itself, and the `start`/`limit` parameters mean nothing to it.
|
|
113
|
+
// Asking again would return the same text, so this is the whole answer.
|
|
114
|
+
if ('text' in parsed)
|
|
115
|
+
return [2 /*return*/, { items: [{ line: parsed.text }] }];
|
|
116
|
+
logChunkJson = parsed;
|
|
87
117
|
if (logChunkJson.items.length === 0)
|
|
88
118
|
return [3 /*break*/, 4];
|
|
89
119
|
logJson.items = __spreadArray(__spreadArray([], __read(logJson.items), false), __read(logChunkJson.items), false);
|
|
90
120
|
start += loglimit;
|
|
91
|
-
|
|
121
|
+
_a.label = 3;
|
|
92
122
|
case 3:
|
|
93
123
|
if (start < logCount) return [3 /*break*/, 1];
|
|
94
|
-
|
|
124
|
+
_a.label = 4;
|
|
95
125
|
case 4: return [2 /*return*/, logJson];
|
|
96
126
|
}
|
|
97
127
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
12
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
|
13
|
+
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
14
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
15
|
+
function step(op) {
|
|
16
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
17
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
18
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
19
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
20
|
+
switch (op[0]) {
|
|
21
|
+
case 0: case 1: t = op; break;
|
|
22
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
23
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
24
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
25
|
+
default:
|
|
26
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
27
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
28
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
29
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
30
|
+
if (t[2]) _.ops.pop();
|
|
31
|
+
_.trys.pop(); continue;
|
|
32
|
+
}
|
|
33
|
+
op = body.call(thisArg, _);
|
|
34
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
35
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
var fetchLogFileContent_1 = require("../fetchLogFileContent");
|
|
40
|
+
/**
|
|
41
|
+
* Two servers answer a log request, in two shapes: Viya with a paged JSON chunk
|
|
42
|
+
* (`{ items: [{ line }] }`), SASjs Server with the log itself. The fetcher has to
|
|
43
|
+
* read both, and the case that broke was the second - a log starting with a digit
|
|
44
|
+
* parsed as a number, so `items` was undefined.
|
|
45
|
+
*/
|
|
46
|
+
var makeSasjs = function (responses) {
|
|
47
|
+
var fetchMock = jest.fn();
|
|
48
|
+
responses.forEach(function (response) { return fetchMock.mockResolvedValueOnce(response); });
|
|
49
|
+
return {
|
|
50
|
+
sasjs: { fetchLogFileContent: fetchMock },
|
|
51
|
+
fetchMock: fetchMock
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
describe('fetchLogFileContent', function () {
|
|
55
|
+
it('pages the JSON chunks Viya returns', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
56
|
+
var _a, sasjs, fetchMock, log;
|
|
57
|
+
return __generator(this, function (_b) {
|
|
58
|
+
switch (_b.label) {
|
|
59
|
+
case 0:
|
|
60
|
+
_a = makeSasjs([
|
|
61
|
+
JSON.stringify({ items: [{ line: 'NOTE: one' }, { line: 'NOTE: two' }] })
|
|
62
|
+
]), sasjs = _a.sasjs, fetchMock = _a.fetchMock;
|
|
63
|
+
return [4 /*yield*/, (0, fetchLogFileContent_1.fetchLogFileContent)(sasjs, 'token', 'https://viya.example/compute/jobs/1/log', 2)];
|
|
64
|
+
case 1:
|
|
65
|
+
log = _b.sent();
|
|
66
|
+
expect(log.items).toEqual([{ line: 'NOTE: one' }, { line: 'NOTE: two' }]);
|
|
67
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
68
|
+
expect(fetchMock).toHaveBeenCalledWith('https://viya.example/compute/jobs/1/log?start=0&limit=2', 'token');
|
|
69
|
+
return [2 /*return*/];
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}); });
|
|
73
|
+
it('stops when Viya answers with no further lines', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
74
|
+
var _a, sasjs, fetchMock, log;
|
|
75
|
+
return __generator(this, function (_b) {
|
|
76
|
+
switch (_b.label) {
|
|
77
|
+
case 0:
|
|
78
|
+
_a = makeSasjs([JSON.stringify({ items: [] })]), sasjs = _a.sasjs, fetchMock = _a.fetchMock;
|
|
79
|
+
return [4 /*yield*/, (0, fetchLogFileContent_1.fetchLogFileContent)(sasjs, 'token', 'log', 50000)];
|
|
80
|
+
case 1:
|
|
81
|
+
log = _b.sent();
|
|
82
|
+
expect(log.items).toEqual([]);
|
|
83
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
84
|
+
return [2 /*return*/];
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}); });
|
|
88
|
+
it('takes the whole body when it is not a JSON chunk', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
89
|
+
var body, _a, sasjs, fetchMock, log;
|
|
90
|
+
return __generator(this, function (_b) {
|
|
91
|
+
switch (_b.label) {
|
|
92
|
+
case 0:
|
|
93
|
+
body = '1 The SAS System\nNOTE: hello\nERROR: something wrong\n';
|
|
94
|
+
_a = makeSasjs([body]), sasjs = _a.sasjs, fetchMock = _a.fetchMock;
|
|
95
|
+
return [4 /*yield*/, (0, fetchLogFileContent_1.fetchLogFileContent)(sasjs, 'token', 'https://sas.4gl.io/SASjsApi/session/1/log', 4)];
|
|
96
|
+
case 1:
|
|
97
|
+
log = _b.sent();
|
|
98
|
+
expect(log.items).toEqual([{ line: body }]);
|
|
99
|
+
// and it is not asked again: the text is the whole log, not a page of it
|
|
100
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
101
|
+
return [2 /*return*/];
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}); });
|
|
105
|
+
it('unwraps a log the adapter has stringified', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
106
|
+
var text, sasjs, log;
|
|
107
|
+
return __generator(this, function (_a) {
|
|
108
|
+
switch (_a.label) {
|
|
109
|
+
case 0:
|
|
110
|
+
text = '1 The SAS System\nERROR: 22-322: Syntax error\n';
|
|
111
|
+
sasjs = makeSasjs([JSON.stringify(text)]).sasjs;
|
|
112
|
+
return [4 /*yield*/, (0, fetchLogFileContent_1.fetchLogFileContent)(sasjs, 'token', 'log', 2)];
|
|
113
|
+
case 1:
|
|
114
|
+
log = _a.sent();
|
|
115
|
+
expect(log.items).toEqual([{ line: text }]);
|
|
116
|
+
return [2 /*return*/];
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}); });
|
|
120
|
+
it('treats an empty body as an empty log', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
121
|
+
var sasjs, _a;
|
|
122
|
+
return __generator(this, function (_b) {
|
|
123
|
+
switch (_b.label) {
|
|
124
|
+
case 0:
|
|
125
|
+
sasjs = makeSasjs(['']).sasjs;
|
|
126
|
+
_a = expect;
|
|
127
|
+
return [4 /*yield*/, (0, fetchLogFileContent_1.fetchLogFileContent)(sasjs, 'token', 'log', 0)];
|
|
128
|
+
case 1:
|
|
129
|
+
_a.apply(void 0, [(_b.sent()).items]).toEqual([{ line: '' }]);
|
|
130
|
+
return [2 /*return*/];
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}); });
|
|
134
|
+
});
|
package/package.json
CHANGED
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"@sasjs/adapter": "^5.1.3",
|
|
74
74
|
"@sasjs/core": "^6.1.1",
|
|
75
75
|
"@sasjs/lint": "4.9.0",
|
|
76
|
-
"@sasjs/utils": "^4.
|
|
76
|
+
"@sasjs/utils": "^4.4.0",
|
|
77
77
|
"axios": "1.20.0",
|
|
78
78
|
"chalk": "4.1.2",
|
|
79
79
|
"dotenv": "18.0.8",
|
|
@@ -123,5 +123,5 @@
|
|
|
123
123
|
"ws": "8.21.0",
|
|
124
124
|
"nanoid": "3.3.18"
|
|
125
125
|
},
|
|
126
|
-
"version": "6.
|
|
126
|
+
"version": "6.9.0"
|
|
127
127
|
}
|