@sasjs/cli 6.8.3 → 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.
@@ -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
- if (_webout) { fs.writeFile(weboutPath, _webout, function () {}); }
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 e_2, _c;
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 (e_2_1) { e_2 = { error: e_2_1 }; }
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 (e_2) throw e_2.error; }
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, e_3_1;
469
- var e_3, _c;
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
- e_3_1 = _d.sent();
501
- e_3 = { error: e_3_1 };
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 (e_3) throw e_3.error; }
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({
@@ -67,8 +67,17 @@ var FlowSubCommand;
67
67
  })(FlowSubCommand || (FlowSubCommand = {}));
68
68
  var syntax = 'flow <subCommand> [options]';
69
69
  var usage = 'sasjs flow <execute> --source <flowPath> [options]';
70
- var description = 'Performs operations on flows of jobs.\n' +
71
- 'Exits non-zero when a flow does not complete: 6 when a job did not succeed, 2 when the flow could not be executed at all.';
70
+ // the codes are read from the enum rather than written out, so a member
71
+ // inserted ahead of them cannot make this text describe numbers the command no
72
+ // longer returns. A function rather than a constant: `types/command/index.ts`
73
+ // re-exports `parse` before `ReturnCode`, and `parse` loads every command, so
74
+ // reading the enum while this module initialises finds it undefined.
75
+ var commandDescription = function () {
76
+ return 'Performs operations on flows of jobs.\n' +
77
+ "Exits ".concat(command_1.ReturnCode.FlowError, " when a flow finishes with a job that did not ") +
78
+ "succeed, and ".concat(command_1.ReturnCode.InternalError, " when the flow could not be executed ") +
79
+ 'at all.';
80
+ };
72
81
  var examples = [
73
82
  {
74
83
  command: 'sasjs flow execute --source /local/flow.json --logFolder /local/log/folder --csvFile /local/some.csv --target myTarget --streamLog',
@@ -114,7 +123,7 @@ var FlowCommand = /** @class */ (function (_super) {
114
123
  return _super.call(this, args, {
115
124
  parseOptions: parseOptions,
116
125
  usage: usage,
117
- description: description,
126
+ description: commandDescription(),
118
127
  examples: examples,
119
128
  syntax: syntax
120
129
  }) || this;
@@ -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, logChunkJson, _a, _b;
75
- return __generator(this, function (_c) {
76
- switch (_c.label) {
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
- _c.label = 1;
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
- logChunkJson = _b.apply(_a, [(_c.sent())]);
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
- _c.label = 3;
121
+ _a.label = 3;
92
122
  case 3:
93
123
  if (start < logCount) return [3 /*break*/, 1];
94
- _c.label = 4;
124
+ _a.label = 4;
95
125
  case 4: return [2 /*return*/, logJson];
96
126
  }
97
127
  });
@@ -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.3.2",
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.8.3"
126
+ "version": "6.9.0"
127
127
  }