@sasjs/api 0.4.0 → 0.6.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 (39) hide show
  1. package/README.md +12 -13
  2. package/build/browser.js +515 -54
  3. package/build/cjs/corpus/drive.js +119 -0
  4. package/build/cjs/corpus/runner.js +18 -2
  5. package/build/cjs/drive/deploy.js +270 -0
  6. package/build/cjs/drive/service.js +5 -0
  7. package/build/cjs/host/dispatch.js +15 -2
  8. package/build/cjs/index.js +10 -1
  9. package/build/cjs/routes/generated.js +8 -0
  10. package/build/cjs/routes/index.js +8 -2
  11. package/build/cjs/routes/overlay.js +181 -40
  12. package/build/cjs/types.js +11 -1
  13. package/build/esm/corpus/drive.d.ts +6 -0
  14. package/build/esm/corpus/drive.d.ts.map +1 -1
  15. package/build/esm/corpus/drive.js +119 -0
  16. package/build/esm/corpus/runner.d.ts.map +1 -1
  17. package/build/esm/corpus/runner.js +18 -2
  18. package/build/esm/drive/deploy.d.ts +143 -0
  19. package/build/esm/drive/deploy.d.ts.map +1 -0
  20. package/build/esm/drive/deploy.js +262 -0
  21. package/build/esm/drive/service.d.ts +8 -0
  22. package/build/esm/drive/service.d.ts.map +1 -1
  23. package/build/esm/drive/service.js +5 -0
  24. package/build/esm/host/dispatch.d.ts.map +1 -1
  25. package/build/esm/host/dispatch.js +15 -2
  26. package/build/esm/index.d.ts +2 -0
  27. package/build/esm/index.d.ts.map +1 -1
  28. package/build/esm/index.js +1 -0
  29. package/build/esm/routes/generated.d.ts.map +1 -1
  30. package/build/esm/routes/generated.js +8 -0
  31. package/build/esm/routes/index.d.ts.map +1 -1
  32. package/build/esm/routes/index.js +8 -2
  33. package/build/esm/routes/overlay.d.ts +26 -3
  34. package/build/esm/routes/overlay.d.ts.map +1 -1
  35. package/build/esm/routes/overlay.js +181 -40
  36. package/build/esm/types.d.ts +26 -1
  37. package/build/esm/types.d.ts.map +1 -1
  38. package/build/esm/types.js +11 -1
  39. package/package.json +1 -1
@@ -8,6 +8,12 @@ const success = { status: 'success' };
8
8
  * host that does not have one. An explicit byte array is both.
9
9
  */
10
10
  const bytes = (...values) => Uint8Array.from(values);
11
+ /** A flat tree of `count` empty file members, for the cap's entry dimension. */
12
+ const manyMembers = (count) => Array.from({ length: count }, (_, at) => ({
13
+ name: `f${at}.txt`,
14
+ type: 'file',
15
+ code: ''
16
+ }));
11
17
  exports.DRIVE_CORPUS = [
12
18
  {
13
19
  id: 'create-file',
@@ -297,6 +303,119 @@ exports.DRIVE_CORPUS = [
297
303
  }
298
304
  }
299
305
  },
306
+ {
307
+ id: 'deploy-tree',
308
+ route: 'POST /SASjsApi/drive/deploy',
309
+ description: 'writes a whole tree under the appLoc it names',
310
+ request: {
311
+ body: {
312
+ appLoc: '/Public/apps/demo',
313
+ fileTree: {
314
+ members: [
315
+ {
316
+ name: 'services',
317
+ type: 'folder',
318
+ members: [{ name: 'run', type: 'service', code: '%put hi;' }]
319
+ },
320
+ { name: 'index.html', type: 'file', code: 'PGgxPkhlbGxvPC9oMT4=' }
321
+ ]
322
+ }
323
+ }
324
+ },
325
+ expect: {
326
+ status: 200,
327
+ body: {
328
+ status: 'success',
329
+ message: 'Files deployed successfully to @sasjs/server.'
330
+ }
331
+ }
332
+ },
333
+ {
334
+ id: 'deploy-refused-by-entry-count',
335
+ route: 'POST /SASjsApi/drive/deploy',
336
+ description: 'refuses a tree over the entry cap, naming the figure and the cap',
337
+ request: {
338
+ body: {
339
+ appLoc: '/Public/apps/many',
340
+ fileTree: { members: manyMembers(10001) }
341
+ }
342
+ },
343
+ expect: {
344
+ status: 413,
345
+ body: {
346
+ status: 'failure',
347
+ message: 'The file tree exceeds the deploy limit: 10001 entries (maximum 10000).'
348
+ },
349
+ error: 'The file tree exceeds the deploy limit: 10001 entries (maximum 10000).'
350
+ }
351
+ },
352
+ {
353
+ id: 'deploy-refused-by-name',
354
+ route: 'POST /SASjsApi/drive/deploy',
355
+ description: 'refuses a member name that would escape the deployment root',
356
+ request: {
357
+ body: {
358
+ appLoc: '/Public/apps/hostile',
359
+ fileTree: {
360
+ members: [{ name: '../escape', type: 'service', code: '%put hi;' }]
361
+ }
362
+ }
363
+ },
364
+ expect: {
365
+ status: 400,
366
+ body: {
367
+ status: 'failure',
368
+ message: "Invalid member name: '../escape.sas'. Member names must be single path segments (no '/', '\\', '..')."
369
+ },
370
+ error: "Invalid member name: '../escape.sas'. Member names must be single path segments (no '/', '\\', '..')."
371
+ }
372
+ },
373
+ {
374
+ id: 'deploy-refused-by-shape',
375
+ route: 'POST /SASjsApi/drive/deploy',
376
+ description: 'refuses a fileTree that is not a tree, in the routes own envelope',
377
+ request: { body: { appLoc: '/Public/apps/bad', fileTree: { members: 1 } } },
378
+ expect: {
379
+ status: 400,
380
+ body: {
381
+ status: 'failure',
382
+ message: 'Provided not supported data format.'
383
+ },
384
+ error: 'Provided not supported data format.'
385
+ }
386
+ },
387
+ {
388
+ id: 'deploy-refused-by-store',
389
+ route: 'POST /SASjsApi/drive/deploy',
390
+ description: 'answers the routes 500 when the drive refuses the write',
391
+ unwritable: true,
392
+ request: {
393
+ body: {
394
+ appLoc: '/Public/apps/demo',
395
+ fileTree: {
396
+ members: [{ name: 'run', type: 'service', code: '%put hi;' }]
397
+ }
398
+ }
399
+ },
400
+ expect: {
401
+ status: 500,
402
+ body: { status: 'failure', message: 'Deployment failed!' },
403
+ error: 'Deployment failed!'
404
+ }
405
+ },
406
+ {
407
+ id: 'deploy-refused-outside-drive',
408
+ route: 'POST /SASjsApi/drive/deploy',
409
+ description: 'refuses an appLoc that resolves outside the drive',
410
+ request: {
411
+ body: { appLoc: '/../outside', fileTree: { members: [] } }
412
+ },
413
+ expect: {
414
+ status: 400,
415
+ body: { status: 'failure', message: 'appLoc cannot be outside drive.' },
416
+ error: 'appLoc cannot be outside drive.'
417
+ }
418
+ },
300
419
  {
301
420
  id: 'bytes-round-trip',
302
421
  route: 'GET /SASjsApi/drive/file',
@@ -15,10 +15,26 @@ const types_js_1 = require("../types.js");
15
15
  * mid-flow can declare the tokens and codes it begins with.
16
16
  */
17
17
  const servicesFor = (testCase) => ({
18
- drive: (0, service_js_1.createDriveService)(new memory_store_js_1.MemoryDriveStore(testCase.seed || {})),
18
+ drive: (0, service_js_1.createDriveService)(testCase.unwritable
19
+ ? new RefusingDriveStore(testCase.seed || {})
20
+ : new memory_store_js_1.MemoryDriveStore(testCase.seed || {})),
19
21
  identity: (0, service_js_2.createIdentityService)(new memory_store_js_2.MemoryIdentityStore(testCase.identity || fixture_js_1.DEFAULT_IDENTITY))
20
22
  });
21
23
  exports.servicesFor = servicesFor;
24
+ /**
25
+ * A drive that reads but refuses every write.
26
+ *
27
+ * Only the corpus uses it: `POST /drive/deploy` answers `500` when storing
28
+ * fails, and a case for that answer needs a store that fails.
29
+ */
30
+ class RefusingDriveStore extends memory_store_js_1.MemoryDriveStore {
31
+ async write() {
32
+ throw new Error('the store refused the write');
33
+ }
34
+ async mkdirp() {
35
+ throw new Error('the store refused the write');
36
+ }
37
+ }
22
38
  /**
23
39
  * Runs a corpus case against the drive the case seeds itself, or against
24
40
  * services a caller supplies - that is how a host (or a failing store) is put
@@ -37,7 +53,7 @@ const runCase = async (testCase, services = (0, exports.servicesFor)(testCase))
37
53
  // only a refusal the API decided on becomes a status; anything else is a
38
54
  // broken host, and reporting it as a 500 would hide it from the suite
39
55
  if ((0, types_js_1.isApiError)(error))
40
- return { status: error.status, error: error.message };
56
+ return { status: error.status, body: error.body, error: error.message };
41
57
  throw error;
42
58
  }
43
59
  };
@@ -0,0 +1,270 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.deployTree = exports.isDeployTree = exports.checkDeployTree = exports.measureDeployTree = exports.decodeBase64 = exports.DEPLOY_SUCCESS = exports.DEPLOY_PAYLOAD_BYTES = exports.DEPLOY_LIMITS = void 0;
4
+ const types_js_1 = require("../types.js");
5
+ const paths_js_1 = require("./paths.js");
6
+ exports.DEPLOY_LIMITS = {
7
+ entries: 10000,
8
+ totalBytes: 100 * 1024 * 1024,
9
+ fileBytes: 20 * 1024 * 1024
10
+ };
11
+ /**
12
+ * The largest a deploy tree can be on the wire, which is what a host's transport
13
+ * has to admit.
14
+ *
15
+ * The cap is stated in DECODED bytes; the tree travels as JSON in which a file
16
+ * member's contents are base64 - 4/3 of what they write - and a service's are
17
+ * escaped text (~1.04). A tree at `totalBytes` therefore reaches the parser at
18
+ * up to ~134MB, and a transport that admits less refuses first with its own
19
+ * shape, so the cap it was meant to enforce is never consulted. 140MB leaves
20
+ * room for the tree's JSON scaffolding.
21
+ *
22
+ * Only the deploy routes use it: everywhere else a smaller limit applies,
23
+ * because a body is buffered before authentication and nothing else needs the
24
+ * headroom.
25
+ */
26
+ exports.DEPLOY_PAYLOAD_BYTES = 140 * 1024 * 1024;
27
+ /**
28
+ * The body a deploy answers success with.
29
+ *
30
+ * The wording is the published contract - a client may key on the message - so
31
+ * it is one constant here rather than a string in each host. It names
32
+ * `@sasjs/server` because that is the document's example, which predates this
33
+ * package; changing it is a document change, not a host's choice.
34
+ */
35
+ exports.DEPLOY_SUCCESS = {
36
+ status: 'success',
37
+ message: 'Files deployed successfully to @sasjs/server.'
38
+ };
39
+ const BASE64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
40
+ /** A base64 body without its padding, which is what the arithmetic needs. */
41
+ const base64Body = (value) => value.replace(/[^A-Za-z0-9+/]/g, '');
42
+ /** The bytes a base64 body decodes to, without decoding it. */
43
+ const base64Length = (value) => Math.floor((base64Body(value).length * 3) / 4);
44
+ /** The bytes a base64 body decodes to, for a store that writes bytes. */
45
+ const decodeBase64 = (value) => {
46
+ const body = base64Body(value);
47
+ const bytes = new Uint8Array(Math.floor((body.length * 3) / 4));
48
+ let buffer = 0;
49
+ let bits = 0;
50
+ let at = 0;
51
+ for (const char of body) {
52
+ buffer = (buffer << 6) | BASE64_ALPHABET.indexOf(char);
53
+ bits += 6;
54
+ if (bits >= 8) {
55
+ bits -= 8;
56
+ bytes[at++] = (buffer >> bits) & 0xff;
57
+ }
58
+ }
59
+ return bytes.subarray(0, at);
60
+ };
61
+ exports.decodeBase64 = decodeBase64;
62
+ /** The bytes a string occupies as UTF-8, without an encoding API. */
63
+ const utf8Length = (value) => {
64
+ let bytes = 0;
65
+ for (let at = 0; at < value.length; at++) {
66
+ const code = value.charCodeAt(at);
67
+ if (code < 0x80)
68
+ bytes += 1;
69
+ else if (code < 0x800)
70
+ bytes += 2;
71
+ else if (code >= 0xd800 && code <= 0xdbff && at + 1 < value.length) {
72
+ // a surrogate pair is one code point of four bytes
73
+ bytes += 4;
74
+ at++;
75
+ }
76
+ else
77
+ bytes += 3;
78
+ }
79
+ return bytes;
80
+ };
81
+ /** The name a member is written under - a service gains the extension. */
82
+ const memberName = (member) => member.type === 'service' ? `${member.name}.sas` : member.name;
83
+ /** The bytes a member writes. */
84
+ const memberBytes = (member) => member.type === 'file' ? base64Length(member.code) : utf8Length(member.code);
85
+ /**
86
+ * Sizes as the messages state them: one decimal in binary megabytes, and bytes
87
+ * below a megabyte - a limit small enough to be measured in bytes should not
88
+ * read as "0 MB", which is what a stricter host would otherwise report.
89
+ */
90
+ const describeSize = (bytes) => bytes < 1024 * 1024
91
+ ? `${bytes} bytes`
92
+ : `${Math.round((bytes / (1024 * 1024)) * 10) / 10} MB`;
93
+ /** A walk's starting point. */
94
+ const emptyWalk = () => ({
95
+ entries: 0,
96
+ totalBytes: 0,
97
+ largest: { path: '', bytes: 0 }
98
+ });
99
+ /**
100
+ * Every member name becomes a path SEGMENT under the deployment root, so a name
101
+ * carrying '/', '\' or '..' is a traversal attempt rather than a name.
102
+ */
103
+ const isSafeSegment = (name) => name !== '' &&
104
+ name !== '.' &&
105
+ name !== '..' &&
106
+ !/[/\\]/.test(name) &&
107
+ // a Windows drive or a NUL byte is never a segment either
108
+ !/[\0]/.test(name);
109
+ /**
110
+ * Walks a tree, counting it and noting the first unsafe name.
111
+ *
112
+ * A name problem is RECORDED rather than returned, so the walk always finishes:
113
+ * a caller that only wants the numbers gets the size of the whole tree, and the
114
+ * validator decides whether the name matters. Returning early would report a
115
+ * truncated tree as if it were the real one.
116
+ */
117
+ const walk = (members, parentPath, acc) => {
118
+ for (const member of members) {
119
+ const name = memberName(member);
120
+ const memberPath = parentPath ? `${parentPath}/${name}` : name;
121
+ if (!isSafeSegment(name) && !acc.nameRefusal)
122
+ acc.nameRefusal = {
123
+ code: 400,
124
+ status: 'failure',
125
+ message: `Invalid member name: '${name}'. Member names must be single path segments (no '/', '\\', '..').`
126
+ };
127
+ acc.entries++;
128
+ if (member.type === 'folder') {
129
+ walk(member.members, memberPath, acc);
130
+ continue;
131
+ }
132
+ const bytes = memberBytes(member);
133
+ acc.totalBytes += bytes;
134
+ if (bytes > acc.largest.bytes)
135
+ acc.largest = { path: memberPath, bytes };
136
+ }
137
+ };
138
+ /**
139
+ * Measures a tree without touching a drive.
140
+ *
141
+ * Every member is counted, whatever its name: this answers "how big is this
142
+ * tree", and a caller that also needs the names checked is `checkDeployTree`.
143
+ */
144
+ const measureDeployTree = (members) => {
145
+ const acc = emptyWalk();
146
+ walk(members, '', acc);
147
+ return {
148
+ entries: acc.entries,
149
+ totalBytes: acc.totalBytes,
150
+ largest: acc.largest
151
+ };
152
+ };
153
+ exports.measureDeployTree = measureDeployTree;
154
+ /**
155
+ * The refusal a file tree earns, or `null` when it can be written.
156
+ *
157
+ * Names are checked first, then the dimensions in a fixed order - entries,
158
+ * total, single file - so a tree breaching two of them is refused for the same
159
+ * one every time, which is what lets a caller key on the message and a test pin
160
+ * it.
161
+ *
162
+ * `limits` defaults to `DEPLOY_LIMITS`; a host may pass its own, stricter set.
163
+ */
164
+ const checkDeployTree = (members, limits = exports.DEPLOY_LIMITS) => {
165
+ const measured = emptyWalk();
166
+ walk(members, '', measured);
167
+ if (measured.nameRefusal)
168
+ return measured.nameRefusal;
169
+ if (measured.entries > limits.entries)
170
+ return {
171
+ code: 413,
172
+ status: 'failure',
173
+ message: `The file tree exceeds the deploy limit: ${measured.entries} entries (maximum ${limits.entries}).`
174
+ };
175
+ if (measured.totalBytes > limits.totalBytes)
176
+ return {
177
+ code: 413,
178
+ status: 'failure',
179
+ message: `The file tree exceeds the deploy limit: ${describeSize(measured.totalBytes)} (maximum ${describeSize(limits.totalBytes)}).`
180
+ };
181
+ if (measured.largest.bytes > limits.fileBytes)
182
+ return {
183
+ code: 413,
184
+ status: 'failure',
185
+ message: `A file in the tree exceeds the deploy limit: ${measured.largest.path} is ${describeSize(measured.largest.bytes)} (maximum ${describeSize(limits.fileBytes)} per file).`
186
+ };
187
+ return null;
188
+ };
189
+ exports.checkDeployTree = checkDeployTree;
190
+ /**
191
+ * The tree's shape, as the wire format states it.
192
+ *
193
+ * A port of `isFileTree` from `@sasjs/utils`, which is what sasjs/server
194
+ * validates with: a tree is an object with a `members` array, and every member
195
+ * is a folder with its own members, or a service/file with a string `code` and
196
+ * a string `name`. Anything else is the `400` the routes already answer.
197
+ */
198
+ const isDeployTree = (value) => {
199
+ if (!value || typeof value !== 'object')
200
+ return false;
201
+ const members = value.members;
202
+ return Array.isArray(members) && members.every(isDeployMember);
203
+ };
204
+ exports.isDeployTree = isDeployTree;
205
+ const isDeployMember = (value) => {
206
+ if (!value || typeof value !== 'object')
207
+ return false;
208
+ const member = value;
209
+ if (typeof member.name !== 'string')
210
+ return false;
211
+ if (member.type === 'folder')
212
+ return Array.isArray(member.members) && member.members.every(isDeployMember);
213
+ if (member.type === 'service' || member.type === 'file')
214
+ return typeof member.code === 'string';
215
+ return false;
216
+ };
217
+ /**
218
+ * Deploys a tree into `appLoc`, or refuses without writing.
219
+ *
220
+ * The order is the promise: shape, then containment, then the cap and the names,
221
+ * and only then the first write. `appLoc` is caller-supplied and every segment
222
+ * must stay a segment, so it is resolved against the drive root and refused when
223
+ * it leaves it - joined naively, a `..` would write outside the drive.
224
+ *
225
+ * A write that fails part-way answers the deploy routes' `500`; the tree before
226
+ * the failure is already on the drive, which the message does not hide.
227
+ */
228
+ const deployTree = async (store, appLoc, tree, limits = exports.DEPLOY_LIMITS) => {
229
+ if (!(0, exports.isDeployTree)(tree))
230
+ throw new types_js_1.ApiError(400, 'Provided not supported data format.', {
231
+ status: 'failure',
232
+ message: 'Provided not supported data format.'
233
+ });
234
+ const root = (0, paths_js_1.normaliseDrivePath)(appLoc);
235
+ if (root === null)
236
+ throw new types_js_1.ApiError(400, 'appLoc cannot be outside drive.', {
237
+ status: 'failure',
238
+ message: 'appLoc cannot be outside drive.'
239
+ });
240
+ const refusal = (0, exports.checkDeployTree)(tree.members, limits);
241
+ // the body is the envelope WITHOUT the refusal's own `code`: that field is
242
+ // how a host turns the refusal into a status, and it is not sent
243
+ if (refusal)
244
+ throw new types_js_1.ApiError(refusal.code, refusal.message, {
245
+ status: refusal.status,
246
+ message: refusal.message
247
+ });
248
+ try {
249
+ await writeMembers(store, tree.members, root);
250
+ }
251
+ catch {
252
+ throw new types_js_1.ApiError(500, 'Deployment failed!', {
253
+ status: 'failure',
254
+ message: 'Deployment failed!'
255
+ });
256
+ }
257
+ };
258
+ exports.deployTree = deployTree;
259
+ /** Writes every member under `parentPath`, depth first. */
260
+ const writeMembers = async (store, members, parentPath) => {
261
+ for (const member of members) {
262
+ const path = `${parentPath}/${memberName(member)}`;
263
+ if (member.type === 'folder') {
264
+ await store.mkdirp(path);
265
+ await writeMembers(store, member.members, path);
266
+ continue;
267
+ }
268
+ await store.write(path, member.type === 'file' ? (0, exports.decodeBase64)(member.code) : member.code);
269
+ }
270
+ };
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createDriveService = void 0;
4
4
  const types_js_1 = require("../types.js");
5
+ const deploy_js_1 = require("./deploy.js");
5
6
  const paths_js_1 = require("./paths.js");
6
7
  /** A path a file operation may act on, or the refusal the operation answers. */
7
8
  const filePath = (input, outside) => {
@@ -145,6 +146,10 @@ const createDriveService = (store) => {
145
146
  if (!stat.directory)
146
147
  throw new types_js_1.ApiError(400, 'Not a Folder.');
147
148
  return { status: 'success', tree: await treeOf(path) };
149
+ },
150
+ async deploy(appLoc, tree) {
151
+ await (0, deploy_js_1.deployTree)(store, appLoc, tree);
152
+ return { status: 'success' };
148
153
  }
149
154
  };
150
155
  };
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.HANDLED_ROUTES = exports.dispatch = void 0;
4
4
  const index_js_1 = require("../routes/index.js");
5
5
  const types_js_1 = require("../types.js");
6
+ const deploy_js_1 = require("../drive/deploy.js");
6
7
  /**
7
8
  * The parameters the table marks required, checked before the handler runs.
8
9
  *
@@ -129,10 +130,21 @@ const dispatch = async (services, routeId, request = {}) => {
129
130
  status: 200,
130
131
  body: await drive.rename(String(request.body?.oldPath || ''), String(request.body?.newPath || ''))
131
132
  };
133
+ case 'POST /SASjsApi/drive/deploy': {
134
+ // `streamWebFolder` is the host's: publishing an app stream is not a drive
135
+ // operation, and a host that has no app stream ignores it.
136
+ await drive.deploy(String(body.appLoc ?? ''), body.fileTree);
137
+ return { status: 200, body: deploy_js_1.DEPLOY_SUCCESS };
138
+ }
132
139
  case 'GET /SASjsApi/drive/filetree':
133
140
  return { status: 200, body: await drive.tree(query._folderPath) };
134
- default:
135
- throw new types_js_1.ApiError(501, `This package has no handler for ${routeId}.`);
141
+ default: {
142
+ // The reason the table carries travels with the refusal: a host developer
143
+ // reading a 501 wants to know what the operation waits on, not only that
144
+ // it is missing.
145
+ const reason = (0, index_js_1.routeById)(routeId)?.reason;
146
+ throw new types_js_1.ApiError(501, `This package has no handler for ${routeId}.${reason ? ` ${reason}` : ''}`);
147
+ }
136
148
  }
137
149
  };
138
150
  exports.dispatch = dispatch;
@@ -147,6 +159,7 @@ exports.HANDLED_ROUTES = [
147
159
  'DELETE /SASjsApi/drive/folder',
148
160
  'POST /SASjsApi/drive/rename',
149
161
  'GET /SASjsApi/drive/filetree',
162
+ 'POST /SASjsApi/drive/deploy',
150
163
  'POST /SASjsApi/auth/token',
151
164
  'POST /SASjsApi/auth/refresh',
152
165
  'POST /SASjsApi/auth/logout',
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.servicesFor = exports.runCase = exports.IDENTITY_CORPUS = exports.DRIVE_CORPUS = exports.parseIdentity = exports.IDENTITY_FORMAT = exports.DEFAULT_IDENTITY = exports.MemoryIdentityStore = exports.createIdentityService = exports.HANDLED_ROUTES = exports.dispatch = exports.TAG_CAPABILITIES = exports.OVERLAY = exports.findRoute = exports.routeById = exports.ROUTES = exports.MemoryDriveStore = exports.createDriveService = exports.isRoot = exports.basename = exports.dirname = exports.hasBlockedExtension = exports.normaliseDrivePath = void 0;
17
+ exports.servicesFor = exports.runCase = exports.IDENTITY_CORPUS = exports.DRIVE_CORPUS = exports.parseIdentity = exports.IDENTITY_FORMAT = exports.DEFAULT_IDENTITY = exports.MemoryIdentityStore = exports.createIdentityService = exports.HANDLED_ROUTES = exports.dispatch = exports.TAG_CAPABILITIES = exports.OVERLAY = exports.findRoute = exports.routeById = exports.ROUTES = exports.MemoryDriveStore = exports.measureDeployTree = exports.isDeployTree = exports.deployTree = exports.decodeBase64 = exports.checkDeployTree = exports.DEPLOY_SUCCESS = exports.DEPLOY_PAYLOAD_BYTES = exports.DEPLOY_LIMITS = exports.createDriveService = exports.isRoot = exports.basename = exports.dirname = exports.hasBlockedExtension = exports.normaliseDrivePath = void 0;
18
18
  /**
19
19
  * `@sasjs/api` - the SASjs Server API described once.
20
20
  *
@@ -39,6 +39,15 @@ Object.defineProperty(exports, "basename", { enumerable: true, get: function ()
39
39
  Object.defineProperty(exports, "isRoot", { enumerable: true, get: function () { return paths_js_1.isRoot; } });
40
40
  var service_js_1 = require("./drive/service.js");
41
41
  Object.defineProperty(exports, "createDriveService", { enumerable: true, get: function () { return service_js_1.createDriveService; } });
42
+ var deploy_js_1 = require("./drive/deploy.js");
43
+ Object.defineProperty(exports, "DEPLOY_LIMITS", { enumerable: true, get: function () { return deploy_js_1.DEPLOY_LIMITS; } });
44
+ Object.defineProperty(exports, "DEPLOY_PAYLOAD_BYTES", { enumerable: true, get: function () { return deploy_js_1.DEPLOY_PAYLOAD_BYTES; } });
45
+ Object.defineProperty(exports, "DEPLOY_SUCCESS", { enumerable: true, get: function () { return deploy_js_1.DEPLOY_SUCCESS; } });
46
+ Object.defineProperty(exports, "checkDeployTree", { enumerable: true, get: function () { return deploy_js_1.checkDeployTree; } });
47
+ Object.defineProperty(exports, "decodeBase64", { enumerable: true, get: function () { return deploy_js_1.decodeBase64; } });
48
+ Object.defineProperty(exports, "deployTree", { enumerable: true, get: function () { return deploy_js_1.deployTree; } });
49
+ Object.defineProperty(exports, "isDeployTree", { enumerable: true, get: function () { return deploy_js_1.isDeployTree; } });
50
+ Object.defineProperty(exports, "measureDeployTree", { enumerable: true, get: function () { return deploy_js_1.measureDeployTree; } });
42
51
  var memory_store_js_1 = require("./drive/memory-store.js");
43
52
  Object.defineProperty(exports, "MemoryDriveStore", { enumerable: true, get: function () { return memory_store_js_1.MemoryDriveStore; } });
44
53
  var index_js_1 = require("./routes/index.js");
@@ -472,6 +472,10 @@ exports.GENERATED_ROUTES = [
472
472
  status: 400,
473
473
  description: 'Invalid Format'
474
474
  },
475
+ {
476
+ status: 413,
477
+ description: 'File tree exceeds the deploy limit'
478
+ },
475
479
  {
476
480
  status: 500,
477
481
  description: 'Execution Error'
@@ -502,6 +506,10 @@ exports.GENERATED_ROUTES = [
502
506
  status: 400,
503
507
  description: 'Invalid Format'
504
508
  },
509
+ {
510
+ status: 413,
511
+ description: 'File tree exceeds the deploy limit'
512
+ },
505
513
  {
506
514
  status: 500,
507
515
  description: 'Execution Error'
@@ -13,7 +13,12 @@ exports.findRoute = exports.routeById = exports.ROUTES = void 0;
13
13
  const generated_js_1 = require("./generated.js");
14
14
  const overlay_js_1 = require("./overlay.js");
15
15
  const merge = (generated) => {
16
- const overlay = overlay_js_1.OVERLAY[generated.id] || {};
16
+ const overlay = overlay_js_1.OVERLAY[generated.id];
17
+ // Every operation states its decision. A route with no entry is a route
18
+ // nobody has looked at, which reads exactly like a route that is about to be
19
+ // done - so it fails the build instead, and the message says what to add.
20
+ if (!overlay)
21
+ throw new Error(`src/routes/overlay.ts has no entry for ${generated.id}. Every operation states whether this package implements it and, when it does not, the reason it does not: add \`{ implemented: false, reason: '...' }\` until a handler exists.`);
17
22
  const documented = new Map(generated.responses.map((r) => [r.status, r.description]));
18
23
  // The document states the statuses and the overlay only annotates the ones a
19
24
  // client keys a body on. Naming a status the document does not state is a
@@ -46,7 +51,8 @@ const merge = (generated) => {
46
51
  responses,
47
52
  hostResponses: overlay.hostResponses || [],
48
53
  capabilities: overlay.capabilities || overlay_js_1.TAG_CAPABILITIES[generated.tag] || [],
49
- implemented: overlay.implemented === true
54
+ implemented: overlay.implemented,
55
+ ...(overlay.implemented ? {} : { reason: overlay.reason })
50
56
  };
51
57
  };
52
58
  exports.ROUTES = generated_js_1.GENERATED_ROUTES.map(merge);