@sasjs/cli 6.19.3 → 6.19.4

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.
@@ -575,6 +575,19 @@ const payloadTooLarge = (limit) => {
575
575
  */
576
576
  const readJsonBody = async (request) => {
577
577
  const body = await readJson(request)
578
+
579
+ // The bridge forwards a JSON body as `json` - a request the page made with
580
+ // `JSON.stringify` has no fields for the envelope it wraps everything in, so
581
+ // the payload is the whole point of the message. Reading only `params` here
582
+ // dropped it: `code/execute` and `code/lint` arrived with no code.
583
+ if (
584
+ body &&
585
+ typeof body === 'object' &&
586
+ body.json &&
587
+ typeof body.json === 'object'
588
+ )
589
+ return { body: body.json, files: [] }
590
+
578
591
  if (
579
592
  body &&
580
593
  typeof body === 'object' &&
@@ -988,8 +1001,21 @@ const routeRequest = async (request, url) => {
988
1001
  return json(answer.body, answer.status)
989
1002
  } catch (e) {
990
1003
  const message = (e && e.stack) || String(e)
991
- logEntry(`${request.method} ${path} ERROR ${(e && e.message) || message}`)
992
- broadcast({ type: 'sasjs-host-error', path, message })
1004
+ const status = (e && e.status) || 500
1005
+ const refusal = status >= 400 && status < 500
1006
+
1007
+ // A 4xx is an ANSWER: the API decided, the caller was told, and a client
1008
+ // that expected it (Studio creates its default program and accepts the 409)
1009
+ // has already handled it. Broadcasting it put four console errors on a page
1010
+ // whose only fault was opening a file that was already there. A 5xx is the
1011
+ // host failing, and that is worth reporting to the page.
1012
+ logEntry(
1013
+ `${request.method} ${path} ${refusal ? 'refused' : 'ERROR'} ${
1014
+ (e && e.message) || message
1015
+ }`
1016
+ )
1017
+
1018
+ if (!refusal) broadcast({ type: 'sasjs-host-error', path, message })
993
1019
  // A refusal the API decided on carries its status and, where the route
994
1020
  // answers an envelope, the body to send. The drive's routes send the message
995
1021
  // itself as plain text - the server answers them with `res.status(code)
@@ -115,9 +115,14 @@ var loadWorker = function (base) {
115
115
  handlers[type].push(fn);
116
116
  },
117
117
  skipWaiting: function () { },
118
- clients: { claim: function () { }, matchAll: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
119
- return [2 /*return*/, []];
120
- }); }); } }
118
+ // a test can put a client here to observe what the host broadcasts; with
119
+ // none, a message reaches nobody and nothing can be asserted about it
120
+ clients: {
121
+ claim: function () { },
122
+ matchAll: function () { return __awaiter(void 0, void 0, void 0, function () { var _a; return __generator(this, function (_b) {
123
+ return [2 /*return*/, (_a = global.__mockHostClients) !== null && _a !== void 0 ? _a : []];
124
+ }); }); }
125
+ }
121
126
  };
122
127
  global.self = self;
123
128
  global.importScripts = function (path) {
@@ -1233,6 +1238,83 @@ describe('service worker host', function () {
1233
1238
  }
1234
1239
  });
1235
1240
  }); });
1241
+ /**
1242
+ * A refusal is the API's answer, and the page that asked gets it. Reporting it
1243
+ * as a host error put console errors on screens that had handled the refusal
1244
+ * themselves - Studio's 409 being the one that started this.
1245
+ */
1246
+ it('reports a host failure to the page, and stays quiet about a refusal', function () { return __awaiter(void 0, void 0, void 0, function () {
1247
+ var worker, messages, write, hostErrors, failed, _a, _b;
1248
+ return __generator(this, function (_c) {
1249
+ switch (_c.label) {
1250
+ case 0:
1251
+ worker = seeded();
1252
+ messages = [];
1253
+ global.__mockHostClients = [
1254
+ { postMessage: function (message) { return messages.push(message); } }
1255
+ ];
1256
+ write = function () {
1257
+ var form = new FormData();
1258
+ form.set('file', new Blob(['x']), 'new.txt');
1259
+ return worker.dispatch('/SASjsApi/drive/file?_filePath=' +
1260
+ encodeURIComponent('/Public/new.txt'), { method: 'POST', body: form });
1261
+ };
1262
+ hostErrors = function () {
1263
+ return messages.filter(function (message) { return message.type === 'sasjs-host-error'; }).length;
1264
+ };
1265
+ // a drive that cannot be written: a failure of the host, not an answer
1266
+ worker.refuseWrites(true);
1267
+ return [4 /*yield*/, write()];
1268
+ case 1:
1269
+ failed = _c.sent();
1270
+ expect(failed.status).toBeGreaterThanOrEqual(500);
1271
+ expect(hostErrors()).toBe(1);
1272
+ // a create over a file that is already there: the API answered, and the
1273
+ // caller handles it
1274
+ messages.length = 0;
1275
+ worker.refuseWrites(false);
1276
+ _a = expect;
1277
+ return [4 /*yield*/, write()];
1278
+ case 2:
1279
+ _a.apply(void 0, [(_c.sent()).status]).toBe(200);
1280
+ _b = expect;
1281
+ return [4 /*yield*/, write()];
1282
+ case 3:
1283
+ _b.apply(void 0, [(_c.sent()).status]).toBe(409);
1284
+ expect(hostErrors()).toBe(0);
1285
+ delete global.__mockHostClients;
1286
+ return [2 /*return*/];
1287
+ }
1288
+ });
1289
+ }); });
1290
+ it('keeps the payload of a JSON body the page bridge forwarded', function () { return __awaiter(void 0, void 0, void 0, function () {
1291
+ var dispatch, body, response, findings;
1292
+ return __generator(this, function (_a) {
1293
+ switch (_a.label) {
1294
+ case 0:
1295
+ dispatch = seeded().dispatch;
1296
+ body = JSON.stringify({
1297
+ params: {},
1298
+ tables: [],
1299
+ json: { code: 'data _null_;\n x = 1; \nrun;\n', filePath: '/a.sas' }
1300
+ });
1301
+ return [4 /*yield*/, dispatch('/SASjsApi/code/lint', {
1302
+ method: 'POST',
1303
+ headers: { 'content-type': 'application/json' },
1304
+ body: body
1305
+ })];
1306
+ case 1:
1307
+ response = _a.sent();
1308
+ expect(response.status).toBe(200);
1309
+ return [4 /*yield*/, response.json()];
1310
+ case 2:
1311
+ findings = _a.sent();
1312
+ expect(Array.isArray(findings)).toBe(true);
1313
+ expect(JSON.stringify(findings)).toContain('trailing spaces');
1314
+ return [2 /*return*/];
1315
+ }
1316
+ });
1317
+ }); });
1236
1318
  it('answers 404 for a drive file the drive does not hold', function () { return __awaiter(void 0, void 0, void 0, function () {
1237
1319
  var dispatch, response, _a;
1238
1320
  return __generator(this, function (_b) {
package/package.json CHANGED
@@ -125,5 +125,5 @@
125
125
  "ws": "8.21.0",
126
126
  "nanoid": "3.3.18"
127
127
  },
128
- "version": "6.19.3"
128
+ "version": "6.19.4"
129
129
  }