@sasjs/cli 6.10.1 → 6.12.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 +221 -75
- package/build/commands/build/internal/mockSite/assets/sw.js +160 -24
- package/build/commands/build/internal/mockSite/mockSite.js +34 -2
- package/build/commands/build/internal/mockSite/spec/mockSite.spec.js +120 -1
- package/build/commands/build/internal/mockSite/spec/runtime-core.spec.js +480 -14
- package/build/commands/build/internal/mockSite/spec/sw.spec.js +710 -4
- package/package.json +3 -2
|
@@ -35,7 +35,39 @@ 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 __read = (this && this.__read) || function (o, n) {
|
|
39
|
+
var m = typeof Symbol === "function" && o[Symbol.iterator];
|
|
40
|
+
if (!m) return o;
|
|
41
|
+
var i = m.call(o), r, ar = [], e;
|
|
42
|
+
try {
|
|
43
|
+
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
|
|
44
|
+
}
|
|
45
|
+
catch (error) { e = { error: error }; }
|
|
46
|
+
finally {
|
|
47
|
+
try {
|
|
48
|
+
if (r && !r.done && (m = i["return"])) m.call(i);
|
|
49
|
+
}
|
|
50
|
+
finally { if (e) throw e.error; }
|
|
51
|
+
}
|
|
52
|
+
return ar;
|
|
53
|
+
};
|
|
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
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
66
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
67
|
+
};
|
|
38
68
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
69
|
+
var path_1 = __importDefault(require("path"));
|
|
70
|
+
var api_1 = require("@sasjs/api");
|
|
39
71
|
/**
|
|
40
72
|
* The other half of the parity claim: the service worker is what turns an
|
|
41
73
|
* /SASjsApi/ request into a response, so its framing (webout body, the _debug
|
|
@@ -47,6 +79,13 @@ var LOGS_SEPARATOR = 'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784';
|
|
|
47
79
|
var drivePath = function (relative) {
|
|
48
80
|
return "/sasjs/drive/files".concat(appLoc, "/").concat(relative);
|
|
49
81
|
};
|
|
82
|
+
/**
|
|
83
|
+
* The browser build of @sasjs/api that mockSite.ts copies into the site, from the
|
|
84
|
+
* installed package - the worker under test must be the worker the build ships.
|
|
85
|
+
*/
|
|
86
|
+
var apiBrowserBuild = function () {
|
|
87
|
+
return path_1.default.join(path_1.default.dirname(require.resolve('@sasjs/api')), '..', '..', 'build', 'browser.js');
|
|
88
|
+
};
|
|
50
89
|
var loadWorker = function () {
|
|
51
90
|
jest.resetModules();
|
|
52
91
|
var handlers = {};
|
|
@@ -65,6 +104,8 @@ var loadWorker = function () {
|
|
|
65
104
|
global.importScripts = function (path) {
|
|
66
105
|
if (path.includes('md5'))
|
|
67
106
|
return;
|
|
107
|
+
if (path.includes('api-browser'))
|
|
108
|
+
return require(apiBrowserBuild());
|
|
68
109
|
require('../assets/runtime-core.js');
|
|
69
110
|
};
|
|
70
111
|
require('../assets/sw.js');
|
|
@@ -514,9 +555,9 @@ describe('service worker host', function () {
|
|
|
514
555
|
response = _b.sent();
|
|
515
556
|
expect(response.status).toBe(404);
|
|
516
557
|
_a = expect;
|
|
517
|
-
return [4 /*yield*/, response.
|
|
558
|
+
return [4 /*yield*/, response.text()];
|
|
518
559
|
case 2:
|
|
519
|
-
_a.apply(void 0, [
|
|
560
|
+
_a.apply(void 0, [_b.sent()]).toBe("File doesn't exist.");
|
|
520
561
|
return [2 /*return*/];
|
|
521
562
|
}
|
|
522
563
|
});
|
|
@@ -567,9 +608,9 @@ describe('service worker host', function () {
|
|
|
567
608
|
response = _c.sent();
|
|
568
609
|
expect(response.status).toBe(500);
|
|
569
610
|
_a = expect;
|
|
570
|
-
return [4 /*yield*/, response.
|
|
611
|
+
return [4 /*yield*/, response.text()];
|
|
571
612
|
case 2:
|
|
572
|
-
_a.apply(void 0, [
|
|
613
|
+
_a.apply(void 0, [_c.sent()]).toContain('Corrupt binary entry');
|
|
573
614
|
return [2 /*return*/];
|
|
574
615
|
}
|
|
575
616
|
});
|
|
@@ -799,4 +840,669 @@ describe('service worker host', function () {
|
|
|
799
840
|
var dispatch = seeded().dispatch;
|
|
800
841
|
expect(dispatch('/index.html')).toBeUndefined();
|
|
801
842
|
});
|
|
843
|
+
describe('drive writes', function () {
|
|
844
|
+
/**
|
|
845
|
+
* The response as a client sees it. A drive refusal is plain text (the
|
|
846
|
+
* server sends `res.status(code).send(err.message)`), a success is JSON.
|
|
847
|
+
*/
|
|
848
|
+
var answer = function (response) { return __awaiter(void 0, void 0, void 0, function () {
|
|
849
|
+
var resolved, type, _a;
|
|
850
|
+
var _b;
|
|
851
|
+
return __generator(this, function (_c) {
|
|
852
|
+
switch (_c.label) {
|
|
853
|
+
case 0: return [4 /*yield*/, response];
|
|
854
|
+
case 1:
|
|
855
|
+
resolved = _c.sent();
|
|
856
|
+
type = resolved.headers.get('content-type') || '';
|
|
857
|
+
_b = {
|
|
858
|
+
status: resolved.status
|
|
859
|
+
};
|
|
860
|
+
if (!type.includes('application/json')) return [3 /*break*/, 3];
|
|
861
|
+
return [4 /*yield*/, resolved.json()];
|
|
862
|
+
case 2:
|
|
863
|
+
_a = _c.sent();
|
|
864
|
+
return [3 /*break*/, 5];
|
|
865
|
+
case 3: return [4 /*yield*/, resolved.text()];
|
|
866
|
+
case 4:
|
|
867
|
+
_a = _c.sent();
|
|
868
|
+
_c.label = 5;
|
|
869
|
+
case 5: return [2 /*return*/, (_b.body = _a,
|
|
870
|
+
_b)];
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
}); };
|
|
874
|
+
it('answers a drive refusal as text, the way the server sends one', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
875
|
+
var dispatch, response, _a;
|
|
876
|
+
return __generator(this, function (_b) {
|
|
877
|
+
switch (_b.label) {
|
|
878
|
+
case 0:
|
|
879
|
+
dispatch = seeded().dispatch;
|
|
880
|
+
return [4 /*yield*/, dispatch('/SASjsApi/drive/file?_filePath=' +
|
|
881
|
+
encodeURIComponent('/data/missing.csv'))
|
|
882
|
+
// the server's drive routes answer `res.status(code).send(err.message)`,
|
|
883
|
+
// and its own suite asserts `res.text` - so a client that reads the body
|
|
884
|
+
// as text must get the message here too, not a JSON envelope
|
|
885
|
+
];
|
|
886
|
+
case 1:
|
|
887
|
+
response = _b.sent();
|
|
888
|
+
// the server's drive routes answer `res.status(code).send(err.message)`,
|
|
889
|
+
// and its own suite asserts `res.text` - so a client that reads the body
|
|
890
|
+
// as text must get the message here too, not a JSON envelope
|
|
891
|
+
expect(response.status).toBe(404);
|
|
892
|
+
expect(response.headers.get('content-type')).toContain('text/plain');
|
|
893
|
+
_a = expect;
|
|
894
|
+
return [4 /*yield*/, response.text()];
|
|
895
|
+
case 2:
|
|
896
|
+
_a.apply(void 0, [_b.sent()]).toBe("File doesn't exist.");
|
|
897
|
+
return [2 /*return*/];
|
|
898
|
+
}
|
|
899
|
+
});
|
|
900
|
+
}); });
|
|
901
|
+
it('creates a file, which the drive then serves', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
902
|
+
var dispatch, created, read, _a;
|
|
903
|
+
return __generator(this, function (_b) {
|
|
904
|
+
switch (_b.label) {
|
|
905
|
+
case 0:
|
|
906
|
+
dispatch = seeded().dispatch;
|
|
907
|
+
return [4 /*yield*/, answer(dispatch('/SASjsApi/drive/file?_filePath=' +
|
|
908
|
+
encodeURIComponent('/data/users.csv'), (function () {
|
|
909
|
+
var form = new FormData();
|
|
910
|
+
form.set('file', new Blob(['id,name\n1,Ann\n']), 'users.csv');
|
|
911
|
+
return { method: 'POST', body: form };
|
|
912
|
+
})()))];
|
|
913
|
+
case 1:
|
|
914
|
+
created = _b.sent();
|
|
915
|
+
expect(created).toEqual({ status: 200, body: { status: 'success' } });
|
|
916
|
+
return [4 /*yield*/, dispatch('/SASjsApi/drive/file?_filePath=' +
|
|
917
|
+
encodeURIComponent('/data/users.csv'))];
|
|
918
|
+
case 2:
|
|
919
|
+
read = _b.sent();
|
|
920
|
+
_a = expect;
|
|
921
|
+
return [4 /*yield*/, read.text()];
|
|
922
|
+
case 3:
|
|
923
|
+
_a.apply(void 0, [_b.sent()]).toBe('id,name\n1,Ann\n');
|
|
924
|
+
return [2 /*return*/];
|
|
925
|
+
}
|
|
926
|
+
});
|
|
927
|
+
}); });
|
|
928
|
+
it('creates a file at the path in the form when the query has none', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
929
|
+
var dispatch, form, created, read, _a;
|
|
930
|
+
return __generator(this, function (_b) {
|
|
931
|
+
switch (_b.label) {
|
|
932
|
+
case 0:
|
|
933
|
+
dispatch = seeded().dispatch;
|
|
934
|
+
form = new FormData();
|
|
935
|
+
form.set('file', new Blob(['x']), 'x.txt');
|
|
936
|
+
form.set('filePath', '/data/from-form.txt');
|
|
937
|
+
return [4 /*yield*/, answer(dispatch('/SASjsApi/drive/file', { method: 'POST', body: form }))];
|
|
938
|
+
case 1:
|
|
939
|
+
created = _b.sent();
|
|
940
|
+
expect(created.body).toEqual({ status: 'success' });
|
|
941
|
+
return [4 /*yield*/, dispatch('/SASjsApi/drive/file?_filePath=' +
|
|
942
|
+
encodeURIComponent('/data/from-form.txt'))];
|
|
943
|
+
case 2:
|
|
944
|
+
read = _b.sent();
|
|
945
|
+
_a = expect;
|
|
946
|
+
return [4 /*yield*/, read.text()];
|
|
947
|
+
case 3:
|
|
948
|
+
_a.apply(void 0, [_b.sent()]).toBe('x');
|
|
949
|
+
return [2 /*return*/];
|
|
950
|
+
}
|
|
951
|
+
});
|
|
952
|
+
}); });
|
|
953
|
+
it('answers 409 for a create over an existing file, 404 for a replace of a missing one', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
954
|
+
var dispatch, form, again, missing;
|
|
955
|
+
return __generator(this, function (_a) {
|
|
956
|
+
switch (_a.label) {
|
|
957
|
+
case 0:
|
|
958
|
+
dispatch = seeded().dispatch;
|
|
959
|
+
form = function () {
|
|
960
|
+
var f = new FormData();
|
|
961
|
+
f.set('file', new Blob(['x']), 'x.csv');
|
|
962
|
+
return f;
|
|
963
|
+
};
|
|
964
|
+
return [4 /*yield*/, dispatch('/SASjsApi/drive/file?_filePath=' + encodeURIComponent('/data/x.csv'), { method: 'POST', body: form() })];
|
|
965
|
+
case 1:
|
|
966
|
+
_a.sent();
|
|
967
|
+
return [4 /*yield*/, answer(dispatch('/SASjsApi/drive/file?_filePath=' + encodeURIComponent('/data/x.csv'), { method: 'POST', body: form() }))];
|
|
968
|
+
case 2:
|
|
969
|
+
again = _a.sent();
|
|
970
|
+
expect(again.status).toBe(409);
|
|
971
|
+
expect(again.body).toEqual('File already exists.');
|
|
972
|
+
return [4 /*yield*/, answer(dispatch('/SASjsApi/drive/file?_filePath=' +
|
|
973
|
+
encodeURIComponent('/data/missing.csv'), { method: 'PATCH', body: form() }))];
|
|
974
|
+
case 3:
|
|
975
|
+
missing = _a.sent();
|
|
976
|
+
expect(missing.status).toBe(404);
|
|
977
|
+
expect(missing.body).toEqual("File doesn't exist.");
|
|
978
|
+
return [2 /*return*/];
|
|
979
|
+
}
|
|
980
|
+
});
|
|
981
|
+
}); });
|
|
982
|
+
it('replaces a file and deletes one', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
983
|
+
var dispatch, form, replacement, replaced, read, _a, removed, gone;
|
|
984
|
+
return __generator(this, function (_b) {
|
|
985
|
+
switch (_b.label) {
|
|
986
|
+
case 0:
|
|
987
|
+
dispatch = seeded().dispatch;
|
|
988
|
+
form = new FormData();
|
|
989
|
+
form.set('file', new Blob(['v1']), 'v.csv');
|
|
990
|
+
return [4 /*yield*/, dispatch('/SASjsApi/drive/file?_filePath=' + encodeURIComponent('/data/v.csv'), { method: 'POST', body: form })];
|
|
991
|
+
case 1:
|
|
992
|
+
_b.sent();
|
|
993
|
+
replacement = new FormData();
|
|
994
|
+
replacement.set('file', new Blob(['v2']), 'v.csv');
|
|
995
|
+
return [4 /*yield*/, answer(dispatch('/SASjsApi/drive/file?_filePath=' + encodeURIComponent('/data/v.csv'), { method: 'PATCH', body: replacement }))];
|
|
996
|
+
case 2:
|
|
997
|
+
replaced = _b.sent();
|
|
998
|
+
expect(replaced.body).toEqual({ status: 'success' });
|
|
999
|
+
return [4 /*yield*/, dispatch('/SASjsApi/drive/file?_filePath=' + encodeURIComponent('/data/v.csv'))];
|
|
1000
|
+
case 3:
|
|
1001
|
+
read = _b.sent();
|
|
1002
|
+
_a = expect;
|
|
1003
|
+
return [4 /*yield*/, read.text()];
|
|
1004
|
+
case 4:
|
|
1005
|
+
_a.apply(void 0, [_b.sent()]).toBe('v2');
|
|
1006
|
+
return [4 /*yield*/, answer(dispatch('/SASjsApi/drive/file?_filePath=' + encodeURIComponent('/data/v.csv'), { method: 'DELETE' }))];
|
|
1007
|
+
case 5:
|
|
1008
|
+
removed = _b.sent();
|
|
1009
|
+
expect(removed.body).toEqual({ status: 'success' });
|
|
1010
|
+
return [4 /*yield*/, dispatch('/SASjsApi/drive/file?_filePath=' + encodeURIComponent('/data/v.csv'))];
|
|
1011
|
+
case 6:
|
|
1012
|
+
gone = _b.sent();
|
|
1013
|
+
expect(gone.status).toBe(404);
|
|
1014
|
+
return [2 /*return*/];
|
|
1015
|
+
}
|
|
1016
|
+
});
|
|
1017
|
+
}); });
|
|
1018
|
+
it('refuses an upload with no file, no path, or a blocked extension', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1019
|
+
var dispatch, empty, noFile, form, noPath, sh, blocked;
|
|
1020
|
+
return __generator(this, function (_a) {
|
|
1021
|
+
switch (_a.label) {
|
|
1022
|
+
case 0:
|
|
1023
|
+
dispatch = seeded().dispatch;
|
|
1024
|
+
empty = new FormData();
|
|
1025
|
+
return [4 /*yield*/, answer(dispatch('/SASjsApi/drive/file?_filePath=' + encodeURIComponent('/data/x.csv'), { method: 'POST', body: empty }))];
|
|
1026
|
+
case 1:
|
|
1027
|
+
noFile = _a.sent();
|
|
1028
|
+
expect(noFile.status).toBe(400);
|
|
1029
|
+
expect(noFile.body).toEqual('"file" is not present.');
|
|
1030
|
+
form = new FormData();
|
|
1031
|
+
form.set('file', new Blob(['x']), 'x.csv');
|
|
1032
|
+
return [4 /*yield*/, answer(dispatch('/SASjsApi/drive/file', { method: 'POST', body: form }))];
|
|
1033
|
+
case 2:
|
|
1034
|
+
noPath = _a.sent();
|
|
1035
|
+
expect(noPath.status).toBe(400);
|
|
1036
|
+
expect(noPath.body).toEqual('"_filePath" is required');
|
|
1037
|
+
sh = new FormData();
|
|
1038
|
+
sh.set('file', new Blob(['x']), 'run.sh');
|
|
1039
|
+
return [4 /*yield*/, answer(dispatch('/SASjsApi/drive/file?_filePath=' +
|
|
1040
|
+
encodeURIComponent('/data/run.sh'), { method: 'POST', body: sh }))];
|
|
1041
|
+
case 3:
|
|
1042
|
+
blocked = _a.sent();
|
|
1043
|
+
expect(blocked.status).toBe(400);
|
|
1044
|
+
expect(blocked.body).toEqual('Invalid file extension');
|
|
1045
|
+
return [2 /*return*/];
|
|
1046
|
+
}
|
|
1047
|
+
});
|
|
1048
|
+
}); });
|
|
1049
|
+
it('refuses a path outside the drive on every write route', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1050
|
+
var dispatch, form, outside, rename;
|
|
1051
|
+
return __generator(this, function (_a) {
|
|
1052
|
+
switch (_a.label) {
|
|
1053
|
+
case 0:
|
|
1054
|
+
dispatch = seeded().dispatch;
|
|
1055
|
+
form = new FormData();
|
|
1056
|
+
form.set('file', new Blob(['x']), 'x.csv');
|
|
1057
|
+
return [4 /*yield*/, answer(dispatch('/SASjsApi/drive/file?_filePath=' +
|
|
1058
|
+
encodeURIComponent('/../escape.csv'), { method: 'POST', body: form }))];
|
|
1059
|
+
case 1:
|
|
1060
|
+
outside = _a.sent();
|
|
1061
|
+
expect(outside.status).toBe(400);
|
|
1062
|
+
return [4 /*yield*/, answer(dispatch('/SASjsApi/drive/rename', {
|
|
1063
|
+
method: 'POST',
|
|
1064
|
+
headers: { 'content-type': 'application/json' },
|
|
1065
|
+
body: JSON.stringify({ oldPath: '/../x', newPath: '/data/y' })
|
|
1066
|
+
}))];
|
|
1067
|
+
case 2:
|
|
1068
|
+
rename = _a.sent();
|
|
1069
|
+
expect(rename.status).toBe(400);
|
|
1070
|
+
return [2 /*return*/];
|
|
1071
|
+
}
|
|
1072
|
+
});
|
|
1073
|
+
}); });
|
|
1074
|
+
it('creates a folder, lists it, and deletes it with its contents', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1075
|
+
var dispatch, made, form, listed, _a, removed, gone;
|
|
1076
|
+
return __generator(this, function (_b) {
|
|
1077
|
+
switch (_b.label) {
|
|
1078
|
+
case 0:
|
|
1079
|
+
dispatch = seeded().dispatch;
|
|
1080
|
+
return [4 /*yield*/, answer(dispatch('/SASjsApi/drive/folder', {
|
|
1081
|
+
method: 'POST',
|
|
1082
|
+
headers: { 'content-type': 'application/json' },
|
|
1083
|
+
body: JSON.stringify({ folderPath: '/data/reports' })
|
|
1084
|
+
}))];
|
|
1085
|
+
case 1:
|
|
1086
|
+
made = _b.sent();
|
|
1087
|
+
expect(made.body).toEqual({ status: 'success' });
|
|
1088
|
+
form = new FormData();
|
|
1089
|
+
form.set('file', new Blob(['a']), 'a.csv');
|
|
1090
|
+
return [4 /*yield*/, dispatch('/SASjsApi/drive/file?_filePath=' +
|
|
1091
|
+
encodeURIComponent('/data/reports/a.csv'), { method: 'POST', body: form })];
|
|
1092
|
+
case 2:
|
|
1093
|
+
_b.sent();
|
|
1094
|
+
return [4 /*yield*/, dispatch('/SASjsApi/drive/folder?_folderPath=' + encodeURIComponent('/data'))];
|
|
1095
|
+
case 3:
|
|
1096
|
+
listed = _b.sent();
|
|
1097
|
+
_a = expect;
|
|
1098
|
+
return [4 /*yield*/, listed.json()];
|
|
1099
|
+
case 4:
|
|
1100
|
+
_a.apply(void 0, [_b.sent()]).toEqual({
|
|
1101
|
+
files: [],
|
|
1102
|
+
folders: ['reports']
|
|
1103
|
+
});
|
|
1104
|
+
return [4 /*yield*/, answer(dispatch('/SASjsApi/drive/folder?_folderPath=' +
|
|
1105
|
+
encodeURIComponent('/data/reports'), { method: 'DELETE' }))];
|
|
1106
|
+
case 5:
|
|
1107
|
+
removed = _b.sent();
|
|
1108
|
+
expect(removed.body).toEqual({ status: 'success' });
|
|
1109
|
+
return [4 /*yield*/, dispatch('/SASjsApi/drive/file?_filePath=' +
|
|
1110
|
+
encodeURIComponent('/data/reports/a.csv'))];
|
|
1111
|
+
case 6:
|
|
1112
|
+
gone = _b.sent();
|
|
1113
|
+
expect(gone.status).toBe(404);
|
|
1114
|
+
return [2 /*return*/];
|
|
1115
|
+
}
|
|
1116
|
+
});
|
|
1117
|
+
}); });
|
|
1118
|
+
it('refuses a folder delete with no path', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1119
|
+
var dispatch, removed;
|
|
1120
|
+
return __generator(this, function (_a) {
|
|
1121
|
+
switch (_a.label) {
|
|
1122
|
+
case 0:
|
|
1123
|
+
dispatch = seeded().dispatch;
|
|
1124
|
+
return [4 /*yield*/, answer(dispatch('/SASjsApi/drive/folder', { method: 'DELETE' }))];
|
|
1125
|
+
case 1:
|
|
1126
|
+
removed = _a.sent();
|
|
1127
|
+
expect(removed.status).toBe(400);
|
|
1128
|
+
expect(removed.body).toEqual('"_folderPath" is required');
|
|
1129
|
+
return [2 /*return*/];
|
|
1130
|
+
}
|
|
1131
|
+
});
|
|
1132
|
+
}); });
|
|
1133
|
+
it('refuses a rename into a blocked extension, as the server does', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1134
|
+
var dispatch, form, toBlocked, read;
|
|
1135
|
+
return __generator(this, function (_a) {
|
|
1136
|
+
switch (_a.label) {
|
|
1137
|
+
case 0:
|
|
1138
|
+
dispatch = seeded().dispatch;
|
|
1139
|
+
form = new FormData();
|
|
1140
|
+
form.set('file', new Blob(['r']), 'r.csv');
|
|
1141
|
+
return [4 /*yield*/, dispatch('/SASjsApi/drive/file?_filePath=' + encodeURIComponent('/data/old.csv'), { method: 'POST', body: form })];
|
|
1142
|
+
case 1:
|
|
1143
|
+
_a.sent();
|
|
1144
|
+
return [4 /*yield*/, answer(dispatch('/SASjsApi/drive/rename', {
|
|
1145
|
+
method: 'POST',
|
|
1146
|
+
headers: { 'content-type': 'application/json' },
|
|
1147
|
+
body: JSON.stringify({
|
|
1148
|
+
oldPath: '/data/old.csv',
|
|
1149
|
+
newPath: '/data/x.sh'
|
|
1150
|
+
})
|
|
1151
|
+
}))
|
|
1152
|
+
// The extension rule is the drive's file-path rule, and a rename is a
|
|
1153
|
+
// file operation: sasjs/api (since 0.2.1, sasjs/api#12) applies the rule
|
|
1154
|
+
// to a rename whose target is a file, so both hosts refuse the move and
|
|
1155
|
+
// no entry can be created that the API itself cannot read back. A folder
|
|
1156
|
+
// is not subject to the file-path schema, so renaming a folder to a
|
|
1157
|
+
// dotted name stays allowed.
|
|
1158
|
+
];
|
|
1159
|
+
case 2:
|
|
1160
|
+
toBlocked = _a.sent();
|
|
1161
|
+
// The extension rule is the drive's file-path rule, and a rename is a
|
|
1162
|
+
// file operation: sasjs/api (since 0.2.1, sasjs/api#12) applies the rule
|
|
1163
|
+
// to a rename whose target is a file, so both hosts refuse the move and
|
|
1164
|
+
// no entry can be created that the API itself cannot read back. A folder
|
|
1165
|
+
// is not subject to the file-path schema, so renaming a folder to a
|
|
1166
|
+
// dotted name stays allowed.
|
|
1167
|
+
expect(toBlocked.status).toBe(400);
|
|
1168
|
+
expect(toBlocked.body).toBe('Invalid file extension');
|
|
1169
|
+
return [4 /*yield*/, answer(dispatch('/SASjsApi/drive/file?_filePath=' +
|
|
1170
|
+
encodeURIComponent('/data/old.csv')))];
|
|
1171
|
+
case 3:
|
|
1172
|
+
read = _a.sent();
|
|
1173
|
+
expect(read.status).toBe(200);
|
|
1174
|
+
expect(read.body).toBe('r');
|
|
1175
|
+
return [2 /*return*/];
|
|
1176
|
+
}
|
|
1177
|
+
});
|
|
1178
|
+
}); });
|
|
1179
|
+
it('refuses a file where a folder already holds the path', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1180
|
+
var dispatch, form, atFolder;
|
|
1181
|
+
return __generator(this, function (_a) {
|
|
1182
|
+
switch (_a.label) {
|
|
1183
|
+
case 0:
|
|
1184
|
+
dispatch = seeded().dispatch;
|
|
1185
|
+
return [4 /*yield*/, dispatch('/SASjsApi/drive/folder', {
|
|
1186
|
+
method: 'POST',
|
|
1187
|
+
headers: { 'content-type': 'application/json' },
|
|
1188
|
+
body: JSON.stringify({ folderPath: '/data/reports' })
|
|
1189
|
+
})];
|
|
1190
|
+
case 1:
|
|
1191
|
+
_a.sent();
|
|
1192
|
+
form = new FormData();
|
|
1193
|
+
form.set('file', new Blob(['r']), 'r.csv');
|
|
1194
|
+
return [4 /*yield*/, answer(dispatch('/SASjsApi/drive/file?_filePath=' +
|
|
1195
|
+
encodeURIComponent('/data/reports'), { method: 'POST', body: form }))
|
|
1196
|
+
// the server fails this at the filesystem (EISDIR); naming it as a conflict
|
|
1197
|
+
// keeps the drive from holding one path as both a file and a folder
|
|
1198
|
+
];
|
|
1199
|
+
case 2:
|
|
1200
|
+
atFolder = _a.sent();
|
|
1201
|
+
// the server fails this at the filesystem (EISDIR); naming it as a conflict
|
|
1202
|
+
// keeps the drive from holding one path as both a file and a folder
|
|
1203
|
+
expect(atFolder.status).toBe(409);
|
|
1204
|
+
return [2 /*return*/];
|
|
1205
|
+
}
|
|
1206
|
+
});
|
|
1207
|
+
}); });
|
|
1208
|
+
it('refuses a file under a path that is already a file', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1209
|
+
var dispatch, form, child, under, stillAFile, _a;
|
|
1210
|
+
return __generator(this, function (_b) {
|
|
1211
|
+
switch (_b.label) {
|
|
1212
|
+
case 0:
|
|
1213
|
+
dispatch = seeded().dispatch;
|
|
1214
|
+
form = new FormData();
|
|
1215
|
+
form.set('file', new Blob(['a']), 'a.csv');
|
|
1216
|
+
return [4 /*yield*/, dispatch('/SASjsApi/drive/file?_filePath=' + encodeURIComponent('/data/a.csv'), { method: 'POST', body: form })];
|
|
1217
|
+
case 1:
|
|
1218
|
+
_b.sent();
|
|
1219
|
+
child = new FormData();
|
|
1220
|
+
child.set('file', new Blob(['b']), 'b.csv');
|
|
1221
|
+
return [4 /*yield*/, answer(dispatch('/SASjsApi/drive/file?_filePath=' +
|
|
1222
|
+
encodeURIComponent('/data/a.csv/child.csv'), { method: 'POST', body: child }))
|
|
1223
|
+
// mkdirp would otherwise turn the file into a folder to hold the child.
|
|
1224
|
+
// The server loses this at the filesystem (ENOTDIR) and answers a 500, so
|
|
1225
|
+
// this answers a 500 too: the refusal is the point, the status is the
|
|
1226
|
+
// server's class, and the message says which path was in the way.
|
|
1227
|
+
];
|
|
1228
|
+
case 2:
|
|
1229
|
+
under = _b.sent();
|
|
1230
|
+
// mkdirp would otherwise turn the file into a folder to hold the child.
|
|
1231
|
+
// The server loses this at the filesystem (ENOTDIR) and answers a 500, so
|
|
1232
|
+
// this answers a 500 too: the refusal is the point, the status is the
|
|
1233
|
+
// server's class, and the message says which path was in the way.
|
|
1234
|
+
expect(under.status).toBe(500);
|
|
1235
|
+
expect(under.body).toEqual({
|
|
1236
|
+
message: 'ENOTDIR: an ancestor of the path is a file: /data/a.csv'
|
|
1237
|
+
});
|
|
1238
|
+
return [4 /*yield*/, dispatch('/SASjsApi/drive/file?_filePath=' + encodeURIComponent('/data/a.csv'))];
|
|
1239
|
+
case 3:
|
|
1240
|
+
stillAFile = _b.sent();
|
|
1241
|
+
_a = expect;
|
|
1242
|
+
return [4 /*yield*/, stillAFile.text()];
|
|
1243
|
+
case 4:
|
|
1244
|
+
_a.apply(void 0, [_b.sent()]).toBe('a');
|
|
1245
|
+
return [2 /*return*/];
|
|
1246
|
+
}
|
|
1247
|
+
});
|
|
1248
|
+
}); });
|
|
1249
|
+
it('serves a download with the type its extension implies', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1250
|
+
var dispatch, form, csv, _a, sas, program;
|
|
1251
|
+
return __generator(this, function (_b) {
|
|
1252
|
+
switch (_b.label) {
|
|
1253
|
+
case 0:
|
|
1254
|
+
dispatch = seeded().dispatch;
|
|
1255
|
+
form = new FormData();
|
|
1256
|
+
form.set('file', new Blob(['id,name\n1,Ann\n']), 'users.csv');
|
|
1257
|
+
return [4 /*yield*/, dispatch('/SASjsApi/drive/file?_filePath=' +
|
|
1258
|
+
encodeURIComponent('/data/users.csv'), { method: 'POST', body: form })];
|
|
1259
|
+
case 1:
|
|
1260
|
+
_b.sent();
|
|
1261
|
+
return [4 /*yield*/, dispatch('/SASjsApi/drive/file?_filePath=' +
|
|
1262
|
+
encodeURIComponent('/data/users.csv'))
|
|
1263
|
+
// an upload is stored as bytes, as multer leaves it on the server; the
|
|
1264
|
+
// extension is what makes it answer text/csv rather than octet-stream
|
|
1265
|
+
];
|
|
1266
|
+
case 2:
|
|
1267
|
+
csv = _b.sent();
|
|
1268
|
+
// an upload is stored as bytes, as multer leaves it on the server; the
|
|
1269
|
+
// extension is what makes it answer text/csv rather than octet-stream
|
|
1270
|
+
expect(csv.headers.get('content-type')).toBe('text/csv');
|
|
1271
|
+
_a = expect;
|
|
1272
|
+
return [4 /*yield*/, csv.text()];
|
|
1273
|
+
case 3:
|
|
1274
|
+
_a.apply(void 0, [_b.sent()]).toBe('id,name\n1,Ann\n');
|
|
1275
|
+
sas = new FormData();
|
|
1276
|
+
sas.set('file', new Blob(['data _null_; run;']), 'p.sas');
|
|
1277
|
+
return [4 /*yield*/, dispatch('/SASjsApi/drive/file?_filePath=' + encodeURIComponent('/data/p.sas'), { method: 'POST', body: sas })];
|
|
1278
|
+
case 4:
|
|
1279
|
+
_b.sent();
|
|
1280
|
+
return [4 /*yield*/, dispatch('/SASjsApi/drive/file?_filePath=' + encodeURIComponent('/data/p.sas'))];
|
|
1281
|
+
case 5:
|
|
1282
|
+
program = _b.sent();
|
|
1283
|
+
expect(program.headers.get('content-type')).toBe('text/plain');
|
|
1284
|
+
return [2 /*return*/];
|
|
1285
|
+
}
|
|
1286
|
+
});
|
|
1287
|
+
}); });
|
|
1288
|
+
it('refuses an upload larger than the host allows, before reading it', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1289
|
+
var dispatch, runtime, form, response;
|
|
1290
|
+
return __generator(this, function (_a) {
|
|
1291
|
+
switch (_a.label) {
|
|
1292
|
+
case 0:
|
|
1293
|
+
dispatch = seeded().dispatch;
|
|
1294
|
+
runtime = loadWorker().runtime;
|
|
1295
|
+
form = new FormData();
|
|
1296
|
+
form.set('file', new Blob([new Uint8Array(runtime.MAX_UPLOAD_BYTES + 1)]), 'big.bin');
|
|
1297
|
+
return [4 /*yield*/, answer(dispatch('/SASjsApi/drive/file?_filePath=' +
|
|
1298
|
+
encodeURIComponent('/data/big.bin'), { method: 'POST', body: form }))];
|
|
1299
|
+
case 1:
|
|
1300
|
+
response = _a.sent();
|
|
1301
|
+
expect(response.status).toBe(413);
|
|
1302
|
+
expect(response.body).toContain('larger than the browser host allows');
|
|
1303
|
+
return [2 /*return*/];
|
|
1304
|
+
}
|
|
1305
|
+
});
|
|
1306
|
+
}); });
|
|
1307
|
+
it('renames a file through the route the API documents', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1308
|
+
var dispatch, form, renamed, read, _a;
|
|
1309
|
+
return __generator(this, function (_b) {
|
|
1310
|
+
switch (_b.label) {
|
|
1311
|
+
case 0:
|
|
1312
|
+
dispatch = seeded().dispatch;
|
|
1313
|
+
form = new FormData();
|
|
1314
|
+
form.set('file', new Blob(['r']), 'r.csv');
|
|
1315
|
+
return [4 /*yield*/, dispatch('/SASjsApi/drive/file?_filePath=' + encodeURIComponent('/data/old.csv'), { method: 'POST', body: form })];
|
|
1316
|
+
case 1:
|
|
1317
|
+
_b.sent();
|
|
1318
|
+
return [4 /*yield*/, answer(dispatch('/SASjsApi/drive/rename', {
|
|
1319
|
+
method: 'POST',
|
|
1320
|
+
headers: { 'content-type': 'application/json' },
|
|
1321
|
+
body: JSON.stringify({
|
|
1322
|
+
oldPath: '/data/old.csv',
|
|
1323
|
+
newPath: '/data/new.csv'
|
|
1324
|
+
})
|
|
1325
|
+
}))];
|
|
1326
|
+
case 2:
|
|
1327
|
+
renamed = _b.sent();
|
|
1328
|
+
expect(renamed.body).toEqual({ status: 'success' });
|
|
1329
|
+
return [4 /*yield*/, dispatch('/SASjsApi/drive/file?_filePath=' + encodeURIComponent('/data/new.csv'))];
|
|
1330
|
+
case 3:
|
|
1331
|
+
read = _b.sent();
|
|
1332
|
+
_a = expect;
|
|
1333
|
+
return [4 /*yield*/, read.text()];
|
|
1334
|
+
case 4:
|
|
1335
|
+
_a.apply(void 0, [_b.sent()]).toBe('r');
|
|
1336
|
+
return [2 /*return*/];
|
|
1337
|
+
}
|
|
1338
|
+
});
|
|
1339
|
+
}); });
|
|
1340
|
+
});
|
|
1341
|
+
describe('conformance corpus', function () {
|
|
1342
|
+
var e_1, _a;
|
|
1343
|
+
/**
|
|
1344
|
+
* The corpus is what "the same API" means: the cases @sasjs/api runs against
|
|
1345
|
+
* its own services, and against a live sasjs/server, run here against this
|
|
1346
|
+
* worker's HTTP surface. A case that answers differently is this host
|
|
1347
|
+
* drifting from the server - the failure mode the hand-written drive routes
|
|
1348
|
+
* used to have - and it is a test rather than a review.
|
|
1349
|
+
*/
|
|
1350
|
+
var httpFor = function (testCase) {
|
|
1351
|
+
var e_2, _a;
|
|
1352
|
+
var _b;
|
|
1353
|
+
var _c = __read(testCase.route.split(' '), 2), method = _c[0], routePath = _c[1];
|
|
1354
|
+
var query = new URLSearchParams();
|
|
1355
|
+
try {
|
|
1356
|
+
for (var _d = __values(Object.entries(testCase.request.query || {})), _e = _d.next(); !_e.done; _e = _d.next()) {
|
|
1357
|
+
var _f = __read(_e.value, 2), key = _f[0], value = _f[1];
|
|
1358
|
+
if (value !== undefined)
|
|
1359
|
+
query.set(key, String(value));
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
catch (e_2_1) { e_2 = { error: e_2_1 }; }
|
|
1363
|
+
finally {
|
|
1364
|
+
try {
|
|
1365
|
+
if (_e && !_e.done && (_a = _d.return)) _a.call(_d);
|
|
1366
|
+
}
|
|
1367
|
+
finally { if (e_2) throw e_2.error; }
|
|
1368
|
+
}
|
|
1369
|
+
var suffix = query.toString() ? "?".concat(query) : '';
|
|
1370
|
+
var init = { method: method };
|
|
1371
|
+
var file = testCase.request.file;
|
|
1372
|
+
if (file) {
|
|
1373
|
+
var form = new FormData();
|
|
1374
|
+
// the Blob types want a view over an ArrayBuffer; a Uint8Array from the
|
|
1375
|
+
// corpus is typed over ArrayBufferLike, which a Blob does not accept
|
|
1376
|
+
var bytes = typeof file.contents === 'string'
|
|
1377
|
+
? new TextEncoder().encode(file.contents)
|
|
1378
|
+
: new Uint8Array(file.contents);
|
|
1379
|
+
form.set('file', new Blob([bytes]), file.name);
|
|
1380
|
+
var formPath = (_b = testCase.request.body) === null || _b === void 0 ? void 0 : _b.filePath;
|
|
1381
|
+
if (formPath !== undefined)
|
|
1382
|
+
form.set('filePath', String(formPath));
|
|
1383
|
+
init.body = form;
|
|
1384
|
+
}
|
|
1385
|
+
else if (testCase.request.body) {
|
|
1386
|
+
init.body = JSON.stringify(testCase.request.body);
|
|
1387
|
+
init.headers = { 'content-type': 'application/json' };
|
|
1388
|
+
}
|
|
1389
|
+
return { path: routePath + suffix, init: init };
|
|
1390
|
+
};
|
|
1391
|
+
/**
|
|
1392
|
+
* The case's own files, at the paths the drive stores them under. `load`
|
|
1393
|
+
* restores the seed snapshot, which is a map of strings - a byte entry is a
|
|
1394
|
+
* snapshot of an upload, so it goes in through the store the way an upload
|
|
1395
|
+
* does.
|
|
1396
|
+
*/
|
|
1397
|
+
var seedDrive = function (runtime, testCase) { return __awaiter(void 0, void 0, void 0, function () {
|
|
1398
|
+
var text, _a, _b, _c, file, contents, _d, _e, _f, file, contents, e_3_1;
|
|
1399
|
+
var e_4, _g, e_3, _h;
|
|
1400
|
+
return __generator(this, function (_j) {
|
|
1401
|
+
switch (_j.label) {
|
|
1402
|
+
case 0:
|
|
1403
|
+
text = {};
|
|
1404
|
+
try {
|
|
1405
|
+
for (_a = __values(Object.entries(testCase.seed || {})), _b = _a.next(); !_b.done; _b = _a.next()) {
|
|
1406
|
+
_c = __read(_b.value, 2), file = _c[0], contents = _c[1];
|
|
1407
|
+
if (typeof contents === 'string')
|
|
1408
|
+
text["/sasjs/drive/files".concat(file)] = contents;
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
catch (e_4_1) { e_4 = { error: e_4_1 }; }
|
|
1412
|
+
finally {
|
|
1413
|
+
try {
|
|
1414
|
+
if (_b && !_b.done && (_g = _a.return)) _g.call(_a);
|
|
1415
|
+
}
|
|
1416
|
+
finally { if (e_4) throw e_4.error; }
|
|
1417
|
+
}
|
|
1418
|
+
return [4 /*yield*/, runtime.load(text)];
|
|
1419
|
+
case 1:
|
|
1420
|
+
_j.sent();
|
|
1421
|
+
_j.label = 2;
|
|
1422
|
+
case 2:
|
|
1423
|
+
_j.trys.push([2, 7, 8, 9]);
|
|
1424
|
+
_d = __values(Object.entries(testCase.seed || {})), _e = _d.next();
|
|
1425
|
+
_j.label = 3;
|
|
1426
|
+
case 3:
|
|
1427
|
+
if (!!_e.done) return [3 /*break*/, 6];
|
|
1428
|
+
_f = __read(_e.value, 2), file = _f[0], contents = _f[1];
|
|
1429
|
+
if (!(typeof contents !== 'string')) return [3 /*break*/, 5];
|
|
1430
|
+
return [4 /*yield*/, runtime.driveStore.write(file, contents)];
|
|
1431
|
+
case 4:
|
|
1432
|
+
_j.sent();
|
|
1433
|
+
_j.label = 5;
|
|
1434
|
+
case 5:
|
|
1435
|
+
_e = _d.next();
|
|
1436
|
+
return [3 /*break*/, 3];
|
|
1437
|
+
case 6: return [3 /*break*/, 9];
|
|
1438
|
+
case 7:
|
|
1439
|
+
e_3_1 = _j.sent();
|
|
1440
|
+
e_3 = { error: e_3_1 };
|
|
1441
|
+
return [3 /*break*/, 9];
|
|
1442
|
+
case 8:
|
|
1443
|
+
try {
|
|
1444
|
+
if (_e && !_e.done && (_h = _d.return)) _h.call(_d);
|
|
1445
|
+
}
|
|
1446
|
+
finally { if (e_3) throw e_3.error; }
|
|
1447
|
+
return [7 /*endfinally*/];
|
|
1448
|
+
case 9: return [2 /*return*/];
|
|
1449
|
+
}
|
|
1450
|
+
});
|
|
1451
|
+
}); };
|
|
1452
|
+
var _loop_1 = function (testCase) {
|
|
1453
|
+
it(testCase.id, function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1454
|
+
var worker, _a, requestPath, init, response, _b, want, _c, _d, _e;
|
|
1455
|
+
return __generator(this, function (_f) {
|
|
1456
|
+
switch (_f.label) {
|
|
1457
|
+
case 0:
|
|
1458
|
+
worker = loadWorker();
|
|
1459
|
+
return [4 /*yield*/, seedDrive(worker.runtime, testCase)];
|
|
1460
|
+
case 1:
|
|
1461
|
+
_f.sent();
|
|
1462
|
+
_a = httpFor(testCase), requestPath = _a.path, init = _a.init;
|
|
1463
|
+
return [4 /*yield*/, worker.dispatch(requestPath, init)];
|
|
1464
|
+
case 2:
|
|
1465
|
+
response = _f.sent();
|
|
1466
|
+
expect(response.status).toBe(testCase.expect.status);
|
|
1467
|
+
if (!(testCase.expect.error !== undefined)) return [3 /*break*/, 4];
|
|
1468
|
+
_b = expect;
|
|
1469
|
+
return [4 /*yield*/, response.text()];
|
|
1470
|
+
case 3:
|
|
1471
|
+
_b.apply(void 0, [_f.sent()]).toBe(testCase.expect.error);
|
|
1472
|
+
_f.label = 4;
|
|
1473
|
+
case 4:
|
|
1474
|
+
want = testCase.expect.contents;
|
|
1475
|
+
if (!(typeof want === 'string')) return [3 /*break*/, 6];
|
|
1476
|
+
_c = expect;
|
|
1477
|
+
return [4 /*yield*/, response.text()];
|
|
1478
|
+
case 5:
|
|
1479
|
+
_c.apply(void 0, [_f.sent()]).toBe(want);
|
|
1480
|
+
return [3 /*break*/, 8];
|
|
1481
|
+
case 6:
|
|
1482
|
+
if (!(want !== undefined)) return [3 /*break*/, 8];
|
|
1483
|
+
_d = expect;
|
|
1484
|
+
_e = Uint8Array.bind;
|
|
1485
|
+
return [4 /*yield*/, response.arrayBuffer()];
|
|
1486
|
+
case 7:
|
|
1487
|
+
_d.apply(void 0, [new (_e.apply(Uint8Array, [void 0, _f.sent()]))()]).toEqual(want);
|
|
1488
|
+
_f.label = 8;
|
|
1489
|
+
case 8: return [2 /*return*/];
|
|
1490
|
+
}
|
|
1491
|
+
});
|
|
1492
|
+
}); });
|
|
1493
|
+
};
|
|
1494
|
+
try {
|
|
1495
|
+
for (var DRIVE_CORPUS_1 = __values(api_1.DRIVE_CORPUS), DRIVE_CORPUS_1_1 = DRIVE_CORPUS_1.next(); !DRIVE_CORPUS_1_1.done; DRIVE_CORPUS_1_1 = DRIVE_CORPUS_1.next()) {
|
|
1496
|
+
var testCase = DRIVE_CORPUS_1_1.value;
|
|
1497
|
+
_loop_1(testCase);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
1501
|
+
finally {
|
|
1502
|
+
try {
|
|
1503
|
+
if (DRIVE_CORPUS_1_1 && !DRIVE_CORPUS_1_1.done && (_a = DRIVE_CORPUS_1.return)) _a.call(DRIVE_CORPUS_1);
|
|
1504
|
+
}
|
|
1505
|
+
finally { if (e_1) throw e_1.error; }
|
|
1506
|
+
}
|
|
1507
|
+
});
|
|
802
1508
|
});
|