@sasjs/cli 6.19.3 → 6.20.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.
@@ -125,8 +125,11 @@
125
125
  xhr.dispatchEvent(new ProgressEvent('loadend'))
126
126
  }
127
127
 
128
+ // `/api` is the short spelling of the API and `/SASjsApi` the canonical one:
129
+ // the build bridges both, so a caller that uses either reaches the mock.
128
130
  const isSasjs = (url) =>
129
- typeof url === 'string' && url.indexOf('/SASjsApi/') >= 0
131
+ typeof url === 'string' &&
132
+ (url.indexOf('/SASjsApi/') >= 0 || url.indexOf('/api/') >= 0)
130
133
 
131
134
  const proto = XMLHttpRequest.prototype
132
135
  const nativeOpen = proto.open
@@ -327,6 +327,15 @@ const RUNTIME = new BrowserRuntime()
327
327
  */
328
328
  const BASE = (self.location.pathname || '').replace(/\/sw\.js.*$/, '')
329
329
 
330
+ /**
331
+ * The short spelling of the App Stream landing page.
332
+ *
333
+ * It is the default: the interface links to `/app`, and sasjs/server serves it
334
+ * there while keeping `/AppStream` as a redirect. The worker mirrors that, so a
335
+ * link the interface builds reaches the same page on the static demo.
336
+ */
337
+ const SHORT_APP_STREAM_PATH = '/app'
338
+
330
339
  const withoutBase = (pathname) =>
331
340
  BASE && pathname.startsWith(BASE)
332
341
  ? pathname.slice(BASE.length) || '/'
@@ -575,6 +584,19 @@ const payloadTooLarge = (limit) => {
575
584
  */
576
585
  const readJsonBody = async (request) => {
577
586
  const body = await readJson(request)
587
+
588
+ // The bridge forwards a JSON body as `json` - a request the page made with
589
+ // `JSON.stringify` has no fields for the envelope it wraps everything in, so
590
+ // the payload is the whole point of the message. Reading only `params` here
591
+ // dropped it: `code/execute` and `code/lint` arrived with no code.
592
+ if (
593
+ body &&
594
+ typeof body === 'object' &&
595
+ body.json &&
596
+ typeof body.json === 'object'
597
+ )
598
+ return { body: body.json, files: [] }
599
+
578
600
  if (
579
601
  body &&
580
602
  typeof body === 'object' &&
@@ -876,6 +898,11 @@ const contentsResponse = (answer, path) => {
876
898
 
877
899
  const routeRequest = async (request, url) => {
878
900
  const path = withoutBase(url.pathname).replace(/\/+$/, '') || '/'
901
+ // `/api` is the short spelling of the API, and `/SASjsApi` the canonical one:
902
+ // both are answered, so the alias is stripped once here and everything below -
903
+ // the route lookup, the drive routes' plain-text refusals, the log - works
904
+ // from one path.
905
+ const apiPath = path.replace(/^\/api(?=\/|$)/i, '/SASjsApi')
879
906
  const query = {}
880
907
  for (const [k, v] of url.searchParams.entries()) query[k] = v
881
908
 
@@ -888,27 +915,31 @@ const routeRequest = async (request, url) => {
888
915
  // every visit. Unanswered, those requests reach the static host and the page
889
916
  // parses this site's index.html as JSON.
890
917
  //
891
- // The answers mirror the server's, because a caller cannot tell the two
892
- // hosts apart otherwise: `/AppStream` redirects to `/AppStream/`, the landing
893
- // page is served, and only a name under it is missing. The listing is empty
894
- // rather than read from `registry()`, which records this deployment's own
895
- // folder so the delete route has something to remove: the stream really does
896
- // serve nothing at `/AppStream/<name>/` here, and a listing that named it
897
- // would offer a tile that 404s.
898
- // `path` has its trailing slashes stripped, and these four answers differ by
899
- // exactly that, so they are decided on the request's own pathname - minus
900
- // the folder the site is served from, which the four patterns do not name.
918
+ // `/app` is the spelling the interface links to and the server serves; the
919
+ // canonical `/AppStream` still answers, because a client built before the
920
+ // short paths existed asks for it. The answers are the same either way, so
921
+ // `/AppStream` redirects to `/app` - exactly as the server redirects it -
922
+ // and the landing page, the listing and the missing-name refusal are decided
923
+ // on both.
924
+ //
925
+ // The listing is empty rather than read from `registry()`, which records this
926
+ // deployment's own folder so the delete route has something to remove: the
927
+ // stream really does serve nothing at `/app/<name>/` here, and a listing that
928
+ // named it would offer a tile that 404s.
929
+ // `path` has its trailing slashes stripped, and the landing answers differ by
930
+ // exactly that, so they are decided on the request's own pathname - minus the
931
+ // folder the site is served from, which the patterns do not name.
901
932
  const stream = withoutBase(url.pathname)
902
933
 
903
934
  if (/^\/AppStream$/i.test(stream))
904
935
  return new Response(null, {
905
936
  status: 301,
906
- headers: { location: `${BASE}/AppStream/` }
937
+ headers: { location: `${BASE}${SHORT_APP_STREAM_PATH}` }
907
938
  })
908
939
 
909
940
  if (
910
- /^\/AppStream\/$/i.test(stream) ||
911
- /^\/AppStream\/index\.html$/i.test(stream)
941
+ /^\/(app|AppStream)\/?$/i.test(stream) ||
942
+ /^\/(app|AppStream)\/index\.html$/i.test(stream)
912
943
  )
913
944
  return new Response(
914
945
  [
@@ -921,9 +952,10 @@ const routeRequest = async (request, url) => {
921
952
  { status: 200, headers: { 'content-type': 'text/html; charset=utf-8' } }
922
953
  )
923
954
 
924
- if (/^\/AppStream\/apps\.json$/i.test(stream)) return json({ apps: [] })
955
+ if (/^\/(app|AppStream)\/apps\.json$/i.test(stream))
956
+ return json({ apps: [] })
925
957
 
926
- if (/^\/AppStream\//i.test(stream)) {
958
+ if (/^\/(app|AppStream)\//i.test(stream)) {
927
959
  return json(
928
960
  {
929
961
  message:
@@ -935,8 +967,9 @@ const routeRequest = async (request, url) => {
935
967
 
936
968
  // The package's own matcher, so a route is recognised however its path is
937
969
  // cased and its parameters are read the way the document states them.
938
- const matched = API.findRoute(request.method, path)
939
- if (!matched) return json({ message: 'Not part of the API: ' + path }, 404)
970
+ const matched = API.findRoute(request.method, apiPath)
971
+ if (!matched)
972
+ return json({ message: 'Not part of the API: ' + apiPath }, 404)
940
973
 
941
974
  const routeId = matched.route.id
942
975
  const subject = await subjectOf(request, query)
@@ -988,8 +1021,21 @@ const routeRequest = async (request, url) => {
988
1021
  return json(answer.body, answer.status)
989
1022
  } catch (e) {
990
1023
  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 })
1024
+ const status = (e && e.status) || 500
1025
+ const refusal = status >= 400 && status < 500
1026
+
1027
+ // A 4xx is an ANSWER: the API decided, the caller was told, and a client
1028
+ // that expected it (Studio creates its default program and accepts the 409)
1029
+ // has already handled it. Broadcasting it put four console errors on a page
1030
+ // whose only fault was opening a file that was already there. A 5xx is the
1031
+ // host failing, and that is worth reporting to the page.
1032
+ logEntry(
1033
+ `${request.method} ${path} ${refusal ? 'refused' : 'ERROR'} ${
1034
+ (e && e.message) || message
1035
+ }`
1036
+ )
1037
+
1038
+ if (!refusal) broadcast({ type: 'sasjs-host-error', path, message })
993
1039
  // A refusal the API decided on carries its status and, where the route
994
1040
  // answers an envelope, the body to send. The drive's routes send the message
995
1041
  // itself as plain text - the server answers them with `res.status(code)
@@ -997,7 +1043,7 @@ const routeRequest = async (request, url) => {
997
1043
  // envelope the other routes already answer with.
998
1044
  if (API.isApiError(e)) {
999
1045
  if (e.body !== undefined) return json(e.body, e.status)
1000
- if (path.includes('/SASjsApi/drive/')) return text(e.message, e.status)
1046
+ if (apiPath.includes('/SASjsApi/drive/')) return text(e.message, e.status)
1001
1047
  return json({ message: e.message }, e.status)
1002
1048
  }
1003
1049
  return json(
@@ -1053,10 +1099,17 @@ self.addEventListener('fetch', (event) => {
1053
1099
  // The host's own paths are answered here, including one the table does not
1054
1100
  // have - falling through to the static host would put the SPA's index.html
1055
1101
  // behind a typo in a caller's URL, or behind a listing an interface parses as
1056
- // JSON. `/AppStream` is one of those: the package serves app deployments, but
1057
- // the listing is the server's web route, and an interface that lists apps asks
1058
- // for it on every visit.
1059
- if (!/^\/(SASjsApi|SASLogon|AppStream)(\/|$)/i.test(path)) return
1102
+ // JSON. `/app` is one of those: the package serves app deployments, but the
1103
+ // listing is the server's web route, and an interface that lists apps asks for
1104
+ // it on every visit.
1105
+ //
1106
+ // `/api` and `/api/` on their own are the API's documentation page - a file
1107
+ // the build copies into the site - so the worker stays out of the way there
1108
+ // and the static host answers it. Every other `/api/...` is an API call on the
1109
+ // short spelling, which routeRequest canonicalises to `/SASjsApi`.
1110
+ if (/^\/api\/?$/i.test(path)) return
1111
+
1112
+ if (!/^\/(SASjsApi|SASLogon|AppStream|api|app)(\/|$)/i.test(path)) return
1060
1113
  event.respondWith(handle(event.request, url))
1061
1114
  })
1062
1115
 
@@ -173,13 +173,34 @@ describe('page-side bridge', function () {
173
173
  fetchImpl = jest.fn();
174
174
  XHR = loadBridge(fetchImpl).XHR;
175
175
  xhr = new XHR();
176
- xhr.open('GET', '/api/health');
176
+ xhr.open('GET', '/health');
177
177
  xhr.send();
178
178
  expect(xhr.sentWith).toBeUndefined();
179
179
  expect(fetchImpl).not.toHaveBeenCalled();
180
180
  return [2 /*return*/];
181
181
  });
182
182
  }); });
183
+ it('bridges the short API spelling as well as the canonical one', function () { return __awaiter(void 0, void 0, void 0, function () {
184
+ var fetchImpl, XHR, xhr, _a, url;
185
+ return __generator(this, function (_b) {
186
+ switch (_b.label) {
187
+ case 0:
188
+ fetchImpl = jest.fn(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
189
+ return [2 /*return*/, jsonResponse('{"status":"ok"}')];
190
+ }); }); });
191
+ XHR = loadBridge(fetchImpl).XHR;
192
+ xhr = new XHR();
193
+ return [4 /*yield*/, run(xhr, 'GET', '/api/info')];
194
+ case 1:
195
+ _b.sent();
196
+ _a = __read(fetchImpl.mock.calls[0], 1), url = _a[0];
197
+ expect(url).toBe('/api/info');
198
+ expect(xhr.status).toBe(200);
199
+ expect(xhr.responseText).toBe('{"status":"ok"}');
200
+ return [2 /*return*/];
201
+ }
202
+ });
203
+ }); });
183
204
  it('converts a multipart body to JSON and replays the response onto the XHR', function () { return __awaiter(void 0, void 0, void 0, function () {
184
205
  var fetchImpl, XHR, form, xhr, _a, init;
185
206
  return __generator(this, function (_b) {
@@ -286,7 +307,7 @@ describe('page-side bridge', function () {
286
307
  expect(second.XHR.prototype).not.toBe(first.XHR.prototype);
287
308
  expect(Object.getPrototypeOf(second.XHR.prototype)).toBe(FakeXHR.prototype);
288
309
  var xhr = new second.XHR();
289
- xhr.open('GET', '/api/health');
310
+ xhr.open('GET', '/health');
290
311
  xhr.send();
291
312
  expect(xhr.nativeSendCount).toBe(1);
292
313
  });
@@ -35,6 +35,17 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
35
35
  if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
36
36
  }
37
37
  };
38
+ var __values = (this && this.__values) || function(o) {
39
+ var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
40
+ if (m) return m.call(o);
41
+ if (o && typeof o.length === "number") return {
42
+ next: function () {
43
+ if (o && i >= o.length) o = void 0;
44
+ return { value: o && o[i++], done: !o };
45
+ }
46
+ };
47
+ throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
48
+ };
38
49
  var __read = (this && this.__read) || function (o, n) {
39
50
  var m = typeof Symbol === "function" && o[Symbol.iterator];
40
51
  if (!m) return o;
@@ -51,17 +62,6 @@ var __read = (this && this.__read) || function (o, n) {
51
62
  }
52
63
  return ar;
53
64
  };
54
- var __values = (this && this.__values) || function(o) {
55
- var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
56
- if (m) return m.call(o);
57
- if (o && typeof o.length === "number") return {
58
- next: function () {
59
- if (o && i >= o.length) o = void 0;
60
- return { value: o && o[i++], done: !o };
61
- }
62
- };
63
- throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
64
- };
65
65
  var __importDefault = (this && this.__importDefault) || function (mod) {
66
66
  return (mod && mod.__esModule) ? mod : { "default": mod };
67
67
  };
@@ -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) {
@@ -893,51 +898,59 @@ describe('service worker host', function () {
893
898
  });
894
899
  }); });
895
900
  it('answers every route under the folder the site is published in', function () { return __awaiter(void 0, void 0, void 0, function () {
896
- var worker, info, _a, page, folder, stream, landing, apps, _b;
897
- var _c;
898
- return __generator(this, function (_d) {
899
- switch (_d.label) {
901
+ var worker, info, _a, page, folder, stream, landing, apps, _b, aliased, _c;
902
+ var _d;
903
+ return __generator(this, function (_e) {
904
+ switch (_e.label) {
900
905
  case 0:
901
906
  worker = loadWorker('/demo');
902
- worker.runtime.load((_c = {},
903
- _c[drivePath('services/public/getdata.js')] = '_webout = "seeded";',
904
- _c));
907
+ worker.runtime.load((_d = {},
908
+ _d[drivePath('services/public/getdata.js')] = '_webout = "seeded";',
909
+ _d));
905
910
  return [4 /*yield*/, worker.dispatch('/demo/SASjsApi/info')];
906
911
  case 1:
907
- info = _d.sent();
912
+ info = _e.sent();
908
913
  expect(info.status).toBe(200);
909
914
  _a = expect;
910
915
  return [4 /*yield*/, info.json()];
911
916
  case 2:
912
- _a.apply(void 0, [(_d.sent()).mode]).toBe('desktop');
917
+ _a.apply(void 0, [(_e.sent()).mode]).toBe('desktop');
913
918
  return [4 /*yield*/, worker.dispatch('/demo/')];
914
919
  case 3:
915
- page = _d.sent();
920
+ page = _e.sent();
916
921
  expect(page.status).toBe(200);
917
922
  expect(page.headers.get('content-type')).toContain('text/html');
918
923
  return [4 /*yield*/, worker.dispatch('/demo')];
919
924
  case 4:
920
- folder = _d.sent();
925
+ folder = _e.sent();
921
926
  expect(folder.status).toBe(200);
922
927
  expect(folder.headers.get('content-type')).toContain('text/html');
923
928
  return [4 /*yield*/, worker.dispatch('/demo/AppStream')];
924
929
  case 5:
925
- stream = _d.sent();
930
+ stream = _e.sent();
926
931
  expect(stream.status).toBe(301);
927
- expect(stream.headers.get('location')).toBe('/demo/AppStream/');
928
- return [4 /*yield*/, worker.dispatch('/demo/AppStream/')];
932
+ expect(stream.headers.get('location')).toBe('/demo/app');
933
+ return [4 /*yield*/, worker.dispatch('/demo/app')];
929
934
  case 6:
930
- landing = _d.sent();
935
+ landing = _e.sent();
931
936
  expect(landing.status).toBe(200);
932
937
  expect(landing.headers.get('content-type')).toContain('text/html');
933
- return [4 /*yield*/, worker.dispatch('/demo/AppStream/apps.json')];
938
+ return [4 /*yield*/, worker.dispatch('/demo/app/apps.json')];
934
939
  case 7:
935
- apps = _d.sent();
940
+ apps = _e.sent();
936
941
  expect(apps.status).toBe(200);
937
942
  _b = expect;
938
943
  return [4 /*yield*/, apps.json()];
939
944
  case 8:
940
- _b.apply(void 0, [_d.sent()]).toEqual({ apps: [] });
945
+ _b.apply(void 0, [_e.sent()]).toEqual({ apps: [] });
946
+ return [4 /*yield*/, worker.dispatch('/demo/api/info')];
947
+ case 9:
948
+ aliased = _e.sent();
949
+ expect(aliased.status).toBe(200);
950
+ _c = expect;
951
+ return [4 /*yield*/, aliased.json()];
952
+ case 10:
953
+ _c.apply(void 0, [(_e.sent()).mode]).toBe('desktop');
941
954
  // a path outside the folder is not the site's to answer
942
955
  expect(worker.dispatch('/other/SASjsApi/info')).toBeUndefined();
943
956
  return [2 /*return*/];
@@ -1233,6 +1246,83 @@ describe('service worker host', function () {
1233
1246
  }
1234
1247
  });
1235
1248
  }); });
1249
+ /**
1250
+ * A refusal is the API's answer, and the page that asked gets it. Reporting it
1251
+ * as a host error put console errors on screens that had handled the refusal
1252
+ * themselves - Studio's 409 being the one that started this.
1253
+ */
1254
+ it('reports a host failure to the page, and stays quiet about a refusal', function () { return __awaiter(void 0, void 0, void 0, function () {
1255
+ var worker, messages, write, hostErrors, failed, _a, _b;
1256
+ return __generator(this, function (_c) {
1257
+ switch (_c.label) {
1258
+ case 0:
1259
+ worker = seeded();
1260
+ messages = [];
1261
+ global.__mockHostClients = [
1262
+ { postMessage: function (message) { return messages.push(message); } }
1263
+ ];
1264
+ write = function () {
1265
+ var form = new FormData();
1266
+ form.set('file', new Blob(['x']), 'new.txt');
1267
+ return worker.dispatch('/SASjsApi/drive/file?_filePath=' +
1268
+ encodeURIComponent('/Public/new.txt'), { method: 'POST', body: form });
1269
+ };
1270
+ hostErrors = function () {
1271
+ return messages.filter(function (message) { return message.type === 'sasjs-host-error'; }).length;
1272
+ };
1273
+ // a drive that cannot be written: a failure of the host, not an answer
1274
+ worker.refuseWrites(true);
1275
+ return [4 /*yield*/, write()];
1276
+ case 1:
1277
+ failed = _c.sent();
1278
+ expect(failed.status).toBeGreaterThanOrEqual(500);
1279
+ expect(hostErrors()).toBe(1);
1280
+ // a create over a file that is already there: the API answered, and the
1281
+ // caller handles it
1282
+ messages.length = 0;
1283
+ worker.refuseWrites(false);
1284
+ _a = expect;
1285
+ return [4 /*yield*/, write()];
1286
+ case 2:
1287
+ _a.apply(void 0, [(_c.sent()).status]).toBe(200);
1288
+ _b = expect;
1289
+ return [4 /*yield*/, write()];
1290
+ case 3:
1291
+ _b.apply(void 0, [(_c.sent()).status]).toBe(409);
1292
+ expect(hostErrors()).toBe(0);
1293
+ delete global.__mockHostClients;
1294
+ return [2 /*return*/];
1295
+ }
1296
+ });
1297
+ }); });
1298
+ it('keeps the payload of a JSON body the page bridge forwarded', function () { return __awaiter(void 0, void 0, void 0, function () {
1299
+ var dispatch, body, response, findings;
1300
+ return __generator(this, function (_a) {
1301
+ switch (_a.label) {
1302
+ case 0:
1303
+ dispatch = seeded().dispatch;
1304
+ body = JSON.stringify({
1305
+ params: {},
1306
+ tables: [],
1307
+ json: { code: 'data _null_;\n x = 1; \nrun;\n', filePath: '/a.sas' }
1308
+ });
1309
+ return [4 /*yield*/, dispatch('/SASjsApi/code/lint', {
1310
+ method: 'POST',
1311
+ headers: { 'content-type': 'application/json' },
1312
+ body: body
1313
+ })];
1314
+ case 1:
1315
+ response = _a.sent();
1316
+ expect(response.status).toBe(200);
1317
+ return [4 /*yield*/, response.json()];
1318
+ case 2:
1319
+ findings = _a.sent();
1320
+ expect(Array.isArray(findings)).toBe(true);
1321
+ expect(JSON.stringify(findings)).toContain('trailing spaces');
1322
+ return [2 /*return*/];
1323
+ }
1324
+ });
1325
+ }); });
1236
1326
  it('answers 404 for a drive file the drive does not hold', function () { return __awaiter(void 0, void 0, void 0, function () {
1237
1327
  var dispatch, response, _a;
1238
1328
  return __generator(this, function (_b) {
@@ -1251,42 +1341,157 @@ describe('service worker host', function () {
1251
1341
  }
1252
1342
  });
1253
1343
  }); });
1254
- it('answers the app stream as the server does, with nothing in it', function () { return __awaiter(void 0, void 0, void 0, function () {
1255
- var dispatch, listing, _a, root, landing, _b, app, _c;
1256
- return __generator(this, function (_d) {
1257
- switch (_d.label) {
1344
+ it('answers the app stream on the short path, and the canonical one as an alias', function () { return __awaiter(void 0, void 0, void 0, function () {
1345
+ var dispatch, _a, _b, path_2, listing, _c, e_1_1, _d, _e, path_3, landing, _f, e_2_1, root, canonicalLanding, _g, _h, _j, path_4, app, _k, e_3_1;
1346
+ var e_1, _l, e_2, _m, e_3, _o;
1347
+ return __generator(this, function (_p) {
1348
+ switch (_p.label) {
1258
1349
  case 0:
1259
1350
  dispatch = seeded().dispatch;
1260
- return [4 /*yield*/, dispatch('/AppStream/apps.json')];
1351
+ _p.label = 1;
1261
1352
  case 1:
1262
- listing = _d.sent();
1263
- expect(listing.status).toBe(200);
1264
- _a = expect;
1265
- return [4 /*yield*/, listing.json()];
1353
+ _p.trys.push([1, 7, 8, 9]);
1354
+ _a = __values(['/app/apps.json', '/AppStream/apps.json']), _b = _a.next();
1355
+ _p.label = 2;
1266
1356
  case 2:
1267
- _a.apply(void 0, [_d.sent()]).toEqual({ apps: [] });
1268
- return [4 /*yield*/, dispatch('/AppStream')];
1357
+ if (!!_b.done) return [3 /*break*/, 6];
1358
+ path_2 = _b.value;
1359
+ return [4 /*yield*/, dispatch(path_2)];
1269
1360
  case 3:
1270
- root = _d.sent();
1271
- expect(root.status).toBe(301);
1272
- expect(root.headers.get('location')).toBe('/AppStream/');
1273
- return [4 /*yield*/, dispatch('/AppStream/')];
1361
+ listing = _p.sent();
1362
+ expect(listing.status).toBe(200);
1363
+ _c = expect;
1364
+ return [4 /*yield*/, listing.json()];
1274
1365
  case 4:
1275
- landing = _d.sent();
1366
+ _c.apply(void 0, [_p.sent()]).toEqual({ apps: [] });
1367
+ _p.label = 5;
1368
+ case 5:
1369
+ _b = _a.next();
1370
+ return [3 /*break*/, 2];
1371
+ case 6: return [3 /*break*/, 9];
1372
+ case 7:
1373
+ e_1_1 = _p.sent();
1374
+ e_1 = { error: e_1_1 };
1375
+ return [3 /*break*/, 9];
1376
+ case 8:
1377
+ try {
1378
+ if (_b && !_b.done && (_l = _a.return)) _l.call(_a);
1379
+ }
1380
+ finally { if (e_1) throw e_1.error; }
1381
+ return [7 /*endfinally*/];
1382
+ case 9:
1383
+ _p.trys.push([9, 15, 16, 17]);
1384
+ _d = __values(['/app', '/app/', '/app/index.html']), _e = _d.next();
1385
+ _p.label = 10;
1386
+ case 10:
1387
+ if (!!_e.done) return [3 /*break*/, 14];
1388
+ path_3 = _e.value;
1389
+ return [4 /*yield*/, dispatch(path_3)];
1390
+ case 11:
1391
+ landing = _p.sent();
1276
1392
  expect(landing.status).toBe(200);
1277
1393
  expect(landing.headers.get('content-type')).toContain('text/html');
1278
- _b = expect;
1394
+ _f = expect;
1279
1395
  return [4 /*yield*/, landing.text()];
1280
- case 5:
1281
- _b.apply(void 0, [_d.sent()]).toContain('serves no apps');
1282
- return [4 /*yield*/, dispatch('/AppStream/Demo/')];
1283
- case 6:
1284
- app = _d.sent();
1396
+ case 12:
1397
+ _f.apply(void 0, [_p.sent()]).toContain('serves no apps');
1398
+ _p.label = 13;
1399
+ case 13:
1400
+ _e = _d.next();
1401
+ return [3 /*break*/, 10];
1402
+ case 14: return [3 /*break*/, 17];
1403
+ case 15:
1404
+ e_2_1 = _p.sent();
1405
+ e_2 = { error: e_2_1 };
1406
+ return [3 /*break*/, 17];
1407
+ case 16:
1408
+ try {
1409
+ if (_e && !_e.done && (_m = _d.return)) _m.call(_d);
1410
+ }
1411
+ finally { if (e_2) throw e_2.error; }
1412
+ return [7 /*endfinally*/];
1413
+ case 17: return [4 /*yield*/, dispatch('/AppStream')];
1414
+ case 18:
1415
+ root = _p.sent();
1416
+ expect(root.status).toBe(301);
1417
+ expect(root.headers.get('location')).toBe('/app');
1418
+ return [4 /*yield*/, dispatch('/AppStream/')];
1419
+ case 19:
1420
+ canonicalLanding = _p.sent();
1421
+ expect(canonicalLanding.status).toBe(200);
1422
+ _g = expect;
1423
+ return [4 /*yield*/, canonicalLanding.text()];
1424
+ case 20:
1425
+ _g.apply(void 0, [_p.sent()]).toContain('serves no apps');
1426
+ _p.label = 21;
1427
+ case 21:
1428
+ _p.trys.push([21, 27, 28, 29]);
1429
+ _h = __values(['/app/Demo/', '/AppStream/Demo/']), _j = _h.next();
1430
+ _p.label = 22;
1431
+ case 22:
1432
+ if (!!_j.done) return [3 /*break*/, 26];
1433
+ path_4 = _j.value;
1434
+ return [4 /*yield*/, dispatch(path_4)];
1435
+ case 23:
1436
+ app = _p.sent();
1285
1437
  expect(app.status).toBe(404);
1286
- _c = expect;
1438
+ _k = expect;
1287
1439
  return [4 /*yield*/, app.json()];
1288
- case 7:
1289
- _c.apply(void 0, [(_d.sent()).message]).toContain('serves no apps');
1440
+ case 24:
1441
+ _k.apply(void 0, [(_p.sent()).message]).toContain('serves no apps');
1442
+ _p.label = 25;
1443
+ case 25:
1444
+ _j = _h.next();
1445
+ return [3 /*break*/, 22];
1446
+ case 26: return [3 /*break*/, 29];
1447
+ case 27:
1448
+ e_3_1 = _p.sent();
1449
+ e_3 = { error: e_3_1 };
1450
+ return [3 /*break*/, 29];
1451
+ case 28:
1452
+ try {
1453
+ if (_j && !_j.done && (_o = _h.return)) _o.call(_h);
1454
+ }
1455
+ finally { if (e_3) throw e_3.error; }
1456
+ return [7 /*endfinally*/];
1457
+ case 29: return [2 /*return*/];
1458
+ }
1459
+ });
1460
+ }); });
1461
+ it('answers the API on /api as well as on /SASjsApi', function () { return __awaiter(void 0, void 0, void 0, function () {
1462
+ var dispatch, canonical, short, _a, _b, _c, missing, _d;
1463
+ return __generator(this, function (_e) {
1464
+ switch (_e.label) {
1465
+ case 0:
1466
+ dispatch = seeded().dispatch;
1467
+ return [4 /*yield*/, dispatch('/SASjsApi/info')];
1468
+ case 1:
1469
+ canonical = _e.sent();
1470
+ return [4 /*yield*/, dispatch('/api/info')];
1471
+ case 2:
1472
+ short = _e.sent();
1473
+ expect(short.status).toBe(200);
1474
+ _b = expect;
1475
+ return [4 /*yield*/, short.json()];
1476
+ case 3:
1477
+ _c = (_a = _b.apply(void 0, [_e.sent()])).toEqual;
1478
+ return [4 /*yield*/, canonical.json()];
1479
+ case 4:
1480
+ _c.apply(_a, [_e.sent()]);
1481
+ return [4 /*yield*/, dispatch('/api/drive/file?_filePath=' + encodeURIComponent('/missing.txt'))];
1482
+ case 5:
1483
+ missing = _e.sent();
1484
+ expect(missing.status).toBe(404);
1485
+ expect(missing.headers.get('content-type')).toContain('text/plain');
1486
+ _d = expect;
1487
+ return [4 /*yield*/, missing.text()];
1488
+ case 6:
1489
+ _d.apply(void 0, [_e.sent()]).toBe("File doesn't exist.");
1490
+ // The documentation page is a file the build copies into the site, so the
1491
+ // worker leaves `/api` and `/api/` to the static host instead of answering
1492
+ // them as routes.
1493
+ expect(dispatch('/api')).toBeUndefined();
1494
+ expect(dispatch('/api/')).toBeUndefined();
1290
1495
  return [2 /*return*/];
1291
1496
  }
1292
1497
  });
@@ -2141,7 +2346,7 @@ describe('service worker host', function () {
2141
2346
  }); });
2142
2347
  });
2143
2348
  describe('conformance corpus', function () {
2144
- var e_1, _a;
2349
+ var e_4, _a;
2145
2350
  /**
2146
2351
  * The corpus is what "the same API" means: the cases @sasjs/api runs against
2147
2352
  * its own services, and against a live sasjs/server, run here against this
@@ -2150,7 +2355,7 @@ describe('service worker host', function () {
2150
2355
  * used to have - and it is a test rather than a review.
2151
2356
  */
2152
2357
  var httpFor = function (testCase) {
2153
- var e_2, _a;
2358
+ var e_5, _a;
2154
2359
  var _b;
2155
2360
  var _c = __read(testCase.route.split(' '), 2), method = _c[0], routePath = _c[1];
2156
2361
  var query = new URLSearchParams();
@@ -2161,12 +2366,12 @@ describe('service worker host', function () {
2161
2366
  query.set(key, String(value));
2162
2367
  }
2163
2368
  }
2164
- catch (e_2_1) { e_2 = { error: e_2_1 }; }
2369
+ catch (e_5_1) { e_5 = { error: e_5_1 }; }
2165
2370
  finally {
2166
2371
  try {
2167
2372
  if (_e && !_e.done && (_a = _d.return)) _a.call(_d);
2168
2373
  }
2169
- finally { if (e_2) throw e_2.error; }
2374
+ finally { if (e_5) throw e_5.error; }
2170
2375
  }
2171
2376
  var suffix = query.toString() ? "?".concat(query) : '';
2172
2377
  var init = { method: method };
@@ -2197,8 +2402,8 @@ describe('service worker host', function () {
2197
2402
  * does.
2198
2403
  */
2199
2404
  var seedDrive = function (runtime, testCase) { return __awaiter(void 0, void 0, void 0, function () {
2200
- var text, _a, _b, _c, file, contents, _d, _e, _f, file, contents, e_3_1;
2201
- var e_4, _g, e_3, _h;
2405
+ var text, _a, _b, _c, file, contents, _d, _e, _f, file, contents, e_6_1;
2406
+ var e_7, _g, e_6, _h;
2202
2407
  return __generator(this, function (_j) {
2203
2408
  switch (_j.label) {
2204
2409
  case 0:
@@ -2210,12 +2415,12 @@ describe('service worker host', function () {
2210
2415
  text["/sasjs/drive/files".concat(file)] = contents;
2211
2416
  }
2212
2417
  }
2213
- catch (e_4_1) { e_4 = { error: e_4_1 }; }
2418
+ catch (e_7_1) { e_7 = { error: e_7_1 }; }
2214
2419
  finally {
2215
2420
  try {
2216
2421
  if (_b && !_b.done && (_g = _a.return)) _g.call(_a);
2217
2422
  }
2218
- finally { if (e_4) throw e_4.error; }
2423
+ finally { if (e_7) throw e_7.error; }
2219
2424
  }
2220
2425
  return [4 /*yield*/, runtime.load(text)];
2221
2426
  case 1:
@@ -2238,14 +2443,14 @@ describe('service worker host', function () {
2238
2443
  return [3 /*break*/, 3];
2239
2444
  case 6: return [3 /*break*/, 9];
2240
2445
  case 7:
2241
- e_3_1 = _j.sent();
2242
- e_3 = { error: e_3_1 };
2446
+ e_6_1 = _j.sent();
2447
+ e_6 = { error: e_6_1 };
2243
2448
  return [3 /*break*/, 9];
2244
2449
  case 8:
2245
2450
  try {
2246
2451
  if (_e && !_e.done && (_h = _d.return)) _h.call(_d);
2247
2452
  }
2248
- finally { if (e_3) throw e_3.error; }
2453
+ finally { if (e_6) throw e_6.error; }
2249
2454
  return [7 /*endfinally*/];
2250
2455
  case 9: return [2 /*return*/];
2251
2456
  }
@@ -2313,12 +2518,12 @@ describe('service worker host', function () {
2313
2518
  _loop_1(testCase);
2314
2519
  }
2315
2520
  }
2316
- catch (e_1_1) { e_1 = { error: e_1_1 }; }
2521
+ catch (e_4_1) { e_4 = { error: e_4_1 }; }
2317
2522
  finally {
2318
2523
  try {
2319
2524
  if (DRIVE_CORPUS_1_1 && !DRIVE_CORPUS_1_1.done && (_a = DRIVE_CORPUS_1.return)) _a.call(DRIVE_CORPUS_1);
2320
2525
  }
2321
- finally { if (e_1) throw e_1.error; }
2526
+ finally { if (e_4) throw e_4.error; }
2322
2527
  }
2323
2528
  });
2324
2529
  });
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.20.0"
129
129
  }