@sasjs/api 0.0.0-stage → 0.1.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.
Files changed (49) hide show
  1. package/LICENCE.md +21 -0
  2. package/README.md +85 -2
  3. package/build/browser.js +2469 -0
  4. package/build/cjs/corpus/drive.js +288 -0
  5. package/build/cjs/corpus/runner.js +35 -0
  6. package/build/cjs/drive/memory-store.js +126 -0
  7. package/build/cjs/drive/paths.js +82 -0
  8. package/build/cjs/drive/service.js +143 -0
  9. package/build/cjs/host/dispatch.js +102 -0
  10. package/build/cjs/index.js +58 -0
  11. package/build/cjs/routes/generated.js +1603 -0
  12. package/build/cjs/routes/index.js +111 -0
  13. package/build/cjs/routes/overlay.js +75 -0
  14. package/build/cjs/types.js +35 -0
  15. package/build/esm/corpus/drive.d.ts +33 -0
  16. package/build/esm/corpus/drive.d.ts.map +1 -0
  17. package/build/esm/corpus/drive.js +285 -0
  18. package/build/esm/corpus/runner.d.ts +27 -0
  19. package/build/esm/corpus/runner.d.ts.map +1 -0
  20. package/build/esm/corpus/runner.js +30 -0
  21. package/build/esm/drive/memory-store.d.ts +37 -0
  22. package/build/esm/drive/memory-store.d.ts.map +1 -0
  23. package/build/esm/drive/memory-store.js +122 -0
  24. package/build/esm/drive/paths.d.ts +49 -0
  25. package/build/esm/drive/paths.d.ts.map +1 -0
  26. package/build/esm/drive/paths.js +73 -0
  27. package/build/esm/drive/service.d.ts +42 -0
  28. package/build/esm/drive/service.d.ts.map +1 -0
  29. package/build/esm/drive/service.js +139 -0
  30. package/build/esm/host/dispatch.d.ts +24 -0
  31. package/build/esm/host/dispatch.d.ts.map +1 -0
  32. package/build/esm/host/dispatch.js +98 -0
  33. package/build/esm/index.d.ts +31 -0
  34. package/build/esm/index.d.ts.map +1 -0
  35. package/build/esm/index.js +24 -0
  36. package/build/esm/package.json +3 -0
  37. package/build/esm/routes/generated.d.ts +14 -0
  38. package/build/esm/routes/generated.d.ts.map +1 -0
  39. package/build/esm/routes/generated.js +1600 -0
  40. package/build/esm/routes/index.d.ts +18 -0
  41. package/build/esm/routes/index.d.ts.map +1 -0
  42. package/build/esm/routes/index.js +106 -0
  43. package/build/esm/routes/overlay.d.ts +40 -0
  44. package/build/esm/routes/overlay.d.ts.map +1 -0
  45. package/build/esm/routes/overlay.js +72 -0
  46. package/build/esm/types.d.ts +110 -0
  47. package/build/esm/types.d.ts.map +1 -0
  48. package/build/esm/types.js +30 -0
  49. package/package.json +55 -4
@@ -0,0 +1,2469 @@
1
+ /* @sasjs/api 0.1.0 - the SASjs Server API, for a service worker. https://github.com/sasjs/api */
2
+ "use strict";
3
+ var SASjsApi = (() => {
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
21
+
22
+ // src/index.ts
23
+ var index_exports = {};
24
+ __export(index_exports, {
25
+ ApiError: () => ApiError,
26
+ DRIVE_CORPUS: () => DRIVE_CORPUS,
27
+ HANDLED_ROUTES: () => HANDLED_ROUTES,
28
+ MemoryDriveStore: () => MemoryDriveStore,
29
+ OVERLAY: () => OVERLAY,
30
+ ROUTES: () => ROUTES,
31
+ TAG_CAPABILITIES: () => TAG_CAPABILITIES,
32
+ basename: () => basename,
33
+ createDriveService: () => createDriveService,
34
+ dirname: () => dirname,
35
+ dispatch: () => dispatch,
36
+ findRoute: () => findRoute,
37
+ hasBlockedExtension: () => hasBlockedExtension,
38
+ isApiError: () => isApiError,
39
+ isRoot: () => isRoot,
40
+ normaliseDrivePath: () => normaliseDrivePath,
41
+ routeById: () => routeById,
42
+ runCase: () => runCase,
43
+ servicesFor: () => servicesFor
44
+ });
45
+
46
+ // src/types.ts
47
+ var ApiError = class extends Error {
48
+ constructor(status, message) {
49
+ super(message);
50
+ this.status = status;
51
+ /** Told apart by marker rather than `instanceof` - see `isApiError`. */
52
+ this.apiError = true;
53
+ this.name = "ApiError";
54
+ }
55
+ };
56
+ var isApiError = (value) => !!value && value.apiError === true;
57
+
58
+ // src/drive/paths.ts
59
+ var BLOCKED_EXTENSION = /\.(exe|sh|htaccess)$/i;
60
+ var hasBlockedExtension = (path) => BLOCKED_EXTENSION.test(path);
61
+ var normaliseDrivePath = (input) => {
62
+ const given = typeof input === "string" ? input : "";
63
+ const segments = [];
64
+ for (const segment of given.replace(/\\/g, "/").split("/")) {
65
+ if (!segment || segment === ".") continue;
66
+ if (segment === "..") {
67
+ if (!segments.length) return null;
68
+ segments.pop();
69
+ continue;
70
+ }
71
+ segments.push(segment);
72
+ }
73
+ return "/" + segments.join("/");
74
+ };
75
+ var dirname = (path) => {
76
+ const cut = path.lastIndexOf("/");
77
+ return cut <= 0 ? "/" : path.slice(0, cut);
78
+ };
79
+ var basename = (path) => path.slice(path.lastIndexOf("/") + 1);
80
+ var isRoot = (path) => path === "/";
81
+ var checkFilePath = (input) => {
82
+ const path = normaliseDrivePath(input);
83
+ if (path === null) return { reason: "outside" };
84
+ if (hasBlockedExtension(path)) return { reason: "extension" };
85
+ return { path };
86
+ };
87
+
88
+ // src/drive/service.ts
89
+ var filePath = (input, outside) => {
90
+ const checked = checkFilePath(input);
91
+ if ("reason" in checked) {
92
+ throw checked.reason === "extension" ? new ApiError(400, "Invalid file extension") : new ApiError(400, outside);
93
+ }
94
+ if (isRoot(checked.path)) throw new ApiError(400, outside);
95
+ return checked.path;
96
+ };
97
+ var folderPath = (input, outside) => {
98
+ const path = normaliseDrivePath(input);
99
+ if (path === null) throw new ApiError(400, outside);
100
+ return path;
101
+ };
102
+ var createDriveService = (store) => {
103
+ const treeOf = async (path) => {
104
+ const listing = await store.list(path);
105
+ const children = [];
106
+ for (const folder of listing.folders) {
107
+ children.push(
108
+ await treeOf(path === "/" ? "/" + folder : path + "/" + folder)
109
+ );
110
+ }
111
+ for (const file of listing.files) {
112
+ children.push({
113
+ name: file,
114
+ relativePath: path === "/" ? "/" + file : path + "/" + file,
115
+ isFolder: false,
116
+ children: []
117
+ });
118
+ }
119
+ return {
120
+ name: path === "/" ? "files" : basename(path),
121
+ relativePath: path,
122
+ isFolder: true,
123
+ children
124
+ };
125
+ };
126
+ return {
127
+ async list(given) {
128
+ const path = folderPath(
129
+ given === void 0 ? "" : given,
130
+ "Can't get folder outside drive."
131
+ );
132
+ if (!await store.exists(path))
133
+ throw new ApiError(404, `Folder doesn't exist.`);
134
+ const stat = await store.stat(path);
135
+ if (!stat.directory) throw new ApiError(400, "Not a Folder.");
136
+ const listing = await store.list(path);
137
+ return {
138
+ files: [...listing.files].sort(),
139
+ folders: [...listing.folders].sort()
140
+ };
141
+ },
142
+ async read(given) {
143
+ const path = filePath(given, "Can't get file outside drive.");
144
+ if (!await store.exists(path))
145
+ throw new ApiError(404, `File doesn't exist.`);
146
+ const contents = await store.read(path);
147
+ return { contents, binary: typeof contents !== "string" };
148
+ },
149
+ async save(given, contents) {
150
+ const path = filePath(given, "Can't put file outside drive.");
151
+ if (await store.exists(path))
152
+ throw new ApiError(409, "File already exists.");
153
+ await store.mkdirp(dirname(path));
154
+ await store.write(path, contents);
155
+ return { status: "success" };
156
+ },
157
+ async update(given, contents) {
158
+ const path = filePath(given, "Can't modify file outside drive.");
159
+ if (!await store.exists(path))
160
+ throw new ApiError(404, `File doesn't exist.`);
161
+ await store.write(path, contents);
162
+ return { status: "success" };
163
+ },
164
+ async remove(given) {
165
+ const path = filePath(given, "Can't delete file outside drive.");
166
+ if (!await store.exists(path))
167
+ throw new ApiError(404, `File doesn't exist.`);
168
+ await store.remove(path);
169
+ return { status: "success" };
170
+ },
171
+ async mkdir(given) {
172
+ const path = folderPath(given, "Can't put folder outside drive.");
173
+ if (isRoot(path))
174
+ throw new ApiError(400, "Can't put folder outside drive.");
175
+ if (await store.exists(path))
176
+ throw new ApiError(409, "Folder already exists.");
177
+ await store.mkdirp(path);
178
+ return { status: "success" };
179
+ },
180
+ async rmdir(given) {
181
+ const path = folderPath(given, "Can't delete folder outside drive.");
182
+ if (isRoot(path))
183
+ throw new ApiError(400, "Can't delete folder outside drive.");
184
+ if (!await store.exists(path) || !(await store.stat(path)).directory)
185
+ throw new ApiError(404, `Folder doesn't exist.`);
186
+ await store.remove(path);
187
+ return { status: "success" };
188
+ },
189
+ async rename(oldPath, newPath) {
190
+ const from = folderPath(oldPath, "Old path can't be outside of drive.");
191
+ const to = folderPath(newPath, "New path can't be outside of drive.");
192
+ if (isRoot(from) || isRoot(to))
193
+ throw new ApiError(400, "New path can't be outside of drive.");
194
+ if (await store.exists(from)) {
195
+ const stat = await store.stat(from);
196
+ if (stat.directory && (to === from || to.startsWith(from + "/")))
197
+ throw new ApiError(
198
+ 400,
199
+ "New path can't be inside the folder being moved."
200
+ );
201
+ if (await store.exists(to))
202
+ throw new ApiError(
203
+ 409,
204
+ stat.directory ? "Folder with new name already exists." : "File with new name already exists."
205
+ );
206
+ await store.move(from, to);
207
+ return { status: "success" };
208
+ }
209
+ throw new ApiError(404, "No file/folder found for provided path.");
210
+ },
211
+ async tree(given) {
212
+ const path = folderPath(
213
+ given === void 0 ? "" : given,
214
+ "Can't get folder outside drive."
215
+ );
216
+ if (!await store.exists(path))
217
+ throw new ApiError(404, `Folder doesn't exist.`);
218
+ const stat = await store.stat(path);
219
+ if (!stat.directory) throw new ApiError(400, "Not a Folder.");
220
+ return { status: "success", tree: await treeOf(path) };
221
+ }
222
+ };
223
+ };
224
+
225
+ // src/drive/memory-store.ts
226
+ var MemoryDriveStore = class {
227
+ constructor(seed = {}) {
228
+ this.files = /* @__PURE__ */ new Map();
229
+ this.folders = /* @__PURE__ */ new Set(["/"]);
230
+ for (const [path, contents] of Object.entries(seed)) {
231
+ this.putSync(path, contents);
232
+ }
233
+ }
234
+ /** Everything on the drive, for a snapshot or an assertion. */
235
+ dump() {
236
+ const out = {};
237
+ for (const [path, contents] of this.files) out[path] = contents;
238
+ return out;
239
+ }
240
+ putSync(path, contents) {
241
+ this.mkdirpSync(dirname(path));
242
+ this.files.set(
243
+ path,
244
+ typeof contents === "string" ? contents : contents.slice()
245
+ );
246
+ }
247
+ mkdirpSync(path) {
248
+ const parts = path.split("/").filter(Boolean);
249
+ let acc = "";
250
+ for (const part of parts) {
251
+ acc += "/" + part;
252
+ this.folders.add(acc);
253
+ }
254
+ }
255
+ children(path) {
256
+ const prefix = path === "/" ? "/" : path + "/";
257
+ const files = [];
258
+ const folders = [];
259
+ for (const candidate of this.files.keys()) {
260
+ if (!candidate.startsWith(prefix)) continue;
261
+ const rest = candidate.slice(prefix.length);
262
+ if (rest && !rest.includes("/")) files.push(rest);
263
+ }
264
+ for (const candidate of this.folders) {
265
+ if (!candidate.startsWith(prefix)) continue;
266
+ const rest = candidate.slice(prefix.length);
267
+ if (rest && !rest.includes("/")) folders.push(rest);
268
+ }
269
+ return { files: files.sort(), folders: folders.sort() };
270
+ }
271
+ async read(path) {
272
+ const contents = this.files.get(path);
273
+ if (contents === void 0) throw new ApiError(404, `File doesn't exist.`);
274
+ return contents;
275
+ }
276
+ async write(path, contents) {
277
+ this.putSync(path, contents);
278
+ }
279
+ async exists(path) {
280
+ return this.files.has(path) || this.folders.has(path);
281
+ }
282
+ async stat(path) {
283
+ if (this.folders.has(path) && !this.files.has(path))
284
+ return { directory: true, size: 0 };
285
+ const contents = this.files.get(path);
286
+ if (contents === void 0) throw new ApiError(404, `File doesn't exist.`);
287
+ return {
288
+ directory: false,
289
+ size: typeof contents === "string" ? contents.length : contents.length
290
+ };
291
+ }
292
+ async list(path) {
293
+ return this.children(path);
294
+ }
295
+ async mkdirp(path) {
296
+ this.mkdirpSync(path);
297
+ }
298
+ async remove(path) {
299
+ if (this.files.delete(path)) return;
300
+ const prefix = path === "/" ? "/" : path + "/";
301
+ for (const candidate of [...this.files.keys()]) {
302
+ if (candidate.startsWith(prefix)) this.files.delete(candidate);
303
+ }
304
+ for (const candidate of [...this.folders]) {
305
+ if (candidate === path || candidate.startsWith(prefix))
306
+ this.folders.delete(candidate);
307
+ }
308
+ }
309
+ async move(from, to) {
310
+ const contents = this.files.get(from);
311
+ if (contents !== void 0) {
312
+ const copy = typeof contents === "string" ? contents : contents.slice();
313
+ this.files.delete(from);
314
+ this.mkdirpSync(dirname(to));
315
+ this.files.set(to, copy);
316
+ return;
317
+ }
318
+ const prefix = from + "/";
319
+ const moves = [];
320
+ for (const [candidate, value] of this.files) {
321
+ if (candidate.startsWith(prefix))
322
+ moves.push([candidate, to + candidate.slice(from.length), value]);
323
+ }
324
+ for (const [source] of moves) this.files.delete(source);
325
+ for (const [, destination, value] of moves) {
326
+ this.mkdirpSync(dirname(destination));
327
+ this.files.set(destination, value);
328
+ }
329
+ for (const candidate of [...this.folders]) {
330
+ if (candidate === from || candidate.startsWith(prefix)) {
331
+ this.folders.delete(candidate);
332
+ this.folders.add(to + candidate.slice(from.length));
333
+ }
334
+ }
335
+ this.mkdirpSync(to);
336
+ }
337
+ };
338
+
339
+ // src/routes/generated.ts
340
+ var GENERATED_ROUTES = [
341
+ {
342
+ id: "GET /",
343
+ method: "GET",
344
+ path: "/",
345
+ tag: "Web",
346
+ summary: "Serve the web interface",
347
+ params: [],
348
+ requestBodies: [],
349
+ responses: [
350
+ {
351
+ status: 200,
352
+ description: "Ok"
353
+ }
354
+ ]
355
+ },
356
+ {
357
+ id: "GET /SASjsApi/admin/diagnostics",
358
+ method: "GET",
359
+ path: "/SASjsApi/admin/diagnostics",
360
+ tag: "Admin",
361
+ summary: "Diagnostics: version, uptime, memory, disk usage",
362
+ params: [],
363
+ requestBodies: [],
364
+ responses: [
365
+ {
366
+ status: 200,
367
+ description: "Ok"
368
+ }
369
+ ]
370
+ },
371
+ {
372
+ id: "GET /SASjsApi/admin/logs",
373
+ method: "GET",
374
+ path: "/SASjsApi/admin/logs",
375
+ tag: "Admin",
376
+ summary: "List the server's log files",
377
+ params: [],
378
+ requestBodies: [],
379
+ responses: [
380
+ {
381
+ status: 200,
382
+ description: "Ok"
383
+ }
384
+ ]
385
+ },
386
+ {
387
+ id: "GET /SASjsApi/admin/logs/{fileName}",
388
+ method: "GET",
389
+ path: "/SASjsApi/admin/logs/{fileName}",
390
+ tag: "Admin",
391
+ summary: "Read the tail of a log file",
392
+ params: [
393
+ {
394
+ name: "fileName",
395
+ in: "path",
396
+ required: true,
397
+ type: "string"
398
+ },
399
+ {
400
+ name: "lines",
401
+ in: "query",
402
+ required: false,
403
+ type: "number"
404
+ }
405
+ ],
406
+ requestBodies: [],
407
+ responses: [
408
+ {
409
+ status: 200,
410
+ description: "Ok"
411
+ }
412
+ ]
413
+ },
414
+ {
415
+ id: "POST /SASjsApi/auth/logout",
416
+ method: "POST",
417
+ path: "/SASjsApi/auth/logout",
418
+ tag: "Auth",
419
+ summary: "Revoke the current tokens",
420
+ params: [],
421
+ requestBodies: [],
422
+ responses: [
423
+ {
424
+ status: 204,
425
+ description: "No content"
426
+ }
427
+ ]
428
+ },
429
+ {
430
+ id: "POST /SASjsApi/auth/refresh",
431
+ method: "POST",
432
+ path: "/SASjsApi/auth/refresh",
433
+ tag: "Auth",
434
+ summary: "Refresh the access and refresh tokens",
435
+ params: [],
436
+ requestBodies: [],
437
+ responses: [
438
+ {
439
+ status: 200,
440
+ description: "Ok"
441
+ }
442
+ ]
443
+ },
444
+ {
445
+ id: "POST /SASjsApi/auth/token",
446
+ method: "POST",
447
+ path: "/SASjsApi/auth/token",
448
+ tag: "Auth",
449
+ summary: "Exchange an authorization code for tokens",
450
+ params: [
451
+ {
452
+ name: "clientId",
453
+ in: "body",
454
+ required: true,
455
+ type: "string"
456
+ },
457
+ {
458
+ name: "code",
459
+ in: "body",
460
+ required: true,
461
+ type: "string"
462
+ }
463
+ ],
464
+ requestBodies: ["application/json"],
465
+ responses: [
466
+ {
467
+ status: 200,
468
+ description: "Ok"
469
+ }
470
+ ]
471
+ },
472
+ {
473
+ id: "PATCH /SASjsApi/auth/updatePassword",
474
+ method: "PATCH",
475
+ path: "/SASjsApi/auth/updatePassword",
476
+ tag: "Auth",
477
+ summary: "Change the current user's password",
478
+ params: [
479
+ {
480
+ name: "currentPassword",
481
+ in: "body",
482
+ required: true,
483
+ type: "string"
484
+ },
485
+ {
486
+ name: "newPassword",
487
+ in: "body",
488
+ required: true,
489
+ type: "string"
490
+ }
491
+ ],
492
+ requestBodies: ["application/json"],
493
+ responses: [
494
+ {
495
+ status: 204,
496
+ description: "No content"
497
+ }
498
+ ]
499
+ },
500
+ {
501
+ id: "GET /SASjsApi/authConfig",
502
+ method: "GET",
503
+ path: "/SASjsApi/authConfig",
504
+ tag: "Auth Config",
505
+ summary: "Get the configured authentication providers",
506
+ params: [],
507
+ requestBodies: [],
508
+ responses: [
509
+ {
510
+ status: 200,
511
+ description: "Ok"
512
+ }
513
+ ]
514
+ },
515
+ {
516
+ id: "POST /SASjsApi/authConfig/synchroniseWithLDAP",
517
+ method: "POST",
518
+ path: "/SASjsApi/authConfig/synchroniseWithLDAP",
519
+ tag: "Auth Config",
520
+ summary: "Import users and groups from LDAP",
521
+ params: [],
522
+ requestBodies: [],
523
+ responses: [
524
+ {
525
+ status: 200,
526
+ description: "Ok"
527
+ }
528
+ ]
529
+ },
530
+ {
531
+ id: "GET /SASjsApi/client",
532
+ method: "GET",
533
+ path: "/SASjsApi/client",
534
+ tag: "Client",
535
+ summary: "List all clients",
536
+ params: [],
537
+ requestBodies: [],
538
+ responses: [
539
+ {
540
+ status: 200,
541
+ description: "Ok"
542
+ }
543
+ ]
544
+ },
545
+ {
546
+ id: "POST /SASjsApi/client",
547
+ method: "POST",
548
+ path: "/SASjsApi/client",
549
+ tag: "Client",
550
+ summary: "Create a client",
551
+ params: [
552
+ {
553
+ name: "clientId",
554
+ in: "body",
555
+ required: true,
556
+ type: "string"
557
+ },
558
+ {
559
+ name: "clientSecret",
560
+ in: "body",
561
+ required: true,
562
+ type: "string"
563
+ },
564
+ {
565
+ name: "accessTokenExpiration",
566
+ in: "body",
567
+ required: false,
568
+ type: "number"
569
+ },
570
+ {
571
+ name: "refreshTokenExpiration",
572
+ in: "body",
573
+ required: false,
574
+ type: "number"
575
+ }
576
+ ],
577
+ requestBodies: ["application/json"],
578
+ responses: [
579
+ {
580
+ status: 200,
581
+ description: "Ok"
582
+ }
583
+ ]
584
+ },
585
+ {
586
+ id: "POST /SASjsApi/code/execute",
587
+ method: "POST",
588
+ path: "/SASjsApi/code/execute",
589
+ tag: "Code",
590
+ summary: "Execute code and return its output",
591
+ params: [
592
+ {
593
+ name: "code",
594
+ in: "body",
595
+ required: true,
596
+ type: "string"
597
+ },
598
+ {
599
+ name: "runTime",
600
+ in: "body",
601
+ required: true,
602
+ type: "RunTimeType"
603
+ }
604
+ ],
605
+ requestBodies: ["application/json"],
606
+ responses: [
607
+ {
608
+ status: 200,
609
+ description: "Ok"
610
+ }
611
+ ]
612
+ },
613
+ {
614
+ id: "POST /SASjsApi/code/format",
615
+ method: "POST",
616
+ path: "/SASjsApi/code/format",
617
+ tag: "Code",
618
+ summary: "Format SAS code and return the formatted text",
619
+ params: [
620
+ {
621
+ name: "code",
622
+ in: "body",
623
+ required: true,
624
+ type: "string"
625
+ },
626
+ {
627
+ name: "filePath",
628
+ in: "body",
629
+ required: false,
630
+ type: "string"
631
+ }
632
+ ],
633
+ requestBodies: ["application/json"],
634
+ responses: [
635
+ {
636
+ status: 200,
637
+ description: "Ok"
638
+ }
639
+ ]
640
+ },
641
+ {
642
+ id: "POST /SASjsApi/code/lint",
643
+ method: "POST",
644
+ path: "/SASjsApi/code/lint",
645
+ tag: "Code",
646
+ summary: "Lint SAS code and return the diagnostics",
647
+ params: [
648
+ {
649
+ name: "code",
650
+ in: "body",
651
+ required: true,
652
+ type: "string"
653
+ },
654
+ {
655
+ name: "filePath",
656
+ in: "body",
657
+ required: false,
658
+ type: "string"
659
+ }
660
+ ],
661
+ requestBodies: ["application/json"],
662
+ responses: [
663
+ {
664
+ status: 200,
665
+ description: "Ok"
666
+ }
667
+ ]
668
+ },
669
+ {
670
+ id: "POST /SASjsApi/code/trigger",
671
+ method: "POST",
672
+ path: "/SASjsApi/code/trigger",
673
+ tag: "Code",
674
+ summary: "Trigger code and return a session ID",
675
+ params: [
676
+ {
677
+ name: "code",
678
+ in: "body",
679
+ required: true,
680
+ type: "string"
681
+ },
682
+ {
683
+ name: "runTime",
684
+ in: "body",
685
+ required: true,
686
+ type: "RunTimeType"
687
+ },
688
+ {
689
+ name: "expiresAfterMins",
690
+ in: "body",
691
+ required: false,
692
+ type: "number"
693
+ }
694
+ ],
695
+ requestBodies: ["application/json"],
696
+ responses: [
697
+ {
698
+ status: 200,
699
+ description: "Ok"
700
+ }
701
+ ]
702
+ },
703
+ {
704
+ id: "POST /SASjsApi/drive/deploy",
705
+ method: "POST",
706
+ path: "/SASjsApi/drive/deploy",
707
+ tag: "Drive",
708
+ summary: "Deploy a file tree to SASjs Drive",
709
+ params: [
710
+ {
711
+ name: "appLoc",
712
+ in: "body",
713
+ required: true,
714
+ type: "string"
715
+ },
716
+ {
717
+ name: "streamWebFolder",
718
+ in: "body",
719
+ required: false,
720
+ type: "string"
721
+ },
722
+ {
723
+ name: "fileTree",
724
+ in: "body",
725
+ required: true,
726
+ type: "FileTree"
727
+ }
728
+ ],
729
+ requestBodies: ["application/json"],
730
+ responses: [
731
+ {
732
+ status: 200,
733
+ description: "Ok"
734
+ },
735
+ {
736
+ status: 400,
737
+ description: "Invalid Format"
738
+ },
739
+ {
740
+ status: 500,
741
+ description: "Execution Error"
742
+ }
743
+ ]
744
+ },
745
+ {
746
+ id: "POST /SASjsApi/drive/deploy/upload",
747
+ method: "POST",
748
+ path: "/SASjsApi/drive/deploy/upload",
749
+ tag: "Drive",
750
+ summary: "Deploy an uploaded file to SASjs Drive",
751
+ params: [
752
+ {
753
+ name: "file",
754
+ in: "formData",
755
+ required: true,
756
+ type: "string"
757
+ }
758
+ ],
759
+ requestBodies: ["multipart/form-data"],
760
+ responses: [
761
+ {
762
+ status: 200,
763
+ description: "Ok"
764
+ },
765
+ {
766
+ status: 400,
767
+ description: "Invalid Format"
768
+ },
769
+ {
770
+ status: 500,
771
+ description: "Execution Error"
772
+ }
773
+ ]
774
+ },
775
+ {
776
+ id: "DELETE /SASjsApi/drive/file",
777
+ method: "DELETE",
778
+ path: "/SASjsApi/drive/file",
779
+ tag: "Drive",
780
+ summary: "Delete a file from SASjs Drive",
781
+ params: [
782
+ {
783
+ name: "_filePath",
784
+ in: "query",
785
+ required: true,
786
+ type: "string"
787
+ }
788
+ ],
789
+ requestBodies: [],
790
+ responses: [
791
+ {
792
+ status: 200,
793
+ description: "Ok"
794
+ },
795
+ {
796
+ status: 400,
797
+ description: "Can't delete file outside drive"
798
+ },
799
+ {
800
+ status: 404,
801
+ description: "File doesn't exist"
802
+ }
803
+ ]
804
+ },
805
+ {
806
+ id: "GET /SASjsApi/drive/file",
807
+ method: "GET",
808
+ path: "/SASjsApi/drive/file",
809
+ tag: "Drive",
810
+ summary: "Get a file from SASjs Drive",
811
+ params: [
812
+ {
813
+ name: "_filePath",
814
+ in: "query",
815
+ required: true,
816
+ type: "string"
817
+ }
818
+ ],
819
+ requestBodies: [],
820
+ responses: [
821
+ {
822
+ status: 200,
823
+ description: "The contents of the file"
824
+ },
825
+ {
826
+ status: 400,
827
+ description: "Can't get file outside drive"
828
+ },
829
+ {
830
+ status: 404,
831
+ description: "File doesn't exist"
832
+ }
833
+ ]
834
+ },
835
+ {
836
+ id: "PATCH /SASjsApi/drive/file",
837
+ method: "PATCH",
838
+ path: "/SASjsApi/drive/file",
839
+ tag: "Drive",
840
+ summary: "Replace a file in SASjs Drive",
841
+ params: [
842
+ {
843
+ name: "_filePath",
844
+ in: "query",
845
+ required: false,
846
+ type: "string"
847
+ },
848
+ {
849
+ name: "file",
850
+ in: "formData",
851
+ required: true,
852
+ type: "string"
853
+ },
854
+ {
855
+ name: "filePath",
856
+ in: "formData",
857
+ required: false,
858
+ type: "string"
859
+ }
860
+ ],
861
+ requestBodies: ["multipart/form-data"],
862
+ responses: [
863
+ {
864
+ status: 200,
865
+ description: "Ok"
866
+ },
867
+ {
868
+ status: 400,
869
+ description: "Can't modify file outside drive"
870
+ },
871
+ {
872
+ status: 404,
873
+ description: "File doesn't exist"
874
+ }
875
+ ]
876
+ },
877
+ {
878
+ id: "POST /SASjsApi/drive/file",
879
+ method: "POST",
880
+ path: "/SASjsApi/drive/file",
881
+ tag: "Drive",
882
+ summary: "Create a file in SASjs Drive",
883
+ params: [
884
+ {
885
+ name: "_filePath",
886
+ in: "query",
887
+ required: false,
888
+ type: "string"
889
+ },
890
+ {
891
+ name: "file",
892
+ in: "formData",
893
+ required: true,
894
+ type: "string"
895
+ },
896
+ {
897
+ name: "filePath",
898
+ in: "formData",
899
+ required: false,
900
+ type: "string"
901
+ }
902
+ ],
903
+ requestBodies: ["multipart/form-data"],
904
+ responses: [
905
+ {
906
+ status: 200,
907
+ description: "Ok"
908
+ },
909
+ {
910
+ status: 400,
911
+ description: "Can't put file outside drive"
912
+ },
913
+ {
914
+ status: 409,
915
+ description: "File already exists"
916
+ }
917
+ ]
918
+ },
919
+ {
920
+ id: "GET /SASjsApi/drive/filetree",
921
+ method: "GET",
922
+ path: "/SASjsApi/drive/filetree",
923
+ tag: "Drive",
924
+ summary: "Get the SASjs Drive file tree, or one folder of it",
925
+ params: [
926
+ {
927
+ name: "_folderPath",
928
+ in: "query",
929
+ required: false,
930
+ type: "string"
931
+ }
932
+ ],
933
+ requestBodies: [],
934
+ responses: [
935
+ {
936
+ status: 200,
937
+ description: "Ok"
938
+ }
939
+ ]
940
+ },
941
+ {
942
+ id: "DELETE /SASjsApi/drive/folder",
943
+ method: "DELETE",
944
+ path: "/SASjsApi/drive/folder",
945
+ tag: "Drive",
946
+ summary: "Delete a folder from SASjs Drive",
947
+ params: [
948
+ {
949
+ name: "_folderPath",
950
+ in: "query",
951
+ required: true,
952
+ type: "string"
953
+ }
954
+ ],
955
+ requestBodies: [],
956
+ responses: [
957
+ {
958
+ status: 200,
959
+ description: "Ok"
960
+ },
961
+ {
962
+ status: 400,
963
+ description: "Can't delete folder outside drive"
964
+ },
965
+ {
966
+ status: 404,
967
+ description: "Folder doesn't exist"
968
+ }
969
+ ]
970
+ },
971
+ {
972
+ id: "GET /SASjsApi/drive/folder",
973
+ method: "GET",
974
+ path: "/SASjsApi/drive/folder",
975
+ tag: "Drive",
976
+ summary: "List a folder in SASjs Drive",
977
+ params: [
978
+ {
979
+ name: "_folderPath",
980
+ in: "query",
981
+ required: false,
982
+ type: "string"
983
+ }
984
+ ],
985
+ requestBodies: [],
986
+ responses: [
987
+ {
988
+ status: 200,
989
+ description: "Ok"
990
+ },
991
+ {
992
+ status: 400,
993
+ description: "Can't get folder outside drive"
994
+ },
995
+ {
996
+ status: 404,
997
+ description: "Folder doesn't exist"
998
+ }
999
+ ]
1000
+ },
1001
+ {
1002
+ id: "POST /SASjsApi/drive/folder",
1003
+ method: "POST",
1004
+ path: "/SASjsApi/drive/folder",
1005
+ tag: "Drive",
1006
+ summary: "Create a folder in SASjs Drive",
1007
+ params: [
1008
+ {
1009
+ name: "folderPath",
1010
+ in: "body",
1011
+ required: true,
1012
+ type: "string"
1013
+ }
1014
+ ],
1015
+ requestBodies: ["application/json"],
1016
+ responses: [
1017
+ {
1018
+ status: 200,
1019
+ description: "Ok"
1020
+ },
1021
+ {
1022
+ status: 400,
1023
+ description: "Can't put folder outside drive"
1024
+ },
1025
+ {
1026
+ status: 409,
1027
+ description: "Folder already exists"
1028
+ }
1029
+ ]
1030
+ },
1031
+ {
1032
+ id: "POST /SASjsApi/drive/rename",
1033
+ method: "POST",
1034
+ path: "/SASjsApi/drive/rename",
1035
+ tag: "Drive",
1036
+ summary: "Rename or move a file or folder in SASjs Drive",
1037
+ params: [
1038
+ {
1039
+ name: "oldPath",
1040
+ in: "body",
1041
+ required: true,
1042
+ type: "string"
1043
+ },
1044
+ {
1045
+ name: "newPath",
1046
+ in: "body",
1047
+ required: true,
1048
+ type: "string"
1049
+ }
1050
+ ],
1051
+ requestBodies: ["application/json"],
1052
+ responses: [
1053
+ {
1054
+ status: 200,
1055
+ description: "Ok"
1056
+ },
1057
+ {
1058
+ status: 400,
1059
+ description: "Can't rename outside drive"
1060
+ },
1061
+ {
1062
+ status: 404,
1063
+ description: "No file/folder found for provided path"
1064
+ },
1065
+ {
1066
+ status: 409,
1067
+ description: "Something already exists at the new path"
1068
+ }
1069
+ ]
1070
+ },
1071
+ {
1072
+ id: "GET /SASjsApi/group",
1073
+ method: "GET",
1074
+ path: "/SASjsApi/group",
1075
+ tag: "Group",
1076
+ summary: "List all groups",
1077
+ params: [],
1078
+ requestBodies: [],
1079
+ responses: [
1080
+ {
1081
+ status: 200,
1082
+ description: "Ok"
1083
+ }
1084
+ ]
1085
+ },
1086
+ {
1087
+ id: "POST /SASjsApi/group",
1088
+ method: "POST",
1089
+ path: "/SASjsApi/group",
1090
+ tag: "Group",
1091
+ summary: "Create a group",
1092
+ params: [
1093
+ {
1094
+ name: "name",
1095
+ in: "body",
1096
+ required: true,
1097
+ type: "string"
1098
+ },
1099
+ {
1100
+ name: "description",
1101
+ in: "body",
1102
+ required: true,
1103
+ type: "string"
1104
+ },
1105
+ {
1106
+ name: "isActive",
1107
+ in: "body",
1108
+ required: false,
1109
+ type: "boolean"
1110
+ }
1111
+ ],
1112
+ requestBodies: ["application/json"],
1113
+ responses: [
1114
+ {
1115
+ status: 200,
1116
+ description: "Ok"
1117
+ }
1118
+ ]
1119
+ },
1120
+ {
1121
+ id: "DELETE /SASjsApi/group/{groupUid}/{userUid}",
1122
+ method: "DELETE",
1123
+ path: "/SASjsApi/group/{groupUid}/{userUid}",
1124
+ tag: "Group",
1125
+ summary: "Remove a user from a group",
1126
+ params: [
1127
+ {
1128
+ name: "groupUid",
1129
+ in: "path",
1130
+ required: true,
1131
+ type: "string"
1132
+ },
1133
+ {
1134
+ name: "userUid",
1135
+ in: "path",
1136
+ required: true,
1137
+ type: "string"
1138
+ }
1139
+ ],
1140
+ requestBodies: [],
1141
+ responses: [
1142
+ {
1143
+ status: 200,
1144
+ description: "Ok"
1145
+ }
1146
+ ]
1147
+ },
1148
+ {
1149
+ id: "POST /SASjsApi/group/{groupUid}/{userUid}",
1150
+ method: "POST",
1151
+ path: "/SASjsApi/group/{groupUid}/{userUid}",
1152
+ tag: "Group",
1153
+ summary: "Add a user to a group",
1154
+ params: [
1155
+ {
1156
+ name: "groupUid",
1157
+ in: "path",
1158
+ required: true,
1159
+ type: "string"
1160
+ },
1161
+ {
1162
+ name: "userUid",
1163
+ in: "path",
1164
+ required: true,
1165
+ type: "string"
1166
+ }
1167
+ ],
1168
+ requestBodies: [],
1169
+ responses: [
1170
+ {
1171
+ status: 200,
1172
+ description: "Ok"
1173
+ }
1174
+ ]
1175
+ },
1176
+ {
1177
+ id: "DELETE /SASjsApi/group/{uid}",
1178
+ method: "DELETE",
1179
+ path: "/SASjsApi/group/{uid}",
1180
+ tag: "Group",
1181
+ summary: "Delete a group",
1182
+ params: [
1183
+ {
1184
+ name: "uid",
1185
+ in: "path",
1186
+ required: true,
1187
+ type: "string"
1188
+ }
1189
+ ],
1190
+ requestBodies: [],
1191
+ responses: [
1192
+ {
1193
+ status: 200,
1194
+ description: "Ok"
1195
+ }
1196
+ ]
1197
+ },
1198
+ {
1199
+ id: "GET /SASjsApi/group/{uid}",
1200
+ method: "GET",
1201
+ path: "/SASjsApi/group/{uid}",
1202
+ tag: "Group",
1203
+ summary: "Get a group by id",
1204
+ params: [
1205
+ {
1206
+ name: "uid",
1207
+ in: "path",
1208
+ required: true,
1209
+ type: "string"
1210
+ }
1211
+ ],
1212
+ requestBodies: [],
1213
+ responses: [
1214
+ {
1215
+ status: 200,
1216
+ description: "Ok"
1217
+ }
1218
+ ]
1219
+ },
1220
+ {
1221
+ id: "GET /SASjsApi/group/by/groupname/{name}",
1222
+ method: "GET",
1223
+ path: "/SASjsApi/group/by/groupname/{name}",
1224
+ tag: "Group",
1225
+ summary: "Get a group by name",
1226
+ params: [
1227
+ {
1228
+ name: "name",
1229
+ in: "path",
1230
+ required: true,
1231
+ type: "string"
1232
+ }
1233
+ ],
1234
+ requestBodies: [],
1235
+ responses: [
1236
+ {
1237
+ status: 200,
1238
+ description: "Ok"
1239
+ }
1240
+ ]
1241
+ },
1242
+ {
1243
+ id: "GET /SASjsApi/info",
1244
+ method: "GET",
1245
+ path: "/SASjsApi/info",
1246
+ tag: "Info",
1247
+ summary: "Get public server information",
1248
+ params: [],
1249
+ requestBodies: [],
1250
+ responses: [
1251
+ {
1252
+ status: 200,
1253
+ description: "Ok"
1254
+ }
1255
+ ]
1256
+ },
1257
+ {
1258
+ id: "GET /SASjsApi/info/authorizedRoutes",
1259
+ method: "GET",
1260
+ path: "/SASjsApi/info/authorizedRoutes",
1261
+ tag: "Info",
1262
+ summary: "List the routes that accept permissions",
1263
+ params: [],
1264
+ requestBodies: [],
1265
+ responses: [
1266
+ {
1267
+ status: 200,
1268
+ description: "Ok"
1269
+ }
1270
+ ]
1271
+ },
1272
+ {
1273
+ id: "GET /SASjsApi/info/myAuthorizedRoutes",
1274
+ method: "GET",
1275
+ path: "/SASjsApi/info/myAuthorizedRoutes",
1276
+ tag: "Info",
1277
+ summary: "List the routes this caller may use",
1278
+ params: [],
1279
+ requestBodies: [],
1280
+ responses: [
1281
+ {
1282
+ status: 200,
1283
+ description: "Ok"
1284
+ }
1285
+ ]
1286
+ },
1287
+ {
1288
+ id: "GET /SASjsApi/permission",
1289
+ method: "GET",
1290
+ path: "/SASjsApi/permission",
1291
+ tag: "Permission",
1292
+ summary: "List the permission rules for the current user",
1293
+ params: [],
1294
+ requestBodies: [],
1295
+ responses: [
1296
+ {
1297
+ status: 200,
1298
+ description: "Ok"
1299
+ }
1300
+ ]
1301
+ },
1302
+ {
1303
+ id: "POST /SASjsApi/permission",
1304
+ method: "POST",
1305
+ path: "/SASjsApi/permission",
1306
+ tag: "Permission",
1307
+ summary: "Create a permission rule",
1308
+ params: [
1309
+ {
1310
+ name: "path",
1311
+ in: "body",
1312
+ required: true,
1313
+ type: "string"
1314
+ },
1315
+ {
1316
+ name: "type",
1317
+ in: "body",
1318
+ required: true,
1319
+ type: "PermissionType"
1320
+ },
1321
+ {
1322
+ name: "setting",
1323
+ in: "body",
1324
+ required: true,
1325
+ type: "PermissionSettingForRoute"
1326
+ },
1327
+ {
1328
+ name: "principalType",
1329
+ in: "body",
1330
+ required: true,
1331
+ type: "PrincipalType"
1332
+ },
1333
+ {
1334
+ name: "principalId",
1335
+ in: "body",
1336
+ required: true,
1337
+ type: "string"
1338
+ }
1339
+ ],
1340
+ requestBodies: ["application/json"],
1341
+ responses: [
1342
+ {
1343
+ status: 200,
1344
+ description: "Ok"
1345
+ }
1346
+ ]
1347
+ },
1348
+ {
1349
+ id: "DELETE /SASjsApi/permission/{uid}",
1350
+ method: "DELETE",
1351
+ path: "/SASjsApi/permission/{uid}",
1352
+ tag: "Permission",
1353
+ summary: "Delete a permission rule",
1354
+ params: [
1355
+ {
1356
+ name: "uid",
1357
+ in: "path",
1358
+ required: true,
1359
+ type: "string"
1360
+ }
1361
+ ],
1362
+ requestBodies: [],
1363
+ responses: [
1364
+ {
1365
+ status: 204,
1366
+ description: "No content"
1367
+ }
1368
+ ]
1369
+ },
1370
+ {
1371
+ id: "PATCH /SASjsApi/permission/{uid}",
1372
+ method: "PATCH",
1373
+ path: "/SASjsApi/permission/{uid}",
1374
+ tag: "Permission",
1375
+ summary: "Update a permission rule",
1376
+ params: [
1377
+ {
1378
+ name: "uid",
1379
+ in: "path",
1380
+ required: true,
1381
+ type: "string"
1382
+ },
1383
+ {
1384
+ name: "setting",
1385
+ in: "body",
1386
+ required: true,
1387
+ type: "PermissionSettingForRoute"
1388
+ }
1389
+ ],
1390
+ requestBodies: ["application/json"],
1391
+ responses: [
1392
+ {
1393
+ status: 200,
1394
+ description: "Ok"
1395
+ }
1396
+ ]
1397
+ },
1398
+ {
1399
+ id: "GET /SASjsApi/session",
1400
+ method: "GET",
1401
+ path: "/SASjsApi/session",
1402
+ tag: "Session",
1403
+ summary: "Get the current session",
1404
+ params: [],
1405
+ requestBodies: [],
1406
+ responses: [
1407
+ {
1408
+ status: 200,
1409
+ description: "Ok"
1410
+ }
1411
+ ]
1412
+ },
1413
+ {
1414
+ id: "GET /SASjsApi/session/{sessionId}",
1415
+ method: "GET",
1416
+ path: "/SASjsApi/session/{sessionId}",
1417
+ tag: "Session",
1418
+ summary: "Get a triggered session",
1419
+ params: [
1420
+ {
1421
+ name: "sessionId",
1422
+ in: "path",
1423
+ required: true,
1424
+ type: "string"
1425
+ }
1426
+ ],
1427
+ requestBodies: [],
1428
+ responses: [
1429
+ {
1430
+ status: 200,
1431
+ description: "Ok"
1432
+ }
1433
+ ]
1434
+ },
1435
+ {
1436
+ id: "GET /SASjsApi/session/{sessionId}/log",
1437
+ method: "GET",
1438
+ path: "/SASjsApi/session/{sessionId}/log",
1439
+ tag: "Session",
1440
+ summary: "Get the log of a triggered session",
1441
+ params: [
1442
+ {
1443
+ name: "sessionId",
1444
+ in: "path",
1445
+ required: true,
1446
+ type: "string"
1447
+ }
1448
+ ],
1449
+ requestBodies: [],
1450
+ responses: [
1451
+ {
1452
+ status: 200,
1453
+ description: "Ok"
1454
+ }
1455
+ ]
1456
+ },
1457
+ {
1458
+ id: "GET /SASjsApi/session/{sessionId}/output",
1459
+ method: "GET",
1460
+ path: "/SASjsApi/session/{sessionId}/output",
1461
+ tag: "Session",
1462
+ summary: "Get the output of a triggered session",
1463
+ params: [
1464
+ {
1465
+ name: "sessionId",
1466
+ in: "path",
1467
+ required: true,
1468
+ type: "string"
1469
+ }
1470
+ ],
1471
+ requestBodies: [],
1472
+ responses: [
1473
+ {
1474
+ status: 200,
1475
+ description: "Ok"
1476
+ }
1477
+ ]
1478
+ },
1479
+ {
1480
+ id: "GET /SASjsApi/session/{sessionId}/state",
1481
+ method: "GET",
1482
+ path: "/SASjsApi/session/{sessionId}/state",
1483
+ tag: "Session",
1484
+ summary: "Get the state of a session",
1485
+ params: [
1486
+ {
1487
+ name: "sessionId",
1488
+ in: "path",
1489
+ required: true,
1490
+ type: "string"
1491
+ }
1492
+ ],
1493
+ requestBodies: [],
1494
+ responses: [
1495
+ {
1496
+ status: 200,
1497
+ description: "Ok"
1498
+ }
1499
+ ]
1500
+ },
1501
+ {
1502
+ id: "GET /SASjsApi/stp/execute",
1503
+ method: "GET",
1504
+ path: "/SASjsApi/stp/execute",
1505
+ tag: "STP",
1506
+ summary: "Execute a stored program",
1507
+ params: [
1508
+ {
1509
+ name: "_program",
1510
+ in: "query",
1511
+ required: true,
1512
+ type: "string"
1513
+ },
1514
+ {
1515
+ name: "_debug",
1516
+ in: "query",
1517
+ required: false,
1518
+ type: "number"
1519
+ }
1520
+ ],
1521
+ requestBodies: [],
1522
+ responses: [
1523
+ {
1524
+ status: 200,
1525
+ description: "Ok"
1526
+ }
1527
+ ]
1528
+ },
1529
+ {
1530
+ id: "POST /SASjsApi/stp/execute",
1531
+ method: "POST",
1532
+ path: "/SASjsApi/stp/execute",
1533
+ tag: "STP",
1534
+ summary: "Execute a stored program",
1535
+ params: [
1536
+ {
1537
+ name: "_program",
1538
+ in: "query",
1539
+ required: false,
1540
+ type: "string"
1541
+ },
1542
+ {
1543
+ name: "_program",
1544
+ in: "body",
1545
+ required: false,
1546
+ type: "string"
1547
+ }
1548
+ ],
1549
+ requestBodies: ["application/json"],
1550
+ responses: [
1551
+ {
1552
+ status: 200,
1553
+ description: "Ok"
1554
+ }
1555
+ ]
1556
+ },
1557
+ {
1558
+ id: "POST /SASjsApi/stp/trigger",
1559
+ method: "POST",
1560
+ path: "/SASjsApi/stp/trigger",
1561
+ tag: "STP",
1562
+ summary: "Trigger a stored program and return a session ID",
1563
+ params: [
1564
+ {
1565
+ name: "_program",
1566
+ in: "query",
1567
+ required: true,
1568
+ type: "string"
1569
+ },
1570
+ {
1571
+ name: "_debug",
1572
+ in: "query",
1573
+ required: false,
1574
+ type: "number"
1575
+ },
1576
+ {
1577
+ name: "expiresAfterMins",
1578
+ in: "query",
1579
+ required: false,
1580
+ type: "number"
1581
+ }
1582
+ ],
1583
+ requestBodies: [],
1584
+ responses: [
1585
+ {
1586
+ status: 200,
1587
+ description: "Ok"
1588
+ }
1589
+ ]
1590
+ },
1591
+ {
1592
+ id: "GET /SASjsApi/user",
1593
+ method: "GET",
1594
+ path: "/SASjsApi/user",
1595
+ tag: "User",
1596
+ summary: "List all users",
1597
+ params: [],
1598
+ requestBodies: [],
1599
+ responses: [
1600
+ {
1601
+ status: 200,
1602
+ description: "Ok"
1603
+ }
1604
+ ]
1605
+ },
1606
+ {
1607
+ id: "POST /SASjsApi/user",
1608
+ method: "POST",
1609
+ path: "/SASjsApi/user",
1610
+ tag: "User",
1611
+ summary: "Create a user",
1612
+ params: [
1613
+ {
1614
+ name: "displayName",
1615
+ in: "body",
1616
+ required: true,
1617
+ type: "string"
1618
+ },
1619
+ {
1620
+ name: "username",
1621
+ in: "body",
1622
+ required: true,
1623
+ type: "string"
1624
+ },
1625
+ {
1626
+ name: "password",
1627
+ in: "body",
1628
+ required: true,
1629
+ type: "string"
1630
+ },
1631
+ {
1632
+ name: "isAdmin",
1633
+ in: "body",
1634
+ required: false,
1635
+ type: "boolean"
1636
+ },
1637
+ {
1638
+ name: "isActive",
1639
+ in: "body",
1640
+ required: false,
1641
+ type: "boolean"
1642
+ },
1643
+ {
1644
+ name: "autoExec",
1645
+ in: "body",
1646
+ required: false,
1647
+ type: "string"
1648
+ }
1649
+ ],
1650
+ requestBodies: ["application/json"],
1651
+ responses: [
1652
+ {
1653
+ status: 200,
1654
+ description: "Ok"
1655
+ }
1656
+ ]
1657
+ },
1658
+ {
1659
+ id: "DELETE /SASjsApi/user/{uid}",
1660
+ method: "DELETE",
1661
+ path: "/SASjsApi/user/{uid}",
1662
+ tag: "User",
1663
+ summary: "Delete a user by id",
1664
+ params: [
1665
+ {
1666
+ name: "uid",
1667
+ in: "path",
1668
+ required: true,
1669
+ type: "string"
1670
+ },
1671
+ {
1672
+ name: "password",
1673
+ in: "body",
1674
+ required: false,
1675
+ type: "string"
1676
+ }
1677
+ ],
1678
+ requestBodies: ["application/json"],
1679
+ responses: [
1680
+ {
1681
+ status: 204,
1682
+ description: "No content"
1683
+ }
1684
+ ]
1685
+ },
1686
+ {
1687
+ id: "GET /SASjsApi/user/{uid}",
1688
+ method: "GET",
1689
+ path: "/SASjsApi/user/{uid}",
1690
+ tag: "User",
1691
+ summary: "Get a user by id",
1692
+ params: [
1693
+ {
1694
+ name: "uid",
1695
+ in: "path",
1696
+ required: true,
1697
+ type: "string"
1698
+ }
1699
+ ],
1700
+ requestBodies: [],
1701
+ responses: [
1702
+ {
1703
+ status: 200,
1704
+ description: "Ok"
1705
+ }
1706
+ ]
1707
+ },
1708
+ {
1709
+ id: "PATCH /SASjsApi/user/{uid}",
1710
+ method: "PATCH",
1711
+ path: "/SASjsApi/user/{uid}",
1712
+ tag: "User",
1713
+ summary: "Update a user by id",
1714
+ params: [
1715
+ {
1716
+ name: "uid",
1717
+ in: "path",
1718
+ required: true,
1719
+ type: "string"
1720
+ },
1721
+ {
1722
+ name: "displayName",
1723
+ in: "body",
1724
+ required: true,
1725
+ type: "string"
1726
+ },
1727
+ {
1728
+ name: "username",
1729
+ in: "body",
1730
+ required: true,
1731
+ type: "string"
1732
+ },
1733
+ {
1734
+ name: "password",
1735
+ in: "body",
1736
+ required: true,
1737
+ type: "string"
1738
+ },
1739
+ {
1740
+ name: "isAdmin",
1741
+ in: "body",
1742
+ required: false,
1743
+ type: "boolean"
1744
+ },
1745
+ {
1746
+ name: "isActive",
1747
+ in: "body",
1748
+ required: false,
1749
+ type: "boolean"
1750
+ },
1751
+ {
1752
+ name: "autoExec",
1753
+ in: "body",
1754
+ required: false,
1755
+ type: "string"
1756
+ }
1757
+ ],
1758
+ requestBodies: ["application/json"],
1759
+ responses: [
1760
+ {
1761
+ status: 200,
1762
+ description: "Ok"
1763
+ }
1764
+ ]
1765
+ },
1766
+ {
1767
+ id: "DELETE /SASjsApi/user/by/username/{username}",
1768
+ method: "DELETE",
1769
+ path: "/SASjsApi/user/by/username/{username}",
1770
+ tag: "User",
1771
+ summary: "Delete a user by username",
1772
+ params: [
1773
+ {
1774
+ name: "username",
1775
+ in: "path",
1776
+ required: true,
1777
+ type: "string"
1778
+ },
1779
+ {
1780
+ name: "password",
1781
+ in: "body",
1782
+ required: false,
1783
+ type: "string"
1784
+ }
1785
+ ],
1786
+ requestBodies: ["application/json"],
1787
+ responses: [
1788
+ {
1789
+ status: 204,
1790
+ description: "No content"
1791
+ }
1792
+ ]
1793
+ },
1794
+ {
1795
+ id: "GET /SASjsApi/user/by/username/{username}",
1796
+ method: "GET",
1797
+ path: "/SASjsApi/user/by/username/{username}",
1798
+ tag: "User",
1799
+ summary: "Get a user by username",
1800
+ params: [
1801
+ {
1802
+ name: "username",
1803
+ in: "path",
1804
+ required: true,
1805
+ type: "string"
1806
+ }
1807
+ ],
1808
+ requestBodies: [],
1809
+ responses: [
1810
+ {
1811
+ status: 200,
1812
+ description: "Ok"
1813
+ }
1814
+ ]
1815
+ },
1816
+ {
1817
+ id: "PATCH /SASjsApi/user/by/username/{username}",
1818
+ method: "PATCH",
1819
+ path: "/SASjsApi/user/by/username/{username}",
1820
+ tag: "User",
1821
+ summary: "Update a user by username",
1822
+ params: [
1823
+ {
1824
+ name: "username",
1825
+ in: "path",
1826
+ required: true,
1827
+ type: "string"
1828
+ },
1829
+ {
1830
+ name: "displayName",
1831
+ in: "body",
1832
+ required: true,
1833
+ type: "string"
1834
+ },
1835
+ {
1836
+ name: "username",
1837
+ in: "body",
1838
+ required: true,
1839
+ type: "string"
1840
+ },
1841
+ {
1842
+ name: "password",
1843
+ in: "body",
1844
+ required: true,
1845
+ type: "string"
1846
+ },
1847
+ {
1848
+ name: "isAdmin",
1849
+ in: "body",
1850
+ required: false,
1851
+ type: "boolean"
1852
+ },
1853
+ {
1854
+ name: "isActive",
1855
+ in: "body",
1856
+ required: false,
1857
+ type: "boolean"
1858
+ },
1859
+ {
1860
+ name: "autoExec",
1861
+ in: "body",
1862
+ required: false,
1863
+ type: "string"
1864
+ }
1865
+ ],
1866
+ requestBodies: ["application/json"],
1867
+ responses: [
1868
+ {
1869
+ status: 200,
1870
+ description: "Ok"
1871
+ }
1872
+ ]
1873
+ },
1874
+ {
1875
+ id: "POST /SASLogon/authorize",
1876
+ method: "POST",
1877
+ path: "/SASLogon/authorize",
1878
+ tag: "Web",
1879
+ summary: "Issue an authorization code",
1880
+ params: [
1881
+ {
1882
+ name: "clientId",
1883
+ in: "body",
1884
+ required: true,
1885
+ type: "string"
1886
+ }
1887
+ ],
1888
+ requestBodies: ["application/json"],
1889
+ responses: [
1890
+ {
1891
+ status: 200,
1892
+ description: "Ok"
1893
+ }
1894
+ ]
1895
+ },
1896
+ {
1897
+ id: "POST /SASLogon/login",
1898
+ method: "POST",
1899
+ path: "/SASLogon/login",
1900
+ tag: "Web",
1901
+ summary: "Sign in with a username and password",
1902
+ params: [
1903
+ {
1904
+ name: "username",
1905
+ in: "body",
1906
+ required: true,
1907
+ type: "string"
1908
+ },
1909
+ {
1910
+ name: "password",
1911
+ in: "body",
1912
+ required: true,
1913
+ type: "string"
1914
+ }
1915
+ ],
1916
+ requestBodies: ["application/json"],
1917
+ responses: [
1918
+ {
1919
+ status: 200,
1920
+ description: "Ok"
1921
+ }
1922
+ ]
1923
+ },
1924
+ {
1925
+ id: "GET /SASLogon/logout",
1926
+ method: "GET",
1927
+ path: "/SASLogon/logout",
1928
+ tag: "Web",
1929
+ summary: "Sign out",
1930
+ params: [],
1931
+ requestBodies: [],
1932
+ responses: [
1933
+ {
1934
+ status: 200,
1935
+ description: "Ok"
1936
+ }
1937
+ ]
1938
+ }
1939
+ ];
1940
+
1941
+ // src/routes/overlay.ts
1942
+ var TAG_CAPABILITIES = {
1943
+ Drive: ["storage"],
1944
+ STP: ["runtime", "storage"],
1945
+ Code: ["runtime"],
1946
+ Session: ["runtime"],
1947
+ Info: [],
1948
+ Admin: ["storage"],
1949
+ Auth: ["identity"],
1950
+ "Auth Config": ["identity"],
1951
+ User: ["identity"],
1952
+ Group: ["identity"],
1953
+ Permission: ["identity"],
1954
+ Client: ["identity"],
1955
+ Web: ["identity"]
1956
+ };
1957
+ var success = { status: "success" };
1958
+ var OVERLAY = {
1959
+ // the route inventory reports what the CALLER may use, so it needs an identity
1960
+ // even though the rest of the Info tag needs nothing
1961
+ "GET /SASjsApi/info/authorizedRoutes": { capabilities: ["identity"] },
1962
+ "GET /SASjsApi/info/myAuthorizedRoutes": { capabilities: ["identity"] },
1963
+ // the body is the file itself, so there is nothing to annotate
1964
+ "GET /SASjsApi/drive/file": { implemented: true },
1965
+ "POST /SASjsApi/drive/file": {
1966
+ implemented: true,
1967
+ responses: { 200: success, 409: "File already exists." }
1968
+ },
1969
+ "PATCH /SASjsApi/drive/file": {
1970
+ implemented: true,
1971
+ responses: { 200: success, 404: `File doesn't exist.` }
1972
+ },
1973
+ "DELETE /SASjsApi/drive/file": {
1974
+ implemented: true,
1975
+ responses: { 200: success, 404: `File doesn't exist.` }
1976
+ },
1977
+ "GET /SASjsApi/drive/folder": {
1978
+ implemented: true,
1979
+ responses: { 200: { files: [], folders: [] } }
1980
+ },
1981
+ "POST /SASjsApi/drive/folder": {
1982
+ implemented: true,
1983
+ responses: { 200: success, 409: "Folder already exists." }
1984
+ },
1985
+ "DELETE /SASjsApi/drive/folder": {
1986
+ implemented: true,
1987
+ responses: { 200: success, 404: `Folder doesn't exist.` }
1988
+ },
1989
+ "POST /SASjsApi/drive/rename": {
1990
+ implemented: true,
1991
+ responses: {
1992
+ 200: success,
1993
+ 409: "File with new name already exists.",
1994
+ 404: "No file/folder found for provided path."
1995
+ }
1996
+ },
1997
+ "GET /SASjsApi/drive/filetree": {
1998
+ implemented: true,
1999
+ responses: { 200: { status: "success", tree: {} } }
2000
+ },
2001
+ // The two deploy routes write a whole tree in one request. They are the only
2002
+ // ones where a caller can exhaust the host's storage, so they carry a cap -
2003
+ // see the plan in sasjs/cli#1550 - and they are not implemented yet.
2004
+ "POST /SASjsApi/drive/deploy": {
2005
+ implemented: false
2006
+ },
2007
+ "POST /SASjsApi/drive/deploy/upload": {
2008
+ implemented: false
2009
+ }
2010
+ };
2011
+
2012
+ // src/routes/index.ts
2013
+ var merge = (generated) => {
2014
+ const overlay = OVERLAY[generated.id] || {};
2015
+ const documented = new Map(
2016
+ generated.responses.map((r) => [r.status, r.description])
2017
+ );
2018
+ for (const status of Object.keys(overlay.responses || {}).map(Number)) {
2019
+ if (!documented.has(status)) {
2020
+ throw new Error(
2021
+ `src/routes/overlay.ts annotates status ${status} for ${generated.id}, which the OpenAPI document does not state. Correct the document in sasjs/server and re-vendor spec/swagger.yaml, or drop the annotation.`
2022
+ );
2023
+ }
2024
+ }
2025
+ const responses = [...documented.keys()].sort((a, b) => a - b).map((status) => {
2026
+ const body = overlay.responses ? overlay.responses[status] : void 0;
2027
+ return {
2028
+ status,
2029
+ description: documented.get(status) || "",
2030
+ ...body === void 0 ? {} : { body }
2031
+ };
2032
+ });
2033
+ return {
2034
+ ...generated,
2035
+ responses,
2036
+ capabilities: overlay.capabilities || TAG_CAPABILITIES[generated.tag] || [],
2037
+ implemented: overlay.implemented === true
2038
+ };
2039
+ };
2040
+ var ROUTES = GENERATED_ROUTES.map(merge);
2041
+ var known = new Set(ROUTES.map((route) => route.id));
2042
+ for (const id of Object.keys(OVERLAY))
2043
+ if (!known.has(id))
2044
+ throw new Error(
2045
+ `src/routes/overlay.ts names an operation the OpenAPI document does not have: ${id}. Remove the entry or refresh spec/swagger.yaml.`
2046
+ );
2047
+ var byId = new Map(ROUTES.map((route) => [route.id, route]));
2048
+ var routeById = (id) => byId.get(id);
2049
+ var escape = (segment) => segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2050
+ var compiled = ROUTES.filter((route) => route.path.includes("{")).map(
2051
+ (route) => {
2052
+ const keys = [];
2053
+ const pattern = route.path.split("/").map((segment) => {
2054
+ if (segment.startsWith("{") && segment.endsWith("}")) {
2055
+ keys.push(segment.slice(1, -1));
2056
+ return "([^/]+)";
2057
+ }
2058
+ return escape(segment);
2059
+ }).join("/");
2060
+ return { route, keys, regex: new RegExp("^" + pattern + "/?$") };
2061
+ }
2062
+ );
2063
+ var exact = /* @__PURE__ */ new Map();
2064
+ for (const route of ROUTES)
2065
+ if (!route.path.includes("{"))
2066
+ exact.set(route.method + " " + route.path, route);
2067
+ var findRoute = (method, path) => {
2068
+ const verb = method.toUpperCase();
2069
+ const clean = path.split("?")[0].replace(/\/+$/, "") || "/";
2070
+ const hit = exact.get(verb + " " + clean);
2071
+ if (hit) return { route: hit, params: {} };
2072
+ for (const candidate of compiled) {
2073
+ if (candidate.route.method !== verb) continue;
2074
+ const match = candidate.regex.exec(clean);
2075
+ if (!match) continue;
2076
+ const params = {};
2077
+ candidate.keys.forEach((key, index) => {
2078
+ try {
2079
+ params[key] = decodeURIComponent(match[index + 1]);
2080
+ } catch {
2081
+ params[key] = match[index + 1];
2082
+ }
2083
+ });
2084
+ return { route: candidate.route, params };
2085
+ }
2086
+ return void 0;
2087
+ };
2088
+
2089
+ // src/host/dispatch.ts
2090
+ var validate = (route, request) => {
2091
+ for (const param of route.params) {
2092
+ if (!param.required || param.in === "formData" || param.in === "header")
2093
+ continue;
2094
+ if (param.in === "path") continue;
2095
+ const given = param.in === "query" ? request.query?.[param.name] : request.body?.[param.name];
2096
+ if (given === void 0 || given === "")
2097
+ throw new ApiError(400, `"${param.name}" is required`);
2098
+ }
2099
+ };
2100
+ var targetPath = (request) => {
2101
+ const fromQuery = request.query?._filePath;
2102
+ const fromBody = request.body?.filePath;
2103
+ const path = fromQuery !== void 0 ? String(fromQuery) : fromBody;
2104
+ if (path === void 0) throw new ApiError(400, '"_filePath" is required');
2105
+ return path;
2106
+ };
2107
+ var dispatch = async (services, routeId, request = {}) => {
2108
+ const route = routeById(routeId);
2109
+ if (!route) throw new ApiError(404, `No such operation: ${routeId}`);
2110
+ validate(route, request);
2111
+ const { drive } = services;
2112
+ const query = request.query || {};
2113
+ switch (routeId) {
2114
+ case "GET /SASjsApi/drive/file": {
2115
+ const file = await drive.read(String(query._filePath || ""));
2116
+ return { status: 200, contents: file.contents };
2117
+ }
2118
+ case "POST /SASjsApi/drive/file":
2119
+ case "PATCH /SASjsApi/drive/file": {
2120
+ if (!request.file) throw new ApiError(400, '"file" is not present.');
2121
+ const path = targetPath(request);
2122
+ const result = routeId === "PATCH /SASjsApi/drive/file" ? await drive.update(path, request.file.contents) : await drive.save(path, request.file.contents);
2123
+ return { status: 200, body: result };
2124
+ }
2125
+ case "DELETE /SASjsApi/drive/file":
2126
+ return {
2127
+ status: 200,
2128
+ body: await drive.remove(String(query._filePath || ""))
2129
+ };
2130
+ case "GET /SASjsApi/drive/folder":
2131
+ return { status: 200, body: await drive.list(query._folderPath) };
2132
+ case "POST /SASjsApi/drive/folder":
2133
+ return {
2134
+ status: 200,
2135
+ body: await drive.mkdir(String(request.body?.folderPath || ""))
2136
+ };
2137
+ case "DELETE /SASjsApi/drive/folder":
2138
+ return {
2139
+ status: 200,
2140
+ body: await drive.rmdir(String(query._folderPath || ""))
2141
+ };
2142
+ case "POST /SASjsApi/drive/rename":
2143
+ return {
2144
+ status: 200,
2145
+ body: await drive.rename(
2146
+ String(request.body?.oldPath || ""),
2147
+ String(request.body?.newPath || "")
2148
+ )
2149
+ };
2150
+ case "GET /SASjsApi/drive/filetree":
2151
+ return { status: 200, body: await drive.tree(query._folderPath) };
2152
+ default:
2153
+ throw new ApiError(501, `This package has no handler for ${routeId}.`);
2154
+ }
2155
+ };
2156
+ var HANDLED_ROUTES = [
2157
+ "GET /SASjsApi/drive/file",
2158
+ "POST /SASjsApi/drive/file",
2159
+ "PATCH /SASjsApi/drive/file",
2160
+ "DELETE /SASjsApi/drive/file",
2161
+ "GET /SASjsApi/drive/folder",
2162
+ "POST /SASjsApi/drive/folder",
2163
+ "DELETE /SASjsApi/drive/folder",
2164
+ "POST /SASjsApi/drive/rename",
2165
+ "GET /SASjsApi/drive/filetree"
2166
+ ];
2167
+
2168
+ // src/corpus/drive.ts
2169
+ var success2 = { status: "success" };
2170
+ var bytes = (...values) => Uint8Array.from(values);
2171
+ var DRIVE_CORPUS = [
2172
+ {
2173
+ id: "create-file",
2174
+ route: "POST /SASjsApi/drive/file",
2175
+ description: "creates a file the drive did not have",
2176
+ request: {
2177
+ query: { _filePath: "/data/users.csv" },
2178
+ file: { name: "users.csv", contents: "id,name\n1,Ann\n" }
2179
+ },
2180
+ expect: { status: 200, body: success2 }
2181
+ },
2182
+ {
2183
+ id: "create-file-at-form-path",
2184
+ route: "POST /SASjsApi/drive/file",
2185
+ description: "takes the destination from the form field when the query has none",
2186
+ request: {
2187
+ file: { name: "x.csv", contents: "x" },
2188
+ body: { filePath: "/data/from-form.csv" }
2189
+ },
2190
+ expect: { status: 200, body: success2 }
2191
+ },
2192
+ {
2193
+ id: "create-file-over-existing",
2194
+ route: "POST /SASjsApi/drive/file",
2195
+ description: "refuses a create over a file that exists, as 409",
2196
+ seed: { "/data/exists.csv": "a" },
2197
+ request: {
2198
+ query: { _filePath: "/data/exists.csv" },
2199
+ file: { name: "x", contents: "b" }
2200
+ },
2201
+ expect: { status: 409, error: "File already exists." }
2202
+ },
2203
+ {
2204
+ id: "create-file-blocked-extension",
2205
+ route: "POST /SASjsApi/drive/file",
2206
+ description: "refuses a refused extension before touching the drive",
2207
+ request: {
2208
+ query: { _filePath: "/data/run.sh" },
2209
+ file: { name: "run.sh", contents: "x" }
2210
+ },
2211
+ expect: { status: 400, error: "Invalid file extension" }
2212
+ },
2213
+ {
2214
+ id: "create-file-outside-drive",
2215
+ route: "POST /SASjsApi/drive/file",
2216
+ description: "refuses a path that climbs out of the drive",
2217
+ request: {
2218
+ query: { _filePath: "/../escape.csv" },
2219
+ file: { name: "x", contents: "x" }
2220
+ },
2221
+ expect: { status: 400, error: "Can't put file outside drive." }
2222
+ },
2223
+ {
2224
+ id: "create-file-without-payload",
2225
+ route: "POST /SASjsApi/drive/file",
2226
+ description: "answers for a missing file with the server wording",
2227
+ request: { query: { _filePath: "/data/x.csv" } },
2228
+ expect: { status: 400, error: '"file" is not present.' }
2229
+ },
2230
+ {
2231
+ id: "create-file-without-destination",
2232
+ route: "POST /SASjsApi/drive/file",
2233
+ description: "answers for a missing destination with the validation wording",
2234
+ request: { file: { name: "x", contents: "x" } },
2235
+ expect: { status: 400, error: '"_filePath" is required' }
2236
+ },
2237
+ {
2238
+ id: "replace-file",
2239
+ route: "PATCH /SASjsApi/drive/file",
2240
+ description: "replaces the contents of a file that exists",
2241
+ seed: { "/data/keep.csv": "old" },
2242
+ request: {
2243
+ query: { _filePath: "/data/keep.csv" },
2244
+ file: { name: "keep.csv", contents: "new" }
2245
+ },
2246
+ expect: { status: 200, body: success2 }
2247
+ },
2248
+ {
2249
+ id: "replace-file-missing",
2250
+ route: "PATCH /SASjsApi/drive/file",
2251
+ description: "refuses a replace of a file that is not there, as 404",
2252
+ request: {
2253
+ query: { _filePath: "/data/missing.csv" },
2254
+ file: { name: "x", contents: "x" }
2255
+ },
2256
+ expect: { status: 404, error: `File doesn't exist.` }
2257
+ },
2258
+ {
2259
+ id: "replace-file-outside-drive",
2260
+ route: "PATCH /SASjsApi/drive/file",
2261
+ description: "refuses a replace of a path that climbs out of the drive",
2262
+ request: {
2263
+ query: { _filePath: "/../escape.csv" },
2264
+ file: { name: "x", contents: "x" }
2265
+ },
2266
+ expect: { status: 400, error: "Can't modify file outside drive." }
2267
+ },
2268
+ {
2269
+ id: "read-file",
2270
+ route: "GET /SASjsApi/drive/file",
2271
+ description: "answers with the file contents",
2272
+ seed: { "/data/keep.csv": "a,b\n1,2\n" },
2273
+ request: { query: { _filePath: "/data/keep.csv" } },
2274
+ expect: { status: 200, contents: "a,b\n1,2\n" }
2275
+ },
2276
+ {
2277
+ id: "read-file-missing",
2278
+ route: "GET /SASjsApi/drive/file",
2279
+ description: "refuses a read of a file that is not there",
2280
+ request: { query: { _filePath: "/data/missing.csv" } },
2281
+ expect: { status: 404, error: `File doesn't exist.` }
2282
+ },
2283
+ {
2284
+ id: "read-file-outside-drive",
2285
+ route: "GET /SASjsApi/drive/file",
2286
+ description: "refuses a read of a path that climbs out of the drive",
2287
+ request: { query: { _filePath: "/../escape.csv" } },
2288
+ expect: { status: 400, error: "Can't get file outside drive." }
2289
+ },
2290
+ {
2291
+ id: "delete-file",
2292
+ route: "DELETE /SASjsApi/drive/file",
2293
+ description: "deletes a file that exists",
2294
+ seed: { "/data/keep.csv": "a" },
2295
+ request: { query: { _filePath: "/data/keep.csv" } },
2296
+ expect: { status: 200, body: success2 }
2297
+ },
2298
+ {
2299
+ id: "delete-file-missing",
2300
+ route: "DELETE /SASjsApi/drive/file",
2301
+ description: "refuses a delete of a file that is not there",
2302
+ request: { query: { _filePath: "/data/missing.csv" } },
2303
+ expect: { status: 404, error: `File doesn't exist.` }
2304
+ },
2305
+ {
2306
+ id: "delete-file-outside-drive",
2307
+ route: "DELETE /SASjsApi/drive/file",
2308
+ description: "refuses a delete of a path that climbs out of the drive",
2309
+ request: { query: { _filePath: "/../escape.csv" } },
2310
+ expect: { status: 400, error: "Can't delete file outside drive." }
2311
+ },
2312
+ {
2313
+ id: "list-folder",
2314
+ route: "GET /SASjsApi/drive/folder",
2315
+ description: "lists the files and folders of a folder",
2316
+ seed: { "/data/a.csv": "a", "/data/reports/b.csv": "b" },
2317
+ request: { query: { _folderPath: "/data" } },
2318
+ expect: { status: 200, body: { files: ["a.csv"], folders: ["reports"] } }
2319
+ },
2320
+ {
2321
+ id: "list-folder-missing",
2322
+ route: "GET /SASjsApi/drive/folder",
2323
+ description: "refuses a listing of a folder that is not there",
2324
+ request: { query: { _folderPath: "/nope" } },
2325
+ expect: { status: 404, error: `Folder doesn't exist.` }
2326
+ },
2327
+ {
2328
+ id: "list-folder-outside-drive",
2329
+ route: "GET /SASjsApi/drive/folder",
2330
+ description: "refuses a listing of a path that climbs out of the drive",
2331
+ request: { query: { _folderPath: "/../escape" } },
2332
+ expect: { status: 400, error: "Can't get folder outside drive." }
2333
+ },
2334
+ {
2335
+ id: "delete-folder-without-path",
2336
+ route: "DELETE /SASjsApi/drive/folder",
2337
+ description: "answers for a missing folder path, because the table marks it required",
2338
+ request: {},
2339
+ expect: { status: 400, error: '"_folderPath" is required' }
2340
+ },
2341
+ {
2342
+ id: "create-folder-without-path",
2343
+ route: "POST /SASjsApi/drive/folder",
2344
+ description: "answers for a missing folder path in the body",
2345
+ request: { body: {} },
2346
+ expect: { status: 400, error: '"folderPath" is required' }
2347
+ },
2348
+ {
2349
+ id: "create-folder",
2350
+ route: "POST /SASjsApi/drive/folder",
2351
+ description: "creates a folder",
2352
+ request: { body: { folderPath: "/data/reports" } },
2353
+ expect: { status: 200, body: success2 }
2354
+ },
2355
+ {
2356
+ id: "create-folder-existing",
2357
+ route: "POST /SASjsApi/drive/folder",
2358
+ description: "refuses a folder that already exists, as 409",
2359
+ seed: { "/data/reports/a.csv": "a" },
2360
+ request: { body: { folderPath: "/data/reports" } },
2361
+ expect: { status: 409, error: "Folder already exists." }
2362
+ },
2363
+ {
2364
+ id: "delete-folder",
2365
+ route: "DELETE /SASjsApi/drive/folder",
2366
+ description: "deletes a folder with everything under it",
2367
+ seed: { "/data/reports/a.csv": "a", "/data/reports/nested/b.csv": "b" },
2368
+ request: { query: { _folderPath: "/data/reports" } },
2369
+ expect: { status: 200, body: success2 }
2370
+ },
2371
+ {
2372
+ id: "delete-folder-missing",
2373
+ route: "DELETE /SASjsApi/drive/folder",
2374
+ description: "refuses a delete of a folder that is not there",
2375
+ request: { query: { _folderPath: "/nope" } },
2376
+ expect: { status: 404, error: `Folder doesn't exist.` }
2377
+ },
2378
+ {
2379
+ id: "rename-file",
2380
+ route: "POST /SASjsApi/drive/rename",
2381
+ description: "moves a file to a new path",
2382
+ seed: { "/data/old.csv": "r" },
2383
+ request: { body: { oldPath: "/data/old.csv", newPath: "/data/new.csv" } },
2384
+ expect: { status: 200, body: success2 }
2385
+ },
2386
+ {
2387
+ id: "rename-onto-existing",
2388
+ route: "POST /SASjsApi/drive/rename",
2389
+ description: "refuses a move onto something that exists",
2390
+ seed: { "/data/a.csv": "a", "/data/b.csv": "b" },
2391
+ request: { body: { oldPath: "/data/a.csv", newPath: "/data/b.csv" } },
2392
+ expect: { status: 409, error: "File with new name already exists." }
2393
+ },
2394
+ {
2395
+ id: "rename-missing",
2396
+ route: "POST /SASjsApi/drive/rename",
2397
+ description: "refuses a move of something that is not there",
2398
+ request: { body: { oldPath: "/data/nope.csv", newPath: "/data/x.csv" } },
2399
+ expect: { status: 404, error: "No file/folder found for provided path." }
2400
+ },
2401
+ {
2402
+ id: "rename-folder-into-itself",
2403
+ route: "POST /SASjsApi/drive/rename",
2404
+ description: "refuses a folder moved inside itself, which the server loses at the filesystem",
2405
+ seed: { "/data/reports/a.csv": "a" },
2406
+ request: {
2407
+ body: { oldPath: "/data/reports", newPath: "/data/reports/nested" }
2408
+ },
2409
+ expect: {
2410
+ status: 400,
2411
+ error: "New path can't be inside the folder being moved."
2412
+ }
2413
+ },
2414
+ {
2415
+ id: "tree",
2416
+ route: "GET /SASjsApi/drive/filetree",
2417
+ description: "answers with the drive tree",
2418
+ seed: { "/data/a.csv": "a" },
2419
+ request: { query: { _folderPath: "/data" } },
2420
+ expect: {
2421
+ status: 200,
2422
+ body: {
2423
+ status: "success",
2424
+ tree: {
2425
+ name: "data",
2426
+ relativePath: "/data",
2427
+ isFolder: true,
2428
+ children: [
2429
+ {
2430
+ name: "a.csv",
2431
+ relativePath: "/data/a.csv",
2432
+ isFolder: false,
2433
+ children: []
2434
+ }
2435
+ ]
2436
+ }
2437
+ }
2438
+ }
2439
+ },
2440
+ {
2441
+ id: "bytes-round-trip",
2442
+ route: "GET /SASjsApi/drive/file",
2443
+ description: "keeps a byte file as bytes",
2444
+ seed: { "/data/pack.zip": bytes(80, 75, 3, 4) },
2445
+ request: { query: { _filePath: "/data/pack.zip" } },
2446
+ expect: { status: 200, contents: bytes(80, 75, 3, 4) }
2447
+ }
2448
+ ];
2449
+
2450
+ // src/corpus/runner.ts
2451
+ var servicesFor = (testCase) => ({
2452
+ drive: createDriveService(new MemoryDriveStore(testCase.seed || {}))
2453
+ });
2454
+ var runCase = async (testCase, services = servicesFor(testCase)) => {
2455
+ try {
2456
+ const response = await dispatch(services, testCase.route, testCase.request);
2457
+ return {
2458
+ status: response.status,
2459
+ body: response.body,
2460
+ contents: response.contents
2461
+ };
2462
+ } catch (error) {
2463
+ if (isApiError(error)) return { status: error.status, error: error.message };
2464
+ throw error;
2465
+ }
2466
+ };
2467
+ return __toCommonJS(index_exports);
2468
+ })();
2469
+ if (typeof self !== 'undefined') self.SASjsApi = SASjsApi