@sasjs/api 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/browser.js CHANGED
@@ -1,4 +1,4 @@
1
- /* @sasjs/api 0.9.0 - the SASjs Server API, for a service worker. https://github.com/sasjs/api */
1
+ /* @sasjs/api 0.10.0 - the SASjs Server API, for a service worker. https://github.com/sasjs/api */
2
2
  "use strict";
3
3
  var SASjsApi = (() => {
4
4
  var __defProp = Object.defineProperty;
@@ -25,6 +25,7 @@ var SASjsApi = (() => {
25
25
  ADMIN_SIGNED_IN: () => ADMIN_SIGNED_IN,
26
26
  ALWAYS_VISIBLE_ROUTES: () => ALWAYS_VISIBLE_ROUTES,
27
27
  ApiError: () => ApiError,
28
+ CODE_CORPUS: () => CODE_CORPUS,
28
29
  DEFAULT_GRANTABLE_ROUTES: () => DEFAULT_GRANTABLE_ROUTES,
29
30
  DEFAULT_IDENTITY: () => DEFAULT_IDENTITY,
30
31
  DEPLOY_LIMITS: () => DEPLOY_LIMITS,
@@ -2459,7 +2460,6 @@ var SASjsApi = (() => {
2459
2460
  Web: ["identity"]
2460
2461
  };
2461
2462
  var DIRECTORY = "Synchronising with an external auth provider needs a directory port, which this package does not have: a host decides which directory it holds accounts in, and answers this itself.";
2462
- var EDITOR = "Linting and formatting come from @sasjs/lint, which this package does not depend on. They land with the code routes so an editor has one wiring.";
2463
2463
  var RUNTIME = "Starting a program needs a runtime port, which this package does not have: a host decides which languages it holds, and answers this itself until then.";
2464
2464
  var SESSION_RECORD = "A triggered session's record, log, output and state belong to the runtime that ran the program, and this package has no runtime port.";
2465
2465
  var HOST_CONFIG = "The body is the host's own configuration - ports, run times and providers - which a mock states for itself and this package cannot know.";
@@ -2529,8 +2529,14 @@ var SASjsApi = (() => {
2529
2529
  // Code - the editor's three calls, and the runtime one
2530
2530
  "POST /SASjsApi/code/execute": { implemented: false, reason: RUNTIME },
2531
2531
  "POST /SASjsApi/code/trigger": { implemented: false, reason: RUNTIME },
2532
- "POST /SASjsApi/code/lint": { implemented: false, reason: EDITOR },
2533
- "POST /SASjsApi/code/format": { implemented: false, reason: EDITOR },
2532
+ "POST /SASjsApi/code/lint": {
2533
+ implemented: true,
2534
+ capabilities: ["identity"]
2535
+ },
2536
+ "POST /SASjsApi/code/format": {
2537
+ implemented: true,
2538
+ capabilities: ["identity"]
2539
+ },
2534
2540
  // Drive - the file and folder rules, and the three deploy routes
2535
2541
  // The body is the file itself, so there is nothing to annotate
2536
2542
  "GET /SASjsApi/drive/file": { implemented: true },
@@ -2772,8 +2778,10 @@ var SASjsApi = (() => {
2772
2778
  continue;
2773
2779
  if (param.in === "path") continue;
2774
2780
  const given = param.in === "query" ? request.query?.[param.name] : request.body?.[param.name];
2775
- if (given === void 0 || given === "")
2781
+ if (given === void 0)
2776
2782
  throw new ApiError(400, `"${param.name}" is required`);
2783
+ if (given === "")
2784
+ throw new ApiError(400, `"${param.name}" is not allowed to be empty`);
2777
2785
  }
2778
2786
  };
2779
2787
  var targetPath = (request) => {
@@ -2786,7 +2794,7 @@ var SASjsApi = (() => {
2786
2794
  var dispatch = async (services, routeId, request = {}) => {
2787
2795
  const route = routeById(routeId);
2788
2796
  if (!route) throw new ApiError(404, `No such operation: ${routeId}`);
2789
- const { drive, identity, records } = services;
2797
+ const { drive, identity, records, code } = services;
2790
2798
  const identityScoped = ["User", "Group", "Permission", "Client"].includes(route.tag) || routeId === "PATCH /SASjsApi/auth/updatePassword";
2791
2799
  if (identityScoped && records) {
2792
2800
  const subject = request.subject ?? (identity ? await identity.authenticate(request.token) : void 0);
@@ -2827,7 +2835,7 @@ var SASjsApi = (() => {
2827
2835
  status: 200,
2828
2836
  body: await identity.token({
2829
2837
  clientId: String(body.clientId ?? ""),
2830
- code: String(body.code ?? "")
2838
+ code: body.code
2831
2839
  })
2832
2840
  };
2833
2841
  case "POST /SASjsApi/auth/refresh":
@@ -2971,6 +2979,32 @@ var SASjsApi = (() => {
2971
2979
  { password: body.password }
2972
2980
  )
2973
2981
  };
2982
+ case "POST /SASjsApi/code/lint":
2983
+ if (!code)
2984
+ throw new ApiError(
2985
+ 501,
2986
+ `This host has no code service, so it cannot answer ${routeId}.`
2987
+ );
2988
+ return {
2989
+ status: 200,
2990
+ body: await code.lint(await caller(), {
2991
+ code: body.code,
2992
+ filePath: body.filePath
2993
+ })
2994
+ };
2995
+ case "POST /SASjsApi/code/format":
2996
+ if (!code)
2997
+ throw new ApiError(
2998
+ 501,
2999
+ `This host has no code service, so it cannot answer ${routeId}.`
3000
+ );
3001
+ return {
3002
+ status: 200,
3003
+ body: await code.format(await caller(), {
3004
+ code: body.code,
3005
+ filePath: body.filePath
3006
+ })
3007
+ };
2974
3008
  case "GET /SASjsApi/info/authorizedRoutes":
2975
3009
  return {
2976
3010
  status: 200,
@@ -3091,6 +3125,8 @@ var SASjsApi = (() => {
3091
3125
  }
3092
3126
  };
3093
3127
  var HANDLED_ROUTES = [
3128
+ "POST /SASjsApi/code/lint",
3129
+ "POST /SASjsApi/code/format",
3094
3130
  "GET /SASjsApi/info/authorizedRoutes",
3095
3131
  "GET /SASjsApi/info/myAuthorizedRoutes",
3096
3132
  "GET /SASjsApi/drive/file",
@@ -3602,6 +3638,53 @@ var SASjsApi = (() => {
3602
3638
  };
3603
3639
  };
3604
3640
 
3641
+ // src/corpus/code.ts
3642
+ var CODE_CORPUS = [
3643
+ {
3644
+ id: "code-lint-missing-header",
3645
+ route: "POST /SASjsApi/code/lint",
3646
+ description: "answers the diagnostics for a buffer, without touching a file",
3647
+ request: {
3648
+ // the routes are permissioned; the runner resolves the fixture's admin
3649
+ subject: { userId: "user-admin", clientId: "web_app" },
3650
+ body: { code: "data _null_;\n x = 1;\nrun;\n" }
3651
+ },
3652
+ expect: {
3653
+ status: 200,
3654
+ body: [
3655
+ {
3656
+ message: "File missing Doxygen header",
3657
+ lineNumber: 1,
3658
+ startColumnNumber: 1,
3659
+ endColumnNumber: 1,
3660
+ severity: 1
3661
+ }
3662
+ ]
3663
+ }
3664
+ },
3665
+ // Two refusals are deliberately not pinned here yet: a `code` that is not a
3666
+ // string, and one that is empty. The package answers 400 `"code" must be a
3667
+ // string` / `"code" is not allowed to be empty` - the messages the server's
3668
+ // schema produces - but sasjs/server's code controller carries no
3669
+ // `@Response(400)` for these routes, so the document states 200 alone and the
3670
+ // parity gate refuses a case answering anything else. The cases land when the
3671
+ // decorators do.
3672
+ {
3673
+ id: "code-format-adds-the-header",
3674
+ route: "POST /SASjsApi/code/format",
3675
+ description: "answers the formatted buffer, header block included",
3676
+ request: {
3677
+ // the routes are permissioned; the runner resolves the fixture's admin
3678
+ subject: { userId: "user-admin", clientId: "web_app" },
3679
+ body: { code: "data _null_;\n x = 1;\nrun;\n" }
3680
+ },
3681
+ expect: {
3682
+ status: 200,
3683
+ body: "/**\n @file\n @brief <Your brief here>\n\n <h4> SAS Macros </h4>\n\n**/\ndata _null_;\n x = 1;\nrun;\n"
3684
+ }
3685
+ }
3686
+ ];
3687
+
3605
3688
  // src/identity/fixture.ts
3606
3689
  var IDENTITY_FORMAT = 1;
3607
3690
  var SECONDS_IN_A_DAY2 = 86400;
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createCodeService = void 0;
4
+ /**
5
+ * The code service, on its own entry point.
6
+ *
7
+ * `@sasjs/lint` reads the filesystem (its config resolution walks up for a
8
+ * `.sasjslint`), so it cannot be bundled for a service worker: the package root
9
+ * stays free of it and a host that wants these routes imports `@sasjs/api/code`.
10
+ */
11
+ var service_js_1 = require("./service.js");
12
+ Object.defineProperty(exports, "createCodeService", { enumerable: true, get: function () { return service_js_1.createCodeService; } });
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createCodeService = void 0;
4
+ const lint_1 = require("@sasjs/lint");
5
+ const types_js_1 = require("../types.js");
6
+ /**
7
+ * `code`, refused the way the server's schema refuses it.
8
+ *
9
+ * Coercing with `String(...)` defeated the refusal: `{"code": 123}` was linted as
10
+ * the text "123" and `{"code": ""}` answered "required" where the server says it
11
+ * is empty. The three messages are Joi's, because they are the ones a client
12
+ * written against the document already handles.
13
+ */
14
+ const codeOf = (body) => {
15
+ const value = body.code;
16
+ if (value === undefined)
17
+ throw new types_js_1.ApiError(400, '"code" is required');
18
+ if (typeof value !== 'string')
19
+ throw new types_js_1.ApiError(400, '"code" must be a string');
20
+ if (value === '')
21
+ throw new types_js_1.ApiError(400, '"code" is not allowed to be empty');
22
+ return value;
23
+ };
24
+ /**
25
+ * The two editor routes, over `@sasjs/lint`.
26
+ *
27
+ * The linter is a dependency because it is the same everywhere, and it is
28
+ * deliberately NOT reachable from the package root: `@sasjs/lint` resolves its
29
+ * configuration by walking the filesystem, so it cannot be bundled into the
30
+ * service worker the browser entry produces. A host that wants these routes
31
+ * imports `@sasjs/api/code`, which is Node-only, exactly like the linter.
32
+ *
33
+ * The *rules* are the host's: which `.sasjslint` applies to a path is a fact
34
+ * about a drive, so `lintConfig` is asked for it.
35
+ * Neither route touches the drive or runs anything; they answer about the text
36
+ * in the request, which is what lets an editor check unsaved changes.
37
+ */
38
+ const createCodeService = (options = {}) => {
39
+ const requireCaller = (caller) => {
40
+ // both routes are permissioned at the gate, so a caller that reached the
41
+ // service without one is a host that skipped it rather than a public route
42
+ if (!caller)
43
+ throw new types_js_1.ApiError(401, 'Unauthorized');
44
+ };
45
+ const configFor = (filePath) => options.lintConfig ? options.lintConfig(filePath) : undefined;
46
+ return {
47
+ async lint(caller, body) {
48
+ requireCaller(caller);
49
+ return (0, lint_1.lintText)(codeOf(body), configFor(body.filePath));
50
+ },
51
+ async format(caller, body) {
52
+ requireCaller(caller);
53
+ return (0, lint_1.formatText)(codeOf(body), configFor(body.filePath));
54
+ }
55
+ };
56
+ };
57
+ exports.createCodeService = createCodeService;
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CODE_CORPUS = void 0;
4
+ /**
5
+ * The editor's corpus: the two routes that answer about a buffer rather than a
6
+ * file.
7
+ *
8
+ * Both expectations are the linter's own output for the same input, measured
9
+ * against `@sasjs/lint` with no configuration - the defaults an untitled buffer
10
+ * lints against. A host that supplies a `.sasjslint` changes the rules for its
11
+ * own drive, which is why the case names no `filePath`.
12
+ *
13
+ * lintText('data _null_;\n x = 1;\nrun;\n')
14
+ * -> [{ message: 'File missing Doxygen header', lineNumber: 1, ... }]
15
+ * formatText(...)
16
+ * -> the header block, then the program with its spacing normalised
17
+ */
18
+ exports.CODE_CORPUS = [
19
+ {
20
+ id: 'code-lint-missing-header',
21
+ route: 'POST /SASjsApi/code/lint',
22
+ description: 'answers the diagnostics for a buffer, without touching a file',
23
+ request: {
24
+ // the routes are permissioned; the runner resolves the fixture's admin
25
+ subject: { userId: 'user-admin', clientId: 'web_app' },
26
+ body: { code: 'data _null_;\n x = 1;\nrun;\n' }
27
+ },
28
+ expect: {
29
+ status: 200,
30
+ body: [
31
+ {
32
+ message: 'File missing Doxygen header',
33
+ lineNumber: 1,
34
+ startColumnNumber: 1,
35
+ endColumnNumber: 1,
36
+ severity: 1
37
+ }
38
+ ]
39
+ }
40
+ },
41
+ // Two refusals are deliberately not pinned here yet: a `code` that is not a
42
+ // string, and one that is empty. The package answers 400 `"code" must be a
43
+ // string` / `"code" is not allowed to be empty` - the messages the server's
44
+ // schema produces - but sasjs/server's code controller carries no
45
+ // `@Response(400)` for these routes, so the document states 200 alone and the
46
+ // parity gate refuses a case answering anything else. The cases land when the
47
+ // decorators do.
48
+ {
49
+ id: 'code-format-adds-the-header',
50
+ route: 'POST /SASjsApi/code/format',
51
+ description: 'answers the formatted buffer, header block included',
52
+ request: {
53
+ // the routes are permissioned; the runner resolves the fixture's admin
54
+ subject: { userId: 'user-admin', clientId: 'web_app' },
55
+ body: { code: 'data _null_;\n x = 1;\nrun;\n' }
56
+ },
57
+ expect: {
58
+ status: 200,
59
+ body: '/**\n @file\n @brief <Your brief here>\n\n <h4> SAS Macros </h4>\n\n**/\ndata _null_;\n x = 1;\nrun;\n'
60
+ }
61
+ }
62
+ ];
@@ -24,8 +24,13 @@ const validate = (route, request) => {
24
24
  const given = param.in === 'query'
25
25
  ? request.query?.[param.name]
26
26
  : request.body?.[param.name];
27
- if (given === undefined || given === '')
27
+ // missing and empty are two refusals, the way the schema has them: a
28
+ // client that sent the field an empty value has a different bug from one
29
+ // that omitted it
30
+ if (given === undefined)
28
31
  throw new types_js_1.ApiError(400, `"${param.name}" is required`);
32
+ if (given === '')
33
+ throw new types_js_1.ApiError(400, `"${param.name}" is not allowed to be empty`);
29
34
  }
30
35
  };
31
36
  /** The drive path a write names: the query parameter wins over the form field. */
@@ -44,7 +49,7 @@ const dispatch = async (services, routeId, request = {}) => {
44
49
  const route = (0, index_js_1.routeById)(routeId);
45
50
  if (!route)
46
51
  throw new types_js_1.ApiError(404, `No such operation: ${routeId}`);
47
- const { drive, identity, records } = services;
52
+ const { drive, identity, records, code } = services;
48
53
  // The server authenticates before its payload schema runs, so an anonymous
49
54
  // request with an incomplete body is a 401 there rather than a 400. The order
50
55
  // is only observable on the route groups that gate on a caller: the sign-in
@@ -92,7 +97,7 @@ const dispatch = async (services, routeId, request = {}) => {
92
97
  status: 200,
93
98
  body: await identity.token({
94
99
  clientId: String(body.clientId ?? ''),
95
- code: String(body.code ?? '')
100
+ code: body.code
96
101
  })
97
102
  };
98
103
  case 'POST /SASjsApi/auth/refresh':
@@ -219,6 +224,26 @@ const dispatch = async (services, routeId, request = {}) => {
219
224
  status: 200,
220
225
  body: await required().deleteUser(await caller(), { username: String(params.username ?? '') }, { password: body.password })
221
226
  };
227
+ case 'POST /SASjsApi/code/lint':
228
+ if (!code)
229
+ throw new types_js_1.ApiError(501, `This host has no code service, so it cannot answer ${routeId}.`);
230
+ return {
231
+ status: 200,
232
+ body: await code.lint(await caller(), {
233
+ code: body.code,
234
+ filePath: body.filePath
235
+ })
236
+ };
237
+ case 'POST /SASjsApi/code/format':
238
+ if (!code)
239
+ throw new types_js_1.ApiError(501, `This host has no code service, so it cannot answer ${routeId}.`);
240
+ return {
241
+ status: 200,
242
+ body: await code.format(await caller(), {
243
+ code: body.code,
244
+ filePath: body.filePath
245
+ })
246
+ };
222
247
  case 'GET /SASjsApi/info/authorizedRoutes':
223
248
  return {
224
249
  status: 200,
@@ -332,6 +357,8 @@ const dispatch = async (services, routeId, request = {}) => {
332
357
  exports.dispatch = dispatch;
333
358
  /** Whether a route is one `dispatch` can answer today. */
334
359
  exports.HANDLED_ROUTES = [
360
+ 'POST /SASjsApi/code/lint',
361
+ 'POST /SASjsApi/code/format',
335
362
  'GET /SASjsApi/info/authorizedRoutes',
336
363
  'GET /SASjsApi/info/myAuthorizedRoutes',
337
364
  'GET /SASjsApi/drive/file',
@@ -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.PROVIDER_MANAGED = exports.ADMIN_SIGNED_IN = exports.RECORD_CORPUS = exports.IDENTITY_CORPUS = exports.DRIVE_CORPUS = exports.parseIdentity = exports.IDENTITY_FORMAT = exports.DEFAULT_IDENTITY = exports.MemoryIdentityStore = exports.sanitiseDisplayName = exports.createRecordService = 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;
17
+ exports.servicesFor = exports.runCase = exports.PROVIDER_MANAGED = exports.ADMIN_SIGNED_IN = exports.RECORD_CORPUS = exports.IDENTITY_CORPUS = exports.DRIVE_CORPUS = exports.parseIdentity = exports.IDENTITY_FORMAT = exports.DEFAULT_IDENTITY = exports.MemoryIdentityStore = exports.CODE_CORPUS = exports.sanitiseDisplayName = exports.createRecordService = 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
  *
@@ -67,6 +67,8 @@ Object.defineProperty(exports, "createIdentityService", { enumerable: true, get:
67
67
  var records_js_1 = require("./identity/records.js");
68
68
  Object.defineProperty(exports, "createRecordService", { enumerable: true, get: function () { return records_js_1.createRecordService; } });
69
69
  Object.defineProperty(exports, "sanitiseDisplayName", { enumerable: true, get: function () { return records_js_1.sanitiseDisplayName; } });
70
+ var code_js_1 = require("./corpus/code.js");
71
+ Object.defineProperty(exports, "CODE_CORPUS", { enumerable: true, get: function () { return code_js_1.CODE_CORPUS; } });
70
72
  var memory_store_js_2 = require("./identity/memory-store.js");
71
73
  Object.defineProperty(exports, "MemoryIdentityStore", { enumerable: true, get: function () { return memory_store_js_2.MemoryIdentityStore; } });
72
74
  var fixture_js_1 = require("./identity/fixture.js");
@@ -26,7 +26,6 @@ exports.TAG_CAPABILITIES = {
26
26
  * thing thirty times.
27
27
  */
28
28
  const DIRECTORY = 'Synchronising with an external auth provider needs a directory port, which this package does not have: a host decides which directory it holds accounts in, and answers this itself.';
29
- const EDITOR = 'Linting and formatting come from @sasjs/lint, which this package does not depend on. They land with the code routes so an editor has one wiring.';
30
29
  const RUNTIME = 'Starting a program needs a runtime port, which this package does not have: a host decides which languages it holds, and answers this itself until then.';
31
30
  const SESSION_RECORD = "A triggered session's record, log, output and state belong to the runtime that ran the program, and this package has no runtime port.";
32
31
  const HOST_CONFIG = "The body is the host's own configuration - ports, run times and providers - which a mock states for itself and this package cannot know.";
@@ -96,8 +95,14 @@ exports.OVERLAY = {
96
95
  // Code - the editor's three calls, and the runtime one
97
96
  'POST /SASjsApi/code/execute': { implemented: false, reason: RUNTIME },
98
97
  'POST /SASjsApi/code/trigger': { implemented: false, reason: RUNTIME },
99
- 'POST /SASjsApi/code/lint': { implemented: false, reason: EDITOR },
100
- 'POST /SASjsApi/code/format': { implemented: false, reason: EDITOR },
98
+ 'POST /SASjsApi/code/lint': {
99
+ implemented: true,
100
+ capabilities: ['identity']
101
+ },
102
+ 'POST /SASjsApi/code/format': {
103
+ implemented: true,
104
+ capabilities: ['identity']
105
+ },
101
106
  // Drive - the file and folder rules, and the three deploy routes
102
107
  // The body is the file itself, so there is nothing to annotate
103
108
  'GET /SASjsApi/drive/file': { implemented: true },
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The code service, on its own entry point.
3
+ *
4
+ * `@sasjs/lint` reads the filesystem (its config resolution walks up for a
5
+ * `.sasjslint`), so it cannot be bundled for a service worker: the package root
6
+ * stays free of it and a host that wants these routes imports `@sasjs/api/code`.
7
+ */
8
+ export { createCodeService } from './service.js';
9
+ export type { CodeRequest, CodeService, CodeServiceOptions } from './service.js';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/code/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAChD,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The code service, on its own entry point.
3
+ *
4
+ * `@sasjs/lint` reads the filesystem (its config resolution walks up for a
5
+ * `.sasjslint`), so it cannot be bundled for a service worker: the package root
6
+ * stays free of it and a host that wants these routes imports `@sasjs/api/code`.
7
+ */
8
+ export { createCodeService } from './service.js';
@@ -0,0 +1,40 @@
1
+ import type { Diagnostic, LintConfig } from '@sasjs/lint';
2
+ import type { Subject } from '../identity/types.js';
3
+ /** What both code routes take: a buffer, and where it came from when it has a file. */
4
+ export interface CodeRequest {
5
+ code: string;
6
+ /** The drive path the code belongs to, so a host can pick that folder's rules. */
7
+ filePath?: string;
8
+ }
9
+ export interface CodeServiceOptions {
10
+ /**
11
+ * The lint configuration that applies to a file.
12
+ *
13
+ * A host resolves the nearest `.sasjslint` at or above `filePath`, so each
14
+ * folder can carry its own rules; without one the linter's defaults apply,
15
+ * which is what an untitled buffer lints against.
16
+ */
17
+ lintConfig?: (filePath?: string) => LintConfig | undefined;
18
+ }
19
+ export interface CodeService {
20
+ /** The diagnostics for a buffer. Nothing is written and nothing is executed. */
21
+ lint(caller: Subject | undefined, body: CodeRequest): Promise<Diagnostic[]>;
22
+ /** The formatted buffer. */
23
+ format(caller: Subject | undefined, body: CodeRequest): Promise<string>;
24
+ }
25
+ /**
26
+ * The two editor routes, over `@sasjs/lint`.
27
+ *
28
+ * The linter is a dependency because it is the same everywhere, and it is
29
+ * deliberately NOT reachable from the package root: `@sasjs/lint` resolves its
30
+ * configuration by walking the filesystem, so it cannot be bundled into the
31
+ * service worker the browser entry produces. A host that wants these routes
32
+ * imports `@sasjs/api/code`, which is Node-only, exactly like the linter.
33
+ *
34
+ * The *rules* are the host's: which `.sasjslint` applies to a path is a fact
35
+ * about a drive, so `lintConfig` is asked for it.
36
+ * Neither route touches the drive or runs anything; they answer about the text
37
+ * in the request, which is what lets an editor check unsaved changes.
38
+ */
39
+ export declare const createCodeService: (options?: CodeServiceOptions) => CodeService;
40
+ //# sourceMappingURL=service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../../src/code/service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAGzD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAA;AAEnD,uFAAuF;AACvF,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,kFAAkF;IAClF,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,kBAAkB;IACjC;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,CAAC,QAAQ,CAAC,EAAE,MAAM,KAAK,UAAU,GAAG,SAAS,CAAA;CAC3D;AAED,MAAM,WAAW,WAAW;IAC1B,gFAAgF;IAChF,IAAI,CAAC,MAAM,EAAE,OAAO,GAAG,SAAS,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAAA;IAC3E,4BAA4B;IAC5B,MAAM,CAAC,MAAM,EAAE,OAAO,GAAG,SAAS,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;CACxE;AAmBD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,iBAAiB,GAC5B,UAAS,kBAAuB,KAC/B,WAqBF,CAAA"}
@@ -0,0 +1,53 @@
1
+ import { formatText, lintText } from '@sasjs/lint';
2
+ import { ApiError } from '../types.js';
3
+ /**
4
+ * `code`, refused the way the server's schema refuses it.
5
+ *
6
+ * Coercing with `String(...)` defeated the refusal: `{"code": 123}` was linted as
7
+ * the text "123" and `{"code": ""}` answered "required" where the server says it
8
+ * is empty. The three messages are Joi's, because they are the ones a client
9
+ * written against the document already handles.
10
+ */
11
+ const codeOf = (body) => {
12
+ const value = body.code;
13
+ if (value === undefined)
14
+ throw new ApiError(400, '"code" is required');
15
+ if (typeof value !== 'string')
16
+ throw new ApiError(400, '"code" must be a string');
17
+ if (value === '')
18
+ throw new ApiError(400, '"code" is not allowed to be empty');
19
+ return value;
20
+ };
21
+ /**
22
+ * The two editor routes, over `@sasjs/lint`.
23
+ *
24
+ * The linter is a dependency because it is the same everywhere, and it is
25
+ * deliberately NOT reachable from the package root: `@sasjs/lint` resolves its
26
+ * configuration by walking the filesystem, so it cannot be bundled into the
27
+ * service worker the browser entry produces. A host that wants these routes
28
+ * imports `@sasjs/api/code`, which is Node-only, exactly like the linter.
29
+ *
30
+ * The *rules* are the host's: which `.sasjslint` applies to a path is a fact
31
+ * about a drive, so `lintConfig` is asked for it.
32
+ * Neither route touches the drive or runs anything; they answer about the text
33
+ * in the request, which is what lets an editor check unsaved changes.
34
+ */
35
+ export const createCodeService = (options = {}) => {
36
+ const requireCaller = (caller) => {
37
+ // both routes are permissioned at the gate, so a caller that reached the
38
+ // service without one is a host that skipped it rather than a public route
39
+ if (!caller)
40
+ throw new ApiError(401, 'Unauthorized');
41
+ };
42
+ const configFor = (filePath) => options.lintConfig ? options.lintConfig(filePath) : undefined;
43
+ return {
44
+ async lint(caller, body) {
45
+ requireCaller(caller);
46
+ return lintText(codeOf(body), configFor(body.filePath));
47
+ },
48
+ async format(caller, body) {
49
+ requireCaller(caller);
50
+ return formatText(codeOf(body), configFor(body.filePath));
51
+ }
52
+ };
53
+ };
@@ -0,0 +1,17 @@
1
+ import type { CorpusCase } from './drive.js';
2
+ /**
3
+ * The editor's corpus: the two routes that answer about a buffer rather than a
4
+ * file.
5
+ *
6
+ * Both expectations are the linter's own output for the same input, measured
7
+ * against `@sasjs/lint` with no configuration - the defaults an untitled buffer
8
+ * lints against. A host that supplies a `.sasjslint` changes the rules for its
9
+ * own drive, which is why the case names no `filePath`.
10
+ *
11
+ * lintText('data _null_;\n x = 1;\nrun;\n')
12
+ * -> [{ message: 'File missing Doxygen header', lineNumber: 1, ... }]
13
+ * formatText(...)
14
+ * -> the header block, then the program with its spacing normalised
15
+ */
16
+ export declare const CODE_CORPUS: CorpusCase[];
17
+ //# sourceMappingURL=code.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"code.d.ts","sourceRoot":"","sources":["../../../src/corpus/code.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAE5C;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,WAAW,EAAE,UAAU,EA6CnC,CAAA"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The editor's corpus: the two routes that answer about a buffer rather than a
3
+ * file.
4
+ *
5
+ * Both expectations are the linter's own output for the same input, measured
6
+ * against `@sasjs/lint` with no configuration - the defaults an untitled buffer
7
+ * lints against. A host that supplies a `.sasjslint` changes the rules for its
8
+ * own drive, which is why the case names no `filePath`.
9
+ *
10
+ * lintText('data _null_;\n x = 1;\nrun;\n')
11
+ * -> [{ message: 'File missing Doxygen header', lineNumber: 1, ... }]
12
+ * formatText(...)
13
+ * -> the header block, then the program with its spacing normalised
14
+ */
15
+ export const CODE_CORPUS = [
16
+ {
17
+ id: 'code-lint-missing-header',
18
+ route: 'POST /SASjsApi/code/lint',
19
+ description: 'answers the diagnostics for a buffer, without touching a file',
20
+ request: {
21
+ // the routes are permissioned; the runner resolves the fixture's admin
22
+ subject: { userId: 'user-admin', clientId: 'web_app' },
23
+ body: { code: 'data _null_;\n x = 1;\nrun;\n' }
24
+ },
25
+ expect: {
26
+ status: 200,
27
+ body: [
28
+ {
29
+ message: 'File missing Doxygen header',
30
+ lineNumber: 1,
31
+ startColumnNumber: 1,
32
+ endColumnNumber: 1,
33
+ severity: 1
34
+ }
35
+ ]
36
+ }
37
+ },
38
+ // Two refusals are deliberately not pinned here yet: a `code` that is not a
39
+ // string, and one that is empty. The package answers 400 `"code" must be a
40
+ // string` / `"code" is not allowed to be empty` - the messages the server's
41
+ // schema produces - but sasjs/server's code controller carries no
42
+ // `@Response(400)` for these routes, so the document states 200 alone and the
43
+ // parity gate refuses a case answering anything else. The cases land when the
44
+ // decorators do.
45
+ {
46
+ id: 'code-format-adds-the-header',
47
+ route: 'POST /SASjsApi/code/format',
48
+ description: 'answers the formatted buffer, header block included',
49
+ request: {
50
+ // the routes are permissioned; the runner resolves the fixture's admin
51
+ subject: { userId: 'user-admin', clientId: 'web_app' },
52
+ body: { code: 'data _null_;\n x = 1;\nrun;\n' }
53
+ },
54
+ expect: {
55
+ status: 200,
56
+ body: '/**\n @file\n @brief <Your brief here>\n\n <h4> SAS Macros </h4>\n\n**/\ndata _null_;\n x = 1;\nrun;\n'
57
+ }
58
+ }
59
+ ];
@@ -47,6 +47,15 @@ export interface CorpusCase {
47
47
  * minted token is not a value a case can state.
48
48
  */
49
49
  keys?: string[];
50
+ /**
51
+ * How the body travels, where a string could be either. `json` is a JSON
52
+ * string on the wire - what tsoa emits for `Promise<string>` and what the
53
+ * server's `res.json` sends; `text` is a body the route sends itself, which
54
+ * the document carries in the `description` with no content. A case that
55
+ * names neither is read as `text` when the document declares no content and
56
+ * as `json` when it does.
57
+ */
58
+ encoding?: 'json' | 'text';
50
59
  };
51
60
  }
52
61
  export declare const DRIVE_CORPUS: CorpusCase[];
@@ -1 +1 @@
1
- {"version":3,"file":"drive.d.ts","sourceRoot":"","sources":["../../../src/corpus/drive.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AACrD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAE7D,MAAM,WAAW,UAAU;IACzB,6CAA6C;IAC7C,EAAE,EAAE,MAAM,CAAA;IACV,kEAAkE;IAClE,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,kCAAkC;IAClC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAAC,CAAA;IAC1C,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,eAAe,CAAA;IAC1B;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,OAAO,EAAE,UAAU,CAAA;IACnB,MAAM,EAAE;QACN,MAAM,EAAE,MAAM,CAAA;QACd,IAAI,CAAC,EAAE,OAAO,CAAA;QACd,QAAQ,CAAC,EAAE,MAAM,GAAG,UAAU,CAAA;QAC9B,yDAAyD;QACzD,KAAK,CAAC,EAAE,MAAM,CAAA;QACd;;;;WAIG;QACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;QACjC;;;WAGG;QACH,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;KAChB,CAAA;CACF;AAmBD,eAAO,MAAM,YAAY,EAAE,UAAU,EAqapC,CAAA"}
1
+ {"version":3,"file":"drive.d.ts","sourceRoot":"","sources":["../../../src/corpus/drive.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AACrD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAE7D,MAAM,WAAW,UAAU;IACzB,6CAA6C;IAC7C,EAAE,EAAE,MAAM,CAAA;IACV,kEAAkE;IAClE,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,kCAAkC;IAClC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAAC,CAAA;IAC1C,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,eAAe,CAAA;IAC1B;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,OAAO,EAAE,UAAU,CAAA;IACnB,MAAM,EAAE;QACN,MAAM,EAAE,MAAM,CAAA;QACd,IAAI,CAAC,EAAE,OAAO,CAAA;QACd,QAAQ,CAAC,EAAE,MAAM,GAAG,UAAU,CAAA;QAC9B,yDAAyD;QACzD,KAAK,CAAC,EAAE,MAAM,CAAA;QACd;;;;WAIG;QACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;QACjC;;;WAGG;QACH,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;QACf;;;;;;;WAOG;QACH,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAC3B,CAAA;CACF;AAmBD,eAAO,MAAM,YAAY,EAAE,UAAU,EAqapC,CAAA"}
@@ -1,6 +1,7 @@
1
1
  import type { DriveService } from '../drive/service.js';
2
2
  import type { IdentityService } from '../identity/service.js';
3
3
  import type { RecordService } from '../identity/records.js';
4
+ import type { CodeService } from '../code/service.js';
4
5
  import type { Subject } from '../identity/types.js';
5
6
  export interface ApiRequest {
6
7
  query?: Record<string, string | undefined>;
@@ -33,6 +34,8 @@ export interface ApiResponse {
33
34
  contents?: string | Uint8Array;
34
35
  }
35
36
  export interface Services {
37
+ /** The editor routes, where the host supplies a linter. */
38
+ code?: CodeService;
36
39
  drive: DriveService;
37
40
  /**
38
41
  * Users, clients and tokens. A host without one answers 501 for the identity
@@ -1 +1 @@
1
- {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../../../src/host/dispatch.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAC7D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAA;AAOnD,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;IAC1C,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,oFAAoF;IACpF,IAAI,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,CAAA;KAAE,CAAA;IACtD;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,iEAAiE;IACjE,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,mBAAmB;IACnB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,uEAAuE;IACvE,QAAQ,CAAC,EAAE,MAAM,GAAG,UAAU,CAAA;CAC/B;AAED,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,YAAY,CAAA;IACnB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,eAAe,CAAA;IAC1B,yFAAyF;IACzF,OAAO,CAAC,EAAE,aAAa,CAAA;CACxB;AAsCD,eAAO,MAAM,QAAQ,GACnB,UAAU,QAAQ,EAClB,SAAS,MAAM,EACf,UAAS,UAAe,KACvB,OAAO,CAAC,WAAW,CAgXrB,CAAA;AAED,0DAA0D;AAC1D,eAAO,MAAM,cAAc,EAAE,MAAM,EA0ClC,CAAA"}
1
+ {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../../../src/host/dispatch.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAC7D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AACrD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAA;AAOnD,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;IAC1C,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,oFAAoF;IACpF,IAAI,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,CAAA;KAAE,CAAA;IACtD;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,iEAAiE;IACjE,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,mBAAmB;IACnB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,uEAAuE;IACvE,QAAQ,CAAC,EAAE,MAAM,GAAG,UAAU,CAAA;CAC/B;AAED,MAAM,WAAW,QAAQ;IACvB,2DAA2D;IAC3D,IAAI,CAAC,EAAE,WAAW,CAAA;IAClB,KAAK,EAAE,YAAY,CAAA;IACnB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,eAAe,CAAA;IAC1B,yFAAyF;IACzF,OAAO,CAAC,EAAE,aAAa,CAAA;CACxB;AA2CD,eAAO,MAAM,QAAQ,GACnB,UAAU,QAAQ,EAClB,SAAS,MAAM,EACf,UAAS,UAAe,KACvB,OAAO,CAAC,WAAW,CA2YrB,CAAA;AAED,0DAA0D;AAC1D,eAAO,MAAM,cAAc,EAAE,MAAM,EA4ClC,CAAA"}
@@ -21,8 +21,13 @@ const validate = (route, request) => {
21
21
  const given = param.in === 'query'
22
22
  ? request.query?.[param.name]
23
23
  : request.body?.[param.name];
24
- if (given === undefined || given === '')
24
+ // missing and empty are two refusals, the way the schema has them: a
25
+ // client that sent the field an empty value has a different bug from one
26
+ // that omitted it
27
+ if (given === undefined)
25
28
  throw new ApiError(400, `"${param.name}" is required`);
29
+ if (given === '')
30
+ throw new ApiError(400, `"${param.name}" is not allowed to be empty`);
26
31
  }
27
32
  };
28
33
  /** The drive path a write names: the query parameter wins over the form field. */
@@ -41,7 +46,7 @@ export const dispatch = async (services, routeId, request = {}) => {
41
46
  const route = routeById(routeId);
42
47
  if (!route)
43
48
  throw new ApiError(404, `No such operation: ${routeId}`);
44
- const { drive, identity, records } = services;
49
+ const { drive, identity, records, code } = services;
45
50
  // The server authenticates before its payload schema runs, so an anonymous
46
51
  // request with an incomplete body is a 401 there rather than a 400. The order
47
52
  // is only observable on the route groups that gate on a caller: the sign-in
@@ -89,7 +94,7 @@ export const dispatch = async (services, routeId, request = {}) => {
89
94
  status: 200,
90
95
  body: await identity.token({
91
96
  clientId: String(body.clientId ?? ''),
92
- code: String(body.code ?? '')
97
+ code: body.code
93
98
  })
94
99
  };
95
100
  case 'POST /SASjsApi/auth/refresh':
@@ -216,6 +221,26 @@ export const dispatch = async (services, routeId, request = {}) => {
216
221
  status: 200,
217
222
  body: await required().deleteUser(await caller(), { username: String(params.username ?? '') }, { password: body.password })
218
223
  };
224
+ case 'POST /SASjsApi/code/lint':
225
+ if (!code)
226
+ throw new ApiError(501, `This host has no code service, so it cannot answer ${routeId}.`);
227
+ return {
228
+ status: 200,
229
+ body: await code.lint(await caller(), {
230
+ code: body.code,
231
+ filePath: body.filePath
232
+ })
233
+ };
234
+ case 'POST /SASjsApi/code/format':
235
+ if (!code)
236
+ throw new ApiError(501, `This host has no code service, so it cannot answer ${routeId}.`);
237
+ return {
238
+ status: 200,
239
+ body: await code.format(await caller(), {
240
+ code: body.code,
241
+ filePath: body.filePath
242
+ })
243
+ };
219
244
  case 'GET /SASjsApi/info/authorizedRoutes':
220
245
  return {
221
246
  status: 200,
@@ -328,6 +353,8 @@ export const dispatch = async (services, routeId, request = {}) => {
328
353
  };
329
354
  /** Whether a route is one `dispatch` can answer today. */
330
355
  export const HANDLED_ROUTES = [
356
+ 'POST /SASjsApi/code/lint',
357
+ 'POST /SASjsApi/code/format',
331
358
  'GET /SASjsApi/info/authorizedRoutes',
332
359
  'GET /SASjsApi/info/myAuthorizedRoutes',
333
360
  'GET /SASjsApi/drive/file',
@@ -29,6 +29,7 @@ export * from './identity/routeAccess.js';
29
29
  export type { ApiRequest, ApiResponse, Services } from './host/dispatch.js';
30
30
  export { createIdentityService } from './identity/service.js';
31
31
  export { createRecordService, sanitiseDisplayName } from './identity/records.js';
32
+ export { CODE_CORPUS } from './corpus/code.js';
32
33
  export type { AuthorizedRoutesResponse, GroupDetailsResponse, GroupResponse, PermissionResponse, RecordServiceOptions, RecordService, UserDetailsResponse, UserResponse } from './identity/records.js';
33
34
  export type { IdentityService, LoginResult, SessionResult } from './identity/service.js';
34
35
  export { MemoryIdentityStore } from './identity/memory-store.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,cAAc,YAAY,CAAA;AAC1B,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,OAAO,EACP,QAAQ,EACR,MAAM,EACP,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AACvD,OAAO,EACL,aAAa,EACb,oBAAoB,EACpB,cAAc,EACd,eAAe,EACf,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,iBAAiB,EAClB,MAAM,mBAAmB,CAAA;AAC1B,YAAY,EACV,YAAY,EACZ,kBAAkB,EAClB,YAAY,EACZ,aAAa,EACb,UAAU,EACX,MAAM,mBAAmB,CAAA;AAC1B,YAAY,EACV,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,SAAS,EACT,SAAS,EACV,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAA;AAC1D,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAChE,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AAC/D,YAAY,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAG7D,cAAc,2BAA2B,CAAA;AACzC,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAC3E,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AAC7D,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAA;AAChF,YAAY,EACV,wBAAwB,EACxB,oBAAoB,EACpB,aAAa,EACb,kBAAkB,EAClB,oBAAoB,EACpB,aAAa,EACb,mBAAmB,EACnB,YAAY,EACb,MAAM,uBAAuB,CAAA;AAC9B,YAAY,EACV,eAAe,EACf,WAAW,EACX,aAAa,EACd,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AAChE,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,aAAa,EACd,MAAM,uBAAuB,CAAA;AAC9B,YAAY,EACV,eAAe,EACf,qBAAqB,EACrB,mBAAmB,EACnB,oBAAoB,EACpB,yBAAyB,EACzB,oBAAoB,EACpB,mBAAmB,EACpB,MAAM,uBAAuB,CAAA;AAC9B,YAAY,EACV,cAAc,EACd,oBAAoB,EACpB,mBAAmB,EACnB,wBAAwB,EACxB,aAAa,EACb,YAAY,EACZ,kBAAkB,EAClB,QAAQ,EACR,aAAa,EACb,OAAO,EACP,OAAO,EACP,SAAS,EACT,UAAU,EACX,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,EACL,aAAa,EACb,eAAe,EACf,gBAAgB,EACjB,MAAM,qBAAqB,CAAA;AAC5B,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AACzD,YAAY,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,cAAc,YAAY,CAAA;AAC1B,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,OAAO,EACP,QAAQ,EACR,MAAM,EACP,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AACvD,OAAO,EACL,aAAa,EACb,oBAAoB,EACpB,cAAc,EACd,eAAe,EACf,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,iBAAiB,EAClB,MAAM,mBAAmB,CAAA;AAC1B,YAAY,EACV,YAAY,EACZ,kBAAkB,EAClB,YAAY,EACZ,aAAa,EACb,UAAU,EACX,MAAM,mBAAmB,CAAA;AAC1B,YAAY,EACV,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,SAAS,EACT,SAAS,EACV,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAA;AAC1D,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAChE,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AAC/D,YAAY,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAG7D,cAAc,2BAA2B,CAAA;AACzC,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAC3E,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AAC7D,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAA;AAChF,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC9C,YAAY,EACV,wBAAwB,EACxB,oBAAoB,EACpB,aAAa,EACb,kBAAkB,EAClB,oBAAoB,EACpB,aAAa,EACb,mBAAmB,EACnB,YAAY,EACb,MAAM,uBAAuB,CAAA;AAC9B,YAAY,EACV,eAAe,EACf,WAAW,EACX,aAAa,EACd,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AAChE,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,aAAa,EACd,MAAM,uBAAuB,CAAA;AAC9B,YAAY,EACV,eAAe,EACf,qBAAqB,EACrB,mBAAmB,EACnB,oBAAoB,EACpB,yBAAyB,EACzB,oBAAoB,EACpB,mBAAmB,EACpB,MAAM,uBAAuB,CAAA;AAC9B,YAAY,EACV,cAAc,EACd,oBAAoB,EACpB,mBAAmB,EACnB,wBAAwB,EACxB,aAAa,EACb,YAAY,EACZ,kBAAkB,EAClB,QAAQ,EACR,aAAa,EACb,OAAO,EACP,OAAO,EACP,SAAS,EACT,UAAU,EACX,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,EACL,aAAa,EACb,eAAe,EACf,gBAAgB,EACjB,MAAM,qBAAqB,CAAA;AAC5B,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AACzD,YAAY,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA"}
@@ -25,6 +25,7 @@ export { dispatch, HANDLED_ROUTES } from './host/dispatch.js';
25
25
  export * from './identity/routeAccess.js';
26
26
  export { createIdentityService } from './identity/service.js';
27
27
  export { createRecordService, sanitiseDisplayName } from './identity/records.js';
28
+ export { CODE_CORPUS } from './corpus/code.js';
28
29
  export { MemoryIdentityStore } from './identity/memory-store.js';
29
30
  export { DEFAULT_IDENTITY, IDENTITY_FORMAT, parseIdentity } from './identity/fixture.js';
30
31
  export { DRIVE_CORPUS } from './corpus/drive.js';
@@ -1 +1 @@
1
- {"version":3,"file":"overlay.d.ts","sourceRoot":"","sources":["../../../src/routes/overlay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAGzD,yDAAyD;AACzD,UAAU,aAAa;IACrB,YAAY,CAAC,EAAE,UAAU,EAAE,CAAA;IAC3B;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACpC;AAED;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GAAG,aAAa,GACtC,CAAC;IAAE,WAAW,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,WAAW,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAA;AAElE,kFAAkF;AAClF,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,CAczD,CAAA;AA8CD,eAAO,MAAM,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAmNhD,CAAA;AAED,wEAAwE;AACxE,eAAO,MAAM,OAAO,EAAE,UAAU,EAA8C,CAAA"}
1
+ {"version":3,"file":"overlay.d.ts","sourceRoot":"","sources":["../../../src/routes/overlay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAGzD,yDAAyD;AACzD,UAAU,aAAa;IACrB,YAAY,CAAC,EAAE,UAAU,EAAE,CAAA;IAC3B;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACpC;AAED;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GAAG,aAAa,GACtC,CAAC;IAAE,WAAW,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,WAAW,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAA;AAElE,kFAAkF;AAClF,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,CAczD,CAAA;AA2CD,eAAO,MAAM,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAyNhD,CAAA;AAED,wEAAwE;AACxE,eAAO,MAAM,OAAO,EAAE,UAAU,EAA8C,CAAA"}
@@ -23,7 +23,6 @@ export const TAG_CAPABILITIES = {
23
23
  * thing thirty times.
24
24
  */
25
25
  const DIRECTORY = 'Synchronising with an external auth provider needs a directory port, which this package does not have: a host decides which directory it holds accounts in, and answers this itself.';
26
- const EDITOR = 'Linting and formatting come from @sasjs/lint, which this package does not depend on. They land with the code routes so an editor has one wiring.';
27
26
  const RUNTIME = 'Starting a program needs a runtime port, which this package does not have: a host decides which languages it holds, and answers this itself until then.';
28
27
  const SESSION_RECORD = "A triggered session's record, log, output and state belong to the runtime that ran the program, and this package has no runtime port.";
29
28
  const HOST_CONFIG = "The body is the host's own configuration - ports, run times and providers - which a mock states for itself and this package cannot know.";
@@ -93,8 +92,14 @@ export const OVERLAY = {
93
92
  // Code - the editor's three calls, and the runtime one
94
93
  'POST /SASjsApi/code/execute': { implemented: false, reason: RUNTIME },
95
94
  'POST /SASjsApi/code/trigger': { implemented: false, reason: RUNTIME },
96
- 'POST /SASjsApi/code/lint': { implemented: false, reason: EDITOR },
97
- 'POST /SASjsApi/code/format': { implemented: false, reason: EDITOR },
95
+ 'POST /SASjsApi/code/lint': {
96
+ implemented: true,
97
+ capabilities: ['identity']
98
+ },
99
+ 'POST /SASjsApi/code/format': {
100
+ implemented: true,
101
+ capabilities: ['identity']
102
+ },
98
103
  // Drive - the file and folder rules, and the three deploy routes
99
104
  // The body is the file itself, so there is nothing to annotate
100
105
  'GET /SASjsApi/drive/file': { implemented: true },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sasjs/api",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "The SASjs Server API, described once - a generated route table, the services behind it, and the conformance corpus both hosts run",
5
5
  "license": "MIT",
6
6
  "author": "SASjs",
@@ -17,6 +17,11 @@
17
17
  "types": "./build/esm/index.d.ts",
18
18
  "import": "./build/esm/index.js",
19
19
  "require": "./build/cjs/index.js"
20
+ },
21
+ "./code": {
22
+ "types": "./build/esm/code/index.d.ts",
23
+ "import": "./build/esm/code/index.js",
24
+ "require": "./build/cjs/code/index.js"
20
25
  }
21
26
  },
22
27
  "files": [
@@ -60,5 +65,8 @@
60
65
  "deepmerge": "$deepmerge",
61
66
  "glob": "14.0.3",
62
67
  "minimatch": "11.0.4"
68
+ },
69
+ "dependencies": {
70
+ "@sasjs/lint": "5.0.0"
63
71
  }
64
72
  }