@sasjs/server 3.6.2 → 3.7.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/README.md CHANGED
@@ -19,55 +19,61 @@ SASjs Server is available in two modes - Desktop (without authentication) and Se
19
19
 
20
20
  ## Installation
21
21
 
22
- Installation can be made programmatically using command line, or by manually downloading and running the executable.
22
+ Two channels - the npm package and the standalone executable. Both read the same
23
+ environment variables and serve the same API.
23
24
 
24
- ### Programmatic
25
+ ### npm
25
26
 
26
- Fetch the relevant package from github using `curl`, eg as follows (for linux):
27
+ ```bash
28
+ npx @sasjs/server@3.6.2
29
+ ```
30
+
31
+ Or install it once and run `sasjs-server` from then on:
27
32
 
28
33
  ```bash
29
- curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
30
- unzip linux.zip
34
+ npm install -g @sasjs/server
35
+ sasjs-server
31
36
  ```
32
37
 
33
- The app can then be launched with `./api-linux` and prompts followed (if ENV vars not set).
38
+ Requires Node 22 or later. Useful where Node is already managed by the platform,
39
+ and the only channel that runs on arm64 (Apple Silicon, Graviton, a Pi), because
40
+ the standalone executables are built for x64 only.
34
41
 
35
- ### Manual
42
+ Three things to know before choosing it:
36
43
 
37
- 1. Download the relevant package from the [releases](https://github.com/sasjs/server/releases) page
38
- 2. Trigger by double clicking (windows) or executing from commandline.
44
+ - **Pin the version**, as above: an unpinned `npx @sasjs/server` re-resolves
45
+ `latest` on every run, so a restart becomes a silent upgrade.
46
+ - **`.env` is read from the working directory.** `dotenv.config()` is relative to
47
+ where the process starts, so run it from the project directory you want the
48
+ server to own (that directory also gets the `./sasjs_root`). Under `npx` the
49
+ package itself lives in an evictable `~/.npm/_npx/<hash>` cache, so an `.env`
50
+ placed there would be lost - it belongs beside the command, not the code.
51
+ - **It starts in desktop mode**, the single-user mode. The `RUN_AS` check still
52
+ refuses to run as root, so install it under the account that should own the SAS
53
+ sessions rather than starting it with `sudo`.
39
54
 
40
- You are presented with two prompts (if not set as ENV vars):
55
+ Installing the package runs no install script, so it makes no network call of its
56
+ own.
41
57
 
42
- - Location of your `sas.exe` / `sas.sh` executable
43
- - Path to a filesystem location for Stored Programs and temporary files
58
+ ### Standalone executable
44
59
 
45
- ## Deployment options
60
+ Download the relevant package from the [releases](https://github.com/sasjs/server/releases)
61
+ page, or fetch it programmatically (linux):
46
62
 
47
- The server runs as a standalone executable (above) and as a Node process. Both
48
- read the same environment variables and serve the same API.
63
+ ```bash
64
+ curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
65
+ unzip linux.zip
66
+ ```
49
67
 
50
- - **Standalone executable** - the zip from the releases page, with SAS and a data
51
- path supplied either as prompts or as `SAS_PATH` / `DATA_DIR`.
52
- - **Node** - `npx @sasjs/server@3.4.0`, or `npm install -g @sasjs/server` and run
53
- `sasjs-server`. Useful where Node is already managed by the platform, and the
54
- only channel that runs on arm64 (Apple Silicon, Graviton, a Pi), because the
55
- standalone executables are built for x64 only.
68
+ Then launch `./api-linux` (on windows, double click `api-win.exe` - no
69
+ administrative privileges are needed) and answer the two prompts, unless the ENV
70
+ vars are set:
56
71
 
57
- Both channels read the same environment variables and serve the same API, with
58
- two differences worth knowing before choosing the Node one:
72
+ - Location of your `sas.exe` / `sas.sh` executable
73
+ - Path to a filesystem location for Stored Programs and temporary files
59
74
 
60
- - **Pin the version.** An unpinned `npx @sasjs/server` re-resolves `latest` on
61
- every run, so a restart becomes a silent upgrade. The examples above pin it.
62
- - **`.env` is read from the working directory.** `dotenv.config()` is relative to
63
- where the process starts, so run it from the project directory you want the
64
- server to own (that directory also gets the `./sasjs_root`). Under `npx` the
65
- package itself lives in an evictable `~/.npm/_npx/<hash>` cache, so an `.env`
66
- placed there would be lost - it belongs beside the command, not the code.
75
+ ## Deployment options
67
76
 
68
- The Node channel starts in desktop mode, which is the single-user mode; the
69
- `RUN_AS` check still refuses to run as root, so install it under the account that
70
- should own the SAS sessions rather than starting it with `sudo`.
71
77
  - **Embedded** - the server's pieces are also used inside other SASjs tooling,
72
78
  such as the VS Code extension, and the mock backend runs in the browser for
73
79
  front-end development without a server at all.
@@ -98,6 +98,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
98
98
  * the unit-test run deliberately excludes.
99
99
  */
100
100
  var path_1 = __importDefault(require("path"));
101
+ var fs_1 = require("fs");
101
102
  var promises_1 = require("fs/promises");
102
103
  var mongoose_1 = __importDefault(require("mongoose"));
103
104
  var mongodb_memory_server_1 = require("mongodb-memory-server");
@@ -140,7 +141,11 @@ describe('Studio in a browser', function () {
140
141
  case 2:
141
142
  _a.sent();
142
143
  _a.label = 3;
143
- case 3: return [4 /*yield*/, (0, utils_1.fileExists)(path_1.default.join((0, utils_2.getWebBuildFolder)(), 'index.bundle.js'))];
144
+ case 3:
145
+ // The screenshots folder is gitignored, so a fresh checkout has no such
146
+ // directory and a failure screenshot would fail on the write itself.
147
+ (0, fs_1.mkdirSync)(screenshotDir, { recursive: true });
148
+ return [4 /*yield*/, (0, utils_1.fileExists)(path_1.default.join((0, utils_2.getWebBuildFolder)(), 'index.bundle.js'))];
144
149
  case 4:
145
150
  if (!(_a.sent())) {
146
151
  throw new Error("No web build at ".concat((0, utils_2.getWebBuildFolder)(), ". Run `npm run build` in web/ before the browser test."));
@@ -185,7 +190,10 @@ describe('Studio in a browser', function () {
185
190
  // An admin passes every route check, which keeps this test about the
186
191
  // editor rather than about the permission model.
187
192
  expect(dbUser.isAdmin).toBe(true);
188
- process.runTimes = [utils_2.RunTimeType.JS];
193
+ // The fixture is a .sas program, and this suite never executes code for
194
+ // real - the log case fulfils the execute request in the browser - so one
195
+ // runtime, the one the fixture needs, keeps the editor from switching it.
196
+ process.runTimes = [utils_2.RunTimeType.SAS];
189
197
  process.nodeLoc = process.execPath;
190
198
  return [4 /*yield*/, app];
191
199
  case 13:
@@ -200,7 +208,15 @@ describe('Studio in a browser', function () {
200
208
  return [4 /*yield*/, browser.newPage({ viewport: { width: 1280, height: 800 } })];
201
209
  case 15:
202
210
  page = _a.sent();
203
- page.on('pageerror', function (error) { return pageErrors.push(String(error)); });
211
+ page.on('pageerror', function (error) {
212
+ // A request the app aborts surfaces here as an unhandled rejection with
213
+ // axios's "Canceled: Canceled". That is a request superseded by a newer one
214
+ // - the editor re-syncing, a tab switching - not a fault in the tree, which
215
+ // is what this collector exists to catch.
216
+ if (String(error).includes('Canceled: Canceled'))
217
+ return;
218
+ pageErrors.push(String(error));
219
+ });
204
220
  page.on('request', function (request) {
205
221
  var _a;
206
222
  if (!request.url().includes('/SASjsApi/drive/fileTree'))
@@ -576,4 +592,83 @@ describe('Studio in a browser', function () {
576
592
  }
577
593
  });
578
594
  }); }, 60000);
595
+ it('lists the errors, warnings and notable notes of a SAS log, and hides the notes on request', function () { return __awaiter(void 0, void 0, void 0, function () {
596
+ var log, _a, _b, _c, stored;
597
+ return __generator(this, function (_d) {
598
+ switch (_d.label) {
599
+ case 0:
600
+ log = [
601
+ 'NOTE: SAS (r) Proprietary Software 9.4 (TS1M9 MBCS3170)',
602
+ '1 data want;',
603
+ '2 x = age / 0;',
604
+ '3 run;',
605
+ 'WARNING: Division by zero detected at line 3 column 11.',
606
+ 'NOTE: Variable y is uninitialized.',
607
+ 'ERROR Libref X is not assigned.',
608
+ 'NOTE 49-169: The meaning of an identifier after a quoted string may change.',
609
+ 'ERROR: File WORK.MISSING_DS.DATA does not exist.',
610
+ ''
611
+ ].join('\n');
612
+ return [4 /*yield*/, page.route('**/SASjsApi/code/execute', function (route) {
613
+ return route.fulfill({
614
+ status: 200,
615
+ contentType: 'text/plain',
616
+ body: ['<html></html>', log, ''].join('SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784')
617
+ });
618
+ })
619
+ // The fixture program is a .sas file, so opening it selects the SAS runtime
620
+ // and the log pane will render the parsed shape.
621
+ ];
622
+ case 1:
623
+ _d.sent();
624
+ // The fixture program is a .sas file, so opening it selects the SAS runtime
625
+ // and the log pane will render the parsed shape.
626
+ return [4 /*yield*/, page.click('button:has-text("RUN")')];
627
+ case 2:
628
+ // The fixture program is a .sas file, so opening it selects the SAS runtime
629
+ // and the log pane will render the parsed shape.
630
+ _d.sent();
631
+ return [4 /*yield*/, page.waitForSelector('text=Errors (2)', { timeout: 30000 })];
632
+ case 3:
633
+ _d.sent();
634
+ _a = expect;
635
+ return [4 /*yield*/, page.isVisible('text=Warnings (1)')];
636
+ case 4:
637
+ _a.apply(void 0, [_d.sent()]).toBe(true);
638
+ _b = expect;
639
+ return [4 /*yield*/, page.isVisible('text=Notes (2)')];
640
+ case 5:
641
+ _b.apply(void 0, [_d.sent()]).toBe(true);
642
+ return [4 /*yield*/, page.screenshot({
643
+ path: path_1.default.join(screenshotDir, '02-log-issues.png')
644
+ })
645
+ // The switch hides them without another run.
646
+ ];
647
+ case 6:
648
+ _d.sent();
649
+ // The switch hides them without another run.
650
+ return [4 /*yield*/, page.click('[aria-label="Hide notable notes"]')];
651
+ case 7:
652
+ // The switch hides them without another run.
653
+ _d.sent();
654
+ return [4 /*yield*/, page.waitForSelector('text=Notes (2)', {
655
+ state: 'detached',
656
+ timeout: 30000
657
+ })];
658
+ case 8:
659
+ _d.sent();
660
+ _c = expect;
661
+ return [4 /*yield*/, page.isVisible('text=Errors (2)')];
662
+ case 9:
663
+ _c.apply(void 0, [_d.sent()]).toBe(true);
664
+ return [4 /*yield*/, page.evaluate(function () {
665
+ return window.localStorage.getItem('studio-settings');
666
+ })];
667
+ case 10:
668
+ stored = _d.sent();
669
+ expect(JSON.parse(stored)['log.notableNotes']).toBe(false);
670
+ return [2 /*return*/];
671
+ }
672
+ });
673
+ }); }, 120000);
579
674
  });
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.BUILD_INFO = void 0;
4
4
  // Generated by scripts/generateBuildInfo.ts. Do not edit by hand.
5
5
  exports.BUILD_INFO = {
6
- version: "v3.6.2",
7
- gitHash: "6141e36",
8
- buildDate: "2026-10-30T14:54:32.248Z"
6
+ version: "v3.7.0",
7
+ gitHash: "d1cc0c9",
8
+ buildDate: "2026-11-01T21:13:10.033Z"
9
9
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sasjs/server",
3
- "version": "3.6.2",
3
+ "version": "3.7.0",
4
4
  "description": "NodeJS wrapper for calling the SAS binary executable",
5
5
  "license": "MIT",
6
6
  "repository": "https://github.com/sasjs/server",
@@ -35,7 +35,7 @@
35
35
  "express-session": "^1.19.0",
36
36
  "helmet": "^8.3.0",
37
37
  "http-headers-validation": "^0.0.2",
38
- "joi": "^18.2.9",
38
+ "joi": "^18.3.1",
39
39
  "jose": "5.10.0",
40
40
  "jsonwebtoken": "^9.0.3",
41
41
  "ldapjs": "2.3.3",