1
0
mirror of https://github.com/sasjs/server.git synced 2025-12-10 19:34:34 +00:00

Compare commits

...

80 Commits

Author SHA1 Message Date
semantic-release-bot
471c28eaa2 chore(release): 0.39.2 [skip ci]
## [0.39.2](https://github.com/sasjs/server/compare/v0.39.1...v0.39.2) (2025-09-25)

### Bug Fixes

* addressing test fail ([e51b204](e51b20421a))
* packages missmatch ([379ea60](379ea604bc))
* type libs ([6d123c3](6d123c3e23))
* typescript errors ([631e956](631e95604b))
* typescript errors ([198cd79](198cd79354))
2025-09-25 16:57:01 +00:00
Allan Bowe
584ffe9e0e Merge pull request #383 from sasjs/npm_update_20250919
Npm update 20250919
2025-09-25 17:53:46 +01:00
M
e51b20421a fix: addressing test fail 2025-09-25 13:49:32 +02:00
M
631e95604b fix: typescript errors 2025-09-25 13:40:10 +02:00
M
198cd79354 fix: typescript errors 2025-09-25 13:34:55 +02:00
M
379ea604bc fix: packages missmatch 2025-09-25 13:12:23 +02:00
M
9ffa403bcb chore: package-lock 2025-09-25 13:06:06 +02:00
M
6d123c3e23 fix: type libs 2025-09-25 13:03:47 +02:00
M
dda1aadc67 chore(git): Merge branch 'main' into npm_update_20250919 2025-09-25 12:48:10 +02:00
M
d47cf15cdb ci: ubuntu 22 2025-09-25 12:46:19 +02:00
Trevor Moody
d0c7968d66 build: updated package dependencies for /web 2025-09-19 18:24:58 +01:00
Trevor Moody
a5c99971cc build: server/api dependency update 2025-09-19 14:06:50 +01:00
semantic-release-bot
c422e7f02e chore(release): 0.39.1 [skip ci]
## [0.39.1](https://github.com/sasjs/server/compare/v0.39.0...v0.39.1) (2025-03-13)

### Bug Fixes

* extra bit of sleep for file recognition ([f4768bf](f4768bffd3)), closes [#381](https://github.com/sasjs/server/issues/381)
2025-03-13 10:59:10 +00:00
Allan Bowe
02a993611c Merge pull request #382 from sasjs/381-add-slight-delay-to-enable-file-detection
fix: extra bit of sleep for file recognition
2025-03-13 10:56:13 +00:00
aca2fff4ac chore(workflow): run the build workflow on ubuntu 20.04 2025-03-13 15:50:23 +05:00
af1a386b13 chore(workflow): install openssl 1.1 in actions 2025-03-13 15:43:20 +05:00
Allan Bowe
f5018ce1df chore: prettier fix 2025-03-12 17:33:02 +00:00
Allan Bowe
3529232f1f chore: whitespace removal 2025-03-12 17:29:02 +00:00
Allan Bowe
f4768bffd3 fix: extra bit of sleep for file recognition
closes #381
2025-03-12 17:27:57 +00:00
semantic-release-bot
c261745f1d chore(release): 0.39.0 [skip ci]
# [0.39.0](https://github.com/sasjs/server/compare/v0.38.0...v0.39.0) (2024-10-31)

### Bug Fixes

* **api:** fixed condition in processProgram ([48a9a4d](48a9a4dd0e))

### Features

* **api:** added session state endpoint ([6b6546c](6b6546c7ad))
2024-10-31 12:54:02 +00:00
Yury Shkoda
d6e527ecf2 Merge pull request #379 from sasjs/issue-378
Issue 378
2024-10-31 15:51:13 +03:00
Yury
bc2cff1d0d chore(api): updated trigger endpoints description 2024-10-31 15:30:32 +03:00
Yury
66aa9b5891 chore(api): updated trigger endpoints description 2024-10-31 15:20:35 +03:00
Yury
ca17e7c192 chore(api): updated endpoint description 2024-10-31 14:08:56 +03:00
Yury
73df102422 chore(api): updated endpoint description 2024-10-31 12:27:56 +03:00
Yury
48a9a4dd0e fix(api): fixed condition in processProgram 2024-10-31 11:17:20 +03:00
Yury
4f6f735f5b chore(lint): fixed code style issue 2024-10-31 10:08:34 +03:00
Yury
6b6546c7ad feat(api): added session state endpoint 2024-10-30 17:42:50 +03:00
Yury
f94ddc0352 refactor(session): implemented session state 2024-10-30 15:33:06 +03:00
Yury
03670cf0d6 chore(swagger): fixed code/stp trigger examples 2024-10-30 15:25:03 +03:00
semantic-release-bot
ea2ec97c1c chore(release): 0.38.0 [skip ci]
# [0.38.0](https://github.com/sasjs/server/compare/v0.37.0...v0.38.0) (2024-10-30)

### Features

* **api:** enabled query params in stp/trigger endpoint ([5cda9cd](5cda9cd5d8))
2024-10-30 09:25:17 +00:00
Yury Shkoda
832f1156e8 Merge pull request #377 from sasjs/issue-373-stp-fix
feat(api): enabled query params in stp/trigger endpoint
2024-10-30 12:22:10 +03:00
Yury
5cda9cd5d8 feat(api): enabled query params in stp/trigger endpoint 2024-10-30 09:39:47 +03:00
semantic-release-bot
5d576aff91 chore(release): 0.37.0 [skip ci]
# [0.37.0](https://github.com/sasjs/server/compare/v0.36.0...v0.37.0) (2024-10-29)

### Features

* **stp:** added trigger endpoint ([b0723f1](b0723f1444))
2024-10-29 14:11:35 +00:00
Yury Shkoda
a044176054 Merge pull request #375 from sasjs/issue-373-stp
Issue 373 stp
2024-10-29 17:08:38 +03:00
Yury
deee34f5fd chore(stp): removed query logic from trigger endpoint 2024-10-29 16:55:40 +03:00
Yury
b0723f1444 feat(stp): added trigger endpoint 2024-10-29 16:27:53 +03:00
Yury
e9519cb3c6 chore(code): used correct type 2024-10-29 16:20:27 +03:00
semantic-release-bot
0838b8112e chore(release): 0.36.0 [skip ci]
# [0.36.0](https://github.com/sasjs/server/compare/v0.35.4...v0.36.0) (2024-10-29)

### Features

* **code:** added code/trigger API endpoint ([ffcf193](ffcf193b87))
2024-10-29 10:32:01 +00:00
Yury Shkoda
441f8b7726 Merge pull request #374 from sasjs/issue-373
feat(code): added code/trigger API endpoint
2024-10-29 13:29:08 +03:00
Yury
049a7f4b80 chore(swagger): improved description 2024-10-29 12:02:26 +03:00
Yury
3053c68bdf chore(lint): fixed linting issues 2024-10-29 11:40:44 +03:00
Yury
76750e864d chore(lint): fixed lint issue 2024-10-29 11:30:05 +03:00
Yury
ffcf193b87 feat(code): added code/trigger API endpoint 2024-10-29 11:18:04 +03:00
semantic-release-bot
aa2a1cbe13 chore(release): 0.35.4 [skip ci]
## [0.35.4](https://github.com/sasjs/server/compare/v0.35.3...v0.35.4) (2024-01-15)

### Bug Fixes

* **api:** fixed env issue in MacOS executable ([73d965d](73d965daf5))
2024-01-15 13:21:15 +00:00
Yury Shkoda
6f2c53555c Merge pull request #372 from sasjs/issue-371
fix(api): fixed env issue in MacOS executable
2024-01-15 16:18:10 +03:00
Yury
73d965daf5 fix(api): fixed env issue in MacOS executable 2024-01-15 15:14:06 +03:00
semantic-release-bot
4f1763db67 chore(release): 0.35.3 [skip ci]
## [0.35.3](https://github.com/sasjs/server/compare/v0.35.2...v0.35.3) (2023-11-07)

### Bug Fixes

* enable embedded LFs in JS STP vars ([7e8cbbf](7e8cbbf377))
2023-11-07 20:48:28 +00:00
Allan Bowe
28222add04 Merge pull request #370 from sasjs/allanbowe-patch-1
fix: enable embedded LFs in JS STP vars
2023-11-07 20:43:16 +00:00
Allan
068edfd6a5 chore: lint fix 2023-11-07 20:39:05 +00:00
Allan Bowe
7e8cbbf377 fix: enable embedded LFs in JS STP vars 2023-11-07 15:51:32 +00:00
Allan Bowe
1fc1431442 chore: using GITHUB_TOKEN 2023-08-07 20:11:40 +01:00
semantic-release-bot
3387efbb9a chore(release): 0.35.2 [skip ci]
## [0.35.2](https://github.com/sasjs/server/compare/v0.35.1...v0.35.2) (2023-08-07)

### Bug Fixes

* add _debug as optional query param in swagger apis for GET stp/execute ([9586dbb](9586dbb2d0))
2023-08-07 18:53:12 +00:00
Allan Bowe
e2996b495f Merge pull request #365 from sasjs/swagger-fix
fix: add _debug as optional query param in swagger apis for  stp/execute
2023-08-07 19:48:28 +01:00
Allan
41c627f93a chore: lint fix 2023-08-07 19:39:02 +01:00
Allan Bowe
49f5dc7555 Update swagger.yaml 2023-08-07 19:32:29 +01:00
Allan Bowe
f6e77f99a4 Update swagger.yaml 2023-08-07 19:31:20 +01:00
Allan Bowe
b57dfa429b Update stp.ts 2023-08-07 19:30:09 +01:00
9586dbb2d0 fix: add _debug as optional query param in swagger apis for GET stp/execute 2023-08-07 22:01:52 +05:00
semantic-release-bot
a4f78ab48d chore(release): 0.35.1 [skip ci]
## [0.35.1](https://github.com/sasjs/server/compare/v0.35.0...v0.35.1) (2023-07-25)

### Bug Fixes

* **log-separator:** log separator should always wrap log ([8940f4d](8940f4dc47))
2023-07-25 06:05:23 +00:00
Yury Shkoda
2f47a2213b Merge pull request #364 from sasjs/log-separator
fix(log-separator): log separator should always wrap log
2023-07-25 09:01:36 +03:00
Yury Shkoda
0f91395fbb lint: fixed linting issues 2023-07-24 18:36:08 +03:00
Yury Shkoda
167b14fed0 docs(log-separator): left comment 2023-07-24 18:29:20 +03:00
Yury Shkoda
8940f4dc47 fix(log-separator): log separator should always wrap log 2023-07-24 18:27:21 +03:00
semantic-release-bot
48c1ada1b6 chore(release): 0.35.0 [skip ci]
# [0.35.0](https://github.com/sasjs/server/compare/v0.34.2...v0.35.0) (2023-05-03)

### Bug Fixes

* **editor:** fixed log/webout/print tabs ([d2de9dc](d2de9dc13e))
* **execute:** added atribute indicating stp api ([e78f87f](e78f87f5c0))
* **execute:** fixed adding print output ([9aaffce](9aaffce820))
* **execution:** removed empty webout from response ([6dd2f4f](6dd2f4f876))
* **webout:** fixed adding empty webout to response payload ([31df72a](31df72ad88))

### Features

* **editor:** parse print output in response payload ([eb42683](eb42683fff))
2023-05-03 09:34:56 +00:00
Allan Bowe
0532488b55 Merge pull request #360 from sasjs/issue-354
Support print destination natively
2023-05-03 10:31:06 +01:00
Yury Shkoda
d458b5bb81 chore: cleanup 2023-05-03 10:56:17 +03:00
Yury Shkoda
958ab9cad2 chore(execution): add includePrintOutput to ExecuteFileParams 2023-05-03 10:46:21 +03:00
Yury Shkoda
78ceed13e1 docs(code): updated execute endpoint info 2023-05-02 16:01:00 +03:00
Yury Shkoda
a17814fc90 chore(stp): removed redundant argument 2023-05-02 15:53:13 +03:00
Yury Shkoda
9aaffce820 fix(execute): fixed adding print output 2023-05-02 15:49:44 +03:00
Yury Shkoda
e78f87f5c0 fix(execute): added atribute indicating stp api 2023-05-02 15:18:05 +03:00
Yury Shkoda
bd1b58086d docs: left a comment regarding payload parts 2023-05-02 12:10:17 +03:00
Yury Shkoda
9f521634d9 chore(webout): added comment 2023-05-02 11:30:55 +03:00
Yury Shkoda
a696168443 Merge branch 'main' of github.com:sasjs/server into issue-354 2023-05-02 11:17:41 +03:00
Yury Shkoda
31df72ad88 fix(webout): fixed adding empty webout to response payload 2023-05-02 11:17:12 +03:00
Yury Shkoda
eb42683fff feat(editor): parse print output in response payload 2023-05-01 08:18:49 +03:00
Yury Shkoda
d2de9dc13e fix(editor): fixed log/webout/print tabs 2023-05-01 07:28:23 +03:00
Yury Shkoda
6dd2f4f876 fix(execution): removed empty webout from response 2023-04-28 17:25:30 +03:00
Yury Shkoda
c0f38ba7c9 wip(print-output): added print output to response payload 2023-04-28 15:09:44 +03:00
42 changed files with 12041 additions and 23639 deletions

View File

@@ -5,7 +5,7 @@ on:
jobs: jobs:
lint: lint:
runs-on: ubuntu-latest runs-on: ubuntu-22.04
strategy: strategy:
matrix: matrix:
@@ -28,7 +28,7 @@ jobs:
run: npm run lint-web run: npm run lint-web
build-api: build-api:
runs-on: ubuntu-latest runs-on: ubuntu-22.04
strategy: strategy:
matrix: matrix:
@@ -66,7 +66,7 @@ jobs:
CI: true CI: true
build-web: build-web:
runs-on: ubuntu-latest runs-on: ubuntu-22.04
strategy: strategy:
matrix: matrix:

View File

@@ -7,7 +7,7 @@ on:
jobs: jobs:
release: release:
runs-on: ubuntu-latest runs-on: ubuntu-22.04
strategy: strategy:
matrix: matrix:
@@ -56,4 +56,4 @@ jobs:
- name: Release - name: Release
run: | run: |
GITHUB_TOKEN=${{ secrets.GH_TOKEN }} semantic-release GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} semantic-release

View File

@@ -1,5 +1,3 @@
{ {
"cSpell.words": [ "cSpell.words": ["autoexec", "initialising"]
"autoexec"
]
} }

View File

@@ -1,3 +1,98 @@
## [0.39.2](https://github.com/sasjs/server/compare/v0.39.1...v0.39.2) (2025-09-25)
### Bug Fixes
* addressing test fail ([e51b204](https://github.com/sasjs/server/commit/e51b20421adc1598ea267c79b1fb4dbc085f97b9))
* packages missmatch ([379ea60](https://github.com/sasjs/server/commit/379ea604bcb5686b5299fae6a32f759c45b275ea))
* type libs ([6d123c3](https://github.com/sasjs/server/commit/6d123c3e23628c1d703eaa13142c77f0da970a55))
* typescript errors ([631e956](https://github.com/sasjs/server/commit/631e95604b64b1a96f2abade659348618f3b00b2))
* typescript errors ([198cd79](https://github.com/sasjs/server/commit/198cd79354254511c21ac1acfbf7b6bcfdab2af7))
## [0.39.1](https://github.com/sasjs/server/compare/v0.39.0...v0.39.1) (2025-03-13)
### Bug Fixes
* extra bit of sleep for file recognition ([f4768bf](https://github.com/sasjs/server/commit/f4768bffd3dbb2fe243966572ba74002024d96e1)), closes [#381](https://github.com/sasjs/server/issues/381)
# [0.39.0](https://github.com/sasjs/server/compare/v0.38.0...v0.39.0) (2024-10-31)
### Bug Fixes
* **api:** fixed condition in processProgram ([48a9a4d](https://github.com/sasjs/server/commit/48a9a4dd0e31f84209635382be4ec4bb2c3a9c0c))
### Features
* **api:** added session state endpoint ([6b6546c](https://github.com/sasjs/server/commit/6b6546c7ad0833347f8dc4cdba6ad19132f7aaef))
# [0.38.0](https://github.com/sasjs/server/compare/v0.37.0...v0.38.0) (2024-10-30)
### Features
* **api:** enabled query params in stp/trigger endpoint ([5cda9cd](https://github.com/sasjs/server/commit/5cda9cd5d8623b7ea2ecd989d7808f47ec866672))
# [0.37.0](https://github.com/sasjs/server/compare/v0.36.0...v0.37.0) (2024-10-29)
### Features
* **stp:** added trigger endpoint ([b0723f1](https://github.com/sasjs/server/commit/b0723f14448d60ffce4f2175cf8a73fc4d4dd0ee))
# [0.36.0](https://github.com/sasjs/server/compare/v0.35.4...v0.36.0) (2024-10-29)
### Features
* **code:** added code/trigger API endpoint ([ffcf193](https://github.com/sasjs/server/commit/ffcf193b87d811b166d79af74013776a253b50b0))
## [0.35.4](https://github.com/sasjs/server/compare/v0.35.3...v0.35.4) (2024-01-15)
### Bug Fixes
* **api:** fixed env issue in MacOS executable ([73d965d](https://github.com/sasjs/server/commit/73d965daf54b16c0921e4b18d11a1e6f8650884d))
## [0.35.3](https://github.com/sasjs/server/compare/v0.35.2...v0.35.3) (2023-11-07)
### Bug Fixes
* enable embedded LFs in JS STP vars ([7e8cbbf](https://github.com/sasjs/server/commit/7e8cbbf377b27a7f5dd9af0bc6605c01f302f5d9))
## [0.35.2](https://github.com/sasjs/server/compare/v0.35.1...v0.35.2) (2023-08-07)
### Bug Fixes
* add _debug as optional query param in swagger apis for GET stp/execute ([9586dbb](https://github.com/sasjs/server/commit/9586dbb2d0d6611061c9efdfb84030144f62c2ee))
## [0.35.1](https://github.com/sasjs/server/compare/v0.35.0...v0.35.1) (2023-07-25)
### Bug Fixes
* **log-separator:** log separator should always wrap log ([8940f4d](https://github.com/sasjs/server/commit/8940f4dc47abae2036b4fcdeb772c31a0ca07cca))
# [0.35.0](https://github.com/sasjs/server/compare/v0.34.2...v0.35.0) (2023-05-03)
### Bug Fixes
* **editor:** fixed log/webout/print tabs ([d2de9dc](https://github.com/sasjs/server/commit/d2de9dc13ef2e980286dd03cca5e22cea443ed0c))
* **execute:** added atribute indicating stp api ([e78f87f](https://github.com/sasjs/server/commit/e78f87f5c00038ea11261dffb525ac8f1024e40b))
* **execute:** fixed adding print output ([9aaffce](https://github.com/sasjs/server/commit/9aaffce82051d81bf39adb69942bb321e9795141))
* **execution:** removed empty webout from response ([6dd2f4f](https://github.com/sasjs/server/commit/6dd2f4f87673336135bc7a6de0d2e143e192c025))
* **webout:** fixed adding empty webout to response payload ([31df72a](https://github.com/sasjs/server/commit/31df72ad88fe2c771d0ef8445d6db9dd147c40c9))
### Features
* **editor:** parse print output in response payload ([eb42683](https://github.com/sasjs/server/commit/eb42683fff701bd5b4d2b68760fe0c3ecad573dd))
## [0.34.2](https://github.com/sasjs/server/compare/v0.34.1...v0.34.2) (2023-05-01) ## [0.34.2](https://github.com/sasjs/server/compare/v0.34.1...v0.34.2) (2023-05-01)

View File

@@ -158,7 +158,7 @@ CORS=
WHITELIST= WHITELIST=
# HELMET Cross Origin Embedder Policy # HELMET Cross Origin Embedder Policy
# Sets the Cross-Origin-Embedder-Policy header to require-corp when `true` # Sets the Cross-Origin-Embedder-Policy header to require-corp when `true`
# options: [true|false] default: true # options: [true|false] default: true
# Docs: https://helmetjs.github.io/#reference (`crossOriginEmbedderPolicy`) # Docs: https://helmetjs.github.io/#reference (`crossOriginEmbedderPolicy`)
HELMET_COEP= HELMET_COEP=

View File

@@ -25,7 +25,7 @@ LDAP_USERS_BASE_DN = <ou=users,dc=cloudron>
LDAP_GROUPS_BASE_DN = <ou=groups,dc=cloudron> LDAP_GROUPS_BASE_DN = <ou=groups,dc=cloudron>
#default value is 100 #default value is 100
MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY=100 MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY=100
#default value is 10 #default value is 10
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP=10 MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP=10

20468
api/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -49,24 +49,24 @@
"author": "4GL Ltd", "author": "4GL Ltd",
"dependencies": { "dependencies": {
"@sasjs/core": "^4.40.1", "@sasjs/core": "^4.40.1",
"@sasjs/utils": "3.2.0", "@sasjs/utils": "^3.5.2",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"connect-mongo": "^4.6.0", "connect-mongo": "^5.1.0",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.7",
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^4.17.1", "express": "^4.21.2",
"express-session": "^1.17.2", "express-session": "^1.18.2",
"helmet": "^5.0.2", "helmet": "^5.0.2",
"joi": "^17.4.2", "joi": "^17.4.2",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^9.0.2",
"ldapjs": "2.3.3", "ldapjs": "2.3.3",
"mongoose": "^6.0.12", "mongoose": "^6.13.8",
"morgan": "^1.10.0", "morgan": "^1.10.1",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"rate-limiter-flexible": "2.4.1", "rate-limiter-flexible": "2.4.1",
"rotating-file-stream": "^3.0.4", "rotating-file-stream": "^3.0.4",
"swagger-ui-express": "4.3.0", "swagger-ui-express": "4.3.0",
"unzipper": "^0.10.11", "unzipper": "^0.12.3",
"url": "^0.10.3" "url": "^0.10.3"
}, },
"devDependencies": { "devDependencies": {
@@ -76,32 +76,32 @@
"@types/cors": "^2.8.12", "@types/cors": "^2.8.12",
"@types/express": "^4.17.12", "@types/express": "^4.17.12",
"@types/express-session": "^1.17.4", "@types/express-session": "^1.17.4",
"@types/jest": "^26.0.24", "@types/jest": "^29.5.0",
"@types/jsonwebtoken": "^8.5.5", "@types/jsonwebtoken": "^8.5.5",
"@types/ldapjs": "^2.2.4", "@types/ldapjs": "^2.2.4",
"@types/morgan": "^1.9.3", "@types/morgan": "^1.9.3",
"@types/multer": "^1.4.7", "@types/multer": "^1.4.7",
"@types/node": "^15.12.2", "@types/node": "^20.0.0",
"@types/supertest": "^2.0.11", "@types/supertest": "^2.0.11",
"@types/swagger-ui-express": "^4.1.3", "@types/swagger-ui-express": "^4.1.3",
"@types/unzipper": "^0.10.5", "@types/unzipper": "^0.10.5",
"adm-zip": "^0.5.9", "adm-zip": "^0.5.9",
"axios": "0.27.2", "axios": "^1.12.2",
"csrf": "^3.1.0", "csrf": "^3.1.0",
"dotenv": "^10.0.0", "dotenv": "^16.0.1",
"http-headers-validation": "^0.0.1", "http-headers-validation": "^0.0.1",
"jest": "^27.0.6", "jest": "^29.7.0",
"mongodb-memory-server": "8.11.4", "mongodb-memory-server": "8.11.4",
"nodejs-file-downloader": "4.10.2", "nodejs-file-downloader": "4.10.2",
"nodemon": "^2.0.7", "nodemon": "^3.0.0",
"pkg": "5.6.0", "pkg": "5.6.0",
"prettier": "^2.3.1", "prettier": "^3.0.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"supertest": "^6.1.3", "supertest": "^6.1.3",
"ts-jest": "^27.0.3", "ts-jest": "^29.1.0",
"ts-node": "^10.0.0", "ts-node": "^10.0.0",
"tsoa": "3.14.1", "tsoa": "3.14.1",
"typescript": "^4.3.2" "typescript": "^5.0.0"
}, },
"nodemonConfig": { "nodemonConfig": {
"ignore": [ "ignore": [

View File

@@ -98,17 +98,47 @@ components:
properties: properties:
code: code:
type: string type: string
description: 'Code of program' description: 'The code to be executed'
example: '* Code HERE;' example: '* Your Code HERE;'
runTime: runTime:
$ref: '#/components/schemas/RunTimeType' $ref: '#/components/schemas/RunTimeType'
description: 'runtime for program' description: 'The runtime for the code - eg SAS, JS, PY or R'
example: js example: js
required: required:
- code - code
- runTime - runTime
type: object type: object
additionalProperties: false additionalProperties: false
TriggerCodeResponse:
properties:
sessionId:
type: string
description: "`sessionId` is the ID of the session and the name of the temporary folder\nused to store code outputs.<br><br>\nFor SAS, this would be the location of the SASWORK folder.<br><br>\n`sessionId` can be used to poll session state using the\nGET /SASjsApi/session/{sessionId}/state endpoint."
example: 20241028074744-54132-1730101664824
required:
- sessionId
type: object
additionalProperties: false
TriggerCodePayload:
properties:
code:
type: string
description: 'The code to be executed'
example: '* Your Code HERE;'
runTime:
$ref: '#/components/schemas/RunTimeType'
description: 'The runtime for the code - eg SAS, JS, PY or R'
example: sas
expiresAfterMins:
type: number
format: double
description: "Amount of minutes after the completion of the job when the session must be\ndestroyed."
example: 15
required:
- code
- runTime
type: object
additionalProperties: false
MemberType.folder: MemberType.folder:
enum: enum:
- folder - folder
@@ -555,6 +585,14 @@ components:
- needsToUpdatePassword - needsToUpdatePassword
type: object type: object
additionalProperties: false additionalProperties: false
SessionState:
enum:
- initialising
- pending
- running
- completed
- failed
type: string
ExecutePostRequestPayload: ExecutePostRequestPayload:
properties: properties:
_program: _program:
@@ -563,6 +601,16 @@ components:
example: /Public/somefolder/some.file example: /Public/somefolder/some.file
type: object type: object
additionalProperties: false additionalProperties: false
TriggerProgramResponse:
properties:
sessionId:
type: string
description: "`sessionId` is the ID of the session and the name of the temporary folder\nused to store program outputs.<br><br>\nFor SAS, this would be the location of the SASWORK folder.<br><br>\n`sessionId` can be used to poll session state using the\nGET /SASjsApi/session/{sessionId}/state endpoint."
example: 20241028074744-54132-1730101664824
required:
- sessionId
type: object
additionalProperties: false
LoginPayload: LoginPayload:
properties: properties:
username: username:
@@ -792,7 +840,7 @@ paths:
- {type: string} - {type: string}
- {type: string, format: byte} - {type: string, format: byte}
description: 'Execute Code on the Specified Runtime' description: 'Execute Code on the Specified Runtime'
summary: 'Run Code and Return Webout Content and Log' summary: "Run Code and Return Webout Content, Log and Print output\nThe order of returned parts of the payload is:\n1. Webout (if present)\n2. Logs UUID (used as separator)\n3. Log\n4. Logs UUID (used as separator)\n5. Print (if present and if the runtime is SAS)\nPlease see"
tags: tags:
- Code - Code
security: security:
@@ -805,6 +853,30 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/ExecuteCodePayload' $ref: '#/components/schemas/ExecuteCodePayload'
/SASjsApi/code/trigger:
post:
operationId: TriggerCode
responses:
'200':
description: Ok
content:
application/json:
schema:
$ref: '#/components/schemas/TriggerCodeResponse'
description: 'Trigger Code on the Specified Runtime'
summary: 'Triggers code and returns SessionId immediately - does not wait for job completion'
tags:
- Code
security:
-
bearerAuth: []
parameters: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/TriggerCodePayload'
/SASjsApi/drive/deploy: /SASjsApi/drive/deploy:
post: post:
operationId: Deploy operationId: Deploy
@@ -1777,6 +1849,30 @@ paths:
- -
bearerAuth: [] bearerAuth: []
parameters: [] parameters: []
'/SASjsApi/session/{sessionId}/state':
get:
operationId: SessionState
responses:
'200':
description: Ok
content:
application/json:
schema:
$ref: '#/components/schemas/SessionState'
description: "The polling endpoint is currently implemented for single-server deployments only.<br>\nLoad balanced / grid topologies will be supported in a future release.<br>\nIf your site requires this, please reach out to SASjs Support."
summary: 'Get session state (initialising, pending, running, completed, failed).'
tags:
- Session
security:
-
bearerAuth: []
parameters:
-
in: path
name: sessionId
required: true
schema:
type: string
/SASjsApi/stp/execute: /SASjsApi/stp/execute:
get: get:
operationId: ExecuteGetRequest operationId: ExecuteGetRequest
@@ -1789,7 +1885,7 @@ paths:
anyOf: anyOf:
- {type: string} - {type: string}
- {type: string, format: byte} - {type: string, format: byte}
description: "Trigger a Stored Program using the _program URL parameter.\n\nAccepts URL parameters and file uploads. For more details, see docs:\n\nhttps://server.sasjs.io/storedprograms" description: "Trigger a Stored Program using the _program URL parameter.\n\nAccepts additional URL parameters (converted to session variables)\nand file uploads. For more details, see docs:\n\nhttps://server.sasjs.io/storedprograms"
summary: 'Execute a Stored Program, returns _webout and (optionally) log.' summary: 'Execute a Stored Program, returns _webout and (optionally) log.'
tags: tags:
- STP - STP
@@ -1798,13 +1894,22 @@ paths:
bearerAuth: [] bearerAuth: []
parameters: parameters:
- -
description: 'Location of code in SASjs Drive' description: 'Location of Stored Program in SASjs Drive.'
in: query in: query
name: _program name: _program
required: true required: true
schema: schema:
type: string type: string
example: /Projects/myApp/some/program example: /Projects/myApp/some/program
-
description: 'Optional query param for setting debug mode (returns the session log in the response body).'
in: query
name: _debug
required: false
schema:
format: double
type: number
example: 131
post: post:
operationId: ExecutePostRequest operationId: ExecutePostRequest
responses: responses:
@@ -1838,6 +1943,50 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/ExecutePostRequestPayload' $ref: '#/components/schemas/ExecutePostRequestPayload'
/SASjsApi/stp/trigger:
post:
operationId: TriggerProgram
responses:
'200':
description: Ok
content:
application/json:
schema:
$ref: '#/components/schemas/TriggerProgramResponse'
description: 'Trigger Program on the Specified Runtime.'
summary: 'Triggers program and returns SessionId immediately - does not wait for program completion.'
tags:
- STP
security:
-
bearerAuth: []
parameters:
-
description: 'Location of code in SASjs Drive.'
in: query
name: _program
required: true
schema:
type: string
example: /Projects/myApp/some/program
-
description: 'Optional query param for setting debug mode.'
in: query
name: _debug
required: false
schema:
format: double
type: number
example: 131
-
description: 'Optional query param for setting amount of minutes after the completion of the program when the session must be destroyed.'
in: query
name: expiresAfterMins
required: false
schema:
format: double
type: number
example: 15
/: /:
get: get:
operationId: Home operationId: Home

View File

@@ -234,9 +234,10 @@ const verifyAuthCode = async (
jwt.verify(code, process.secrets.AUTH_CODE_SECRET, (err, data) => { jwt.verify(code, process.secrets.AUTH_CODE_SECRET, (err, data) => {
if (err) return resolve(undefined) if (err) return resolve(undefined)
const payload = data as InfoJWT
const clientInfo: InfoJWT = { const clientInfo: InfoJWT = {
clientId: data?.clientId, clientId: payload?.clientId,
userId: data?.userId userId: payload?.userId
} }
if (clientInfo.clientId === clientId) { if (clientInfo.clientId === clientId) {
return resolve(clientInfo) return resolve(clientInfo)

View File

@@ -1,34 +1,71 @@
import express from 'express' import express from 'express'
import { Request, Security, Route, Tags, Post, Body } from 'tsoa' import { Request, Security, Route, Tags, Post, Body } from 'tsoa'
import { ExecutionController } from './internal' import { ExecutionController, getSessionController } from './internal'
import { import {
getPreProgramVariables, getPreProgramVariables,
getUserAutoExec, getUserAutoExec,
ModeType, ModeType,
parseLogToArray,
RunTimeType RunTimeType
} from '../utils' } from '../utils'
interface ExecuteCodePayload { interface ExecuteCodePayload {
/** /**
* Code of program * The code to be executed
* @example "* Code HERE;" * @example "* Your Code HERE;"
*/ */
code: string code: string
/** /**
* runtime for program * The runtime for the code - eg SAS, JS, PY or R
* @example "js" * @example "js"
*/ */
runTime: RunTimeType runTime: RunTimeType
} }
interface TriggerCodePayload {
/**
* The code to be executed
* @example "* Your Code HERE;"
*/
code: string
/**
* The runtime for the code - eg SAS, JS, PY or R
* @example "sas"
*/
runTime: RunTimeType
/**
* Amount of minutes after the completion of the job when the session must be
* destroyed.
* @example 15
*/
expiresAfterMins?: number
}
interface TriggerCodeResponse {
/**
* `sessionId` is the ID of the session and the name of the temporary folder
* used to store code outputs.<br><br>
* For SAS, this would be the location of the SASWORK folder.<br><br>
* `sessionId` can be used to poll session state using the
* GET /SASjsApi/session/{sessionId}/state endpoint.
* @example "20241028074744-54132-1730101664824"
*/
sessionId: string
}
@Security('bearerAuth') @Security('bearerAuth')
@Route('SASjsApi/code') @Route('SASjsApi/code')
@Tags('Code') @Tags('Code')
export class CodeController { export class CodeController {
/** /**
* Execute Code on the Specified Runtime * Execute Code on the Specified Runtime
* @summary Run Code and Return Webout Content and Log * @summary Run Code and Return Webout Content, Log and Print output
* The order of returned parts of the payload is:
* 1. Webout (if present)
* 2. Logs UUID (used as separator)
* 3. Log
* 4. Logs UUID (used as separator)
* 5. Print (if present and if the runtime is SAS)
* Please see @sasjs/server/api/src/controllers/internal/Execution.ts for more information
*/ */
@Post('/execute') @Post('/execute')
public async executeCode( public async executeCode(
@@ -37,6 +74,18 @@ export class CodeController {
): Promise<string | Buffer> { ): Promise<string | Buffer> {
return executeCode(request, body) return executeCode(request, body)
} }
/**
* Trigger Code on the Specified Runtime
* @summary Triggers code and returns SessionId immediately - does not wait for job completion
*/
@Post('/trigger')
public async triggerCode(
@Request() request: express.Request,
@Body() body: TriggerCodePayload
): Promise<TriggerCodeResponse> {
return triggerCode(request, body)
}
} }
const executeCode = async ( const executeCode = async (
@@ -55,7 +104,8 @@ const executeCode = async (
preProgramVariables: getPreProgramVariables(req), preProgramVariables: getPreProgramVariables(req),
vars: { ...req.query, _debug: 131 }, vars: { ...req.query, _debug: 131 },
otherArgs: { userAutoExec }, otherArgs: { userAutoExec },
runTime: runTime runTime: runTime,
includePrintOutput: true
}) })
return result return result
@@ -68,3 +118,49 @@ const executeCode = async (
} }
} }
} }
const triggerCode = async (
req: express.Request,
{ code, runTime, expiresAfterMins }: TriggerCodePayload
): Promise<TriggerCodeResponse> => {
const { user } = req
const userAutoExec =
process.env.MODE === ModeType.Server
? user?.autoExec
: await getUserAutoExec()
// get session controller based on runTime
const sessionController = getSessionController(runTime)
// get session
const session = await sessionController.getSession()
// add expiresAfterMins to session if provided
if (expiresAfterMins) {
// expiresAfterMins.used is set initially to false
session.expiresAfterMins = { mins: expiresAfterMins, used: false }
}
try {
// call executeProgram method of ExecutionController without awaiting
new ExecutionController().executeProgram({
program: code,
preProgramVariables: getPreProgramVariables(req),
vars: { ...req.query, _debug: 131 },
otherArgs: { userAutoExec },
runTime: runTime,
includePrintOutput: true,
session // session is provided
})
// return session id
return { sessionId: session.id }
} catch (err: any) {
throw {
code: 400,
status: 'failure',
message: 'Job execution failed.',
error: typeof err === 'object' ? err.toString() : err
}
}
}

View File

@@ -2,7 +2,7 @@ import path from 'path'
import fs from 'fs' import fs from 'fs'
import { getSessionController, processProgram } from './' import { getSessionController, processProgram } from './'
import { readFile, fileExists, createFile, readFileBinary } from '@sasjs/utils' import { readFile, fileExists, createFile, readFileBinary } from '@sasjs/utils'
import { PreProgramVars, Session, TreeNode } from '../../types' import { PreProgramVars, Session, TreeNode, SessionState } from '../../types'
import { import {
extractHeaders, extractHeaders,
getFilesFolder, getFilesFolder,
@@ -33,6 +33,7 @@ interface ExecuteFileParams {
interface ExecuteProgramParams extends Omit<ExecuteFileParams, 'programPath'> { interface ExecuteProgramParams extends Omit<ExecuteFileParams, 'programPath'> {
program: string program: string
includePrintOutput?: boolean
} }
export class ExecutionController { export class ExecutionController {
@@ -67,18 +68,17 @@ export class ExecutionController {
otherArgs, otherArgs,
session: sessionByFileUpload, session: sessionByFileUpload,
runTime, runTime,
forceStringResult forceStringResult,
includePrintOutput
}: ExecuteProgramParams): Promise<ExecuteReturnRaw> { }: ExecuteProgramParams): Promise<ExecuteReturnRaw> {
const sessionController = getSessionController(runTime) const sessionController = getSessionController(runTime)
const session = const session =
sessionByFileUpload ?? (await sessionController.getSession()) sessionByFileUpload ?? (await sessionController.getSession())
session.inUse = true session.state = SessionState.running
session.consumed = true
const logPath = path.join(session.path, 'log.log') const logPath = path.join(session.path, 'log.log')
const headersPath = path.join(session.path, 'stpsrv_header.txt') const headersPath = path.join(session.path, 'stpsrv_header.txt')
const weboutPath = path.join(session.path, 'webout.txt') const weboutPath = path.join(session.path, 'webout.txt')
const tokenFile = path.join(session.path, 'reqHeaders.txt') const tokenFile = path.join(session.path, 'reqHeaders.txt')
@@ -120,13 +120,32 @@ export class ExecutionController {
: '' : ''
// it should be deleted by scheduleSessionDestroy // it should be deleted by scheduleSessionDestroy
session.inUse = false session.state = SessionState.completed
const resultParts = []
// INFO: webout can be a Buffer, that is why it's length should be checked to determine if it is empty
if (webout && webout.length !== 0) resultParts.push(webout)
// INFO: log separator wraps the log from the beginning and the end
resultParts.push(process.logsUUID)
resultParts.push(log)
resultParts.push(process.logsUUID)
if (includePrintOutput && runTime === RunTimeType.SAS) {
const printOutputPath = path.join(session.path, 'output.lst')
const printOutput = (await fileExists(printOutputPath))
? await readFile(printOutputPath)
: ''
if (printOutput) resultParts.push(printOutput)
}
return { return {
httpHeaders, httpHeaders,
result: result:
isDebugOn(vars) || session.crashed isDebugOn(vars) || session.failureReason
? `${webout}\n${process.logsUUID}\n${log}` ? resultParts.join(`\n`)
: webout : webout
} }
} }

View File

@@ -2,11 +2,8 @@ import { Request, RequestHandler } from 'express'
import multer from 'multer' import multer from 'multer'
import { uuidv4 } from '@sasjs/utils' import { uuidv4 } from '@sasjs/utils'
import { getSessionController } from '.' import { getSessionController } from '.'
import { import { executeProgramRawValidation, getRunTimeAndFilePath } from '../../utils'
executeProgramRawValidation, import { SessionState } from '../../types'
getRunTimeAndFilePath,
RunTimeType
} from '../../utils'
export class FileUploadController { export class FileUploadController {
private storage = multer.diskStorage({ private storage = multer.diskStorage({
@@ -56,9 +53,8 @@ export class FileUploadController {
} }
const session = await sessionController.getSession() const session = await sessionController.getSession()
// marking consumed true, so that it's not available // change session state to 'running', so that it's not available for any other request
// as readySession for any other request session.state = SessionState.running
session.consumed = true
req.sasjsSession = session req.sasjsSession = session

View File

@@ -1,5 +1,5 @@
import path from 'path' import path from 'path'
import { Session } from '../../types' import { Session, SessionState } from '../../types'
import { promisify } from 'util' import { promisify } from 'util'
import { execFile } from 'child_process' import { execFile } from 'child_process'
import { import {
@@ -14,8 +14,7 @@ import {
createFile, createFile,
fileExists, fileExists,
generateTimestamp, generateTimestamp,
readFile, readFile
isWindows
} from '@sasjs/utils' } from '@sasjs/utils'
const execFilePromise = promisify(execFile) const execFilePromise = promisify(execFile)
@@ -24,7 +23,9 @@ export class SessionController {
protected sessions: Session[] = [] protected sessions: Session[] = []
protected getReadySessions = (): Session[] => protected getReadySessions = (): Session[] =>
this.sessions.filter((sess: Session) => sess.ready && !sess.consumed) this.sessions.filter(
(session: Session) => session.state === SessionState.pending
)
protected async createSession(): Promise<Session> { protected async createSession(): Promise<Session> {
const sessionId = generateUniqueFileName(generateTimestamp()) const sessionId = generateUniqueFileName(generateTimestamp())
@@ -40,19 +41,18 @@ export class SessionController {
const session: Session = { const session: Session = {
id: sessionId, id: sessionId,
ready: true, state: SessionState.pending,
inUse: true,
consumed: false,
completed: false,
creationTimeStamp, creationTimeStamp,
deathTimeStamp, deathTimeStamp,
path: sessionFolder path: sessionFolder
} }
const headersPath = path.join(session.path, 'stpsrv_header.txt') const headersPath = path.join(session.path, 'stpsrv_header.txt')
await createFile(headersPath, 'content-type: text/html; charset=utf-8') await createFile(headersPath, 'content-type: text/html; charset=utf-8')
this.sessions.push(session) this.sessions.push(session)
return session return session
} }
@@ -67,6 +67,10 @@ export class SessionController {
return session return session
} }
public getSessionById(id: string) {
return this.sessions.find((session) => session.id === id)
}
} }
export class SASSessionController extends SessionController { export class SASSessionController extends SessionController {
@@ -84,10 +88,7 @@ export class SASSessionController extends SessionController {
const session: Session = { const session: Session = {
id: sessionId, id: sessionId,
ready: false, state: SessionState.initialising,
inUse: false,
consumed: false,
completed: false,
creationTimeStamp, creationTimeStamp,
deathTimeStamp, deathTimeStamp,
path: sessionFolder path: sessionFolder
@@ -145,13 +146,20 @@ ${autoExecContent}`
process.sasLoc!.endsWith('sas.exe') ? session.path : '' process.sasLoc!.endsWith('sas.exe') ? session.path : ''
]) ])
.then(() => { .then(() => {
session.completed = true session.state = SessionState.completed
process.logger.info('session completed', session) process.logger.info('session completed', session)
}) })
.catch((err) => { .catch((err) => {
session.completed = true session.state = SessionState.failed
session.crashed = err.toString()
process.logger.error('session crashed', session.id, session.crashed) session.failureReason = err.toString()
process.logger.error(
'session crashed',
session.id,
session.failureReason
)
}) })
// we have a triggered session - add to array // we have a triggered session - add to array
@@ -168,15 +176,19 @@ ${autoExecContent}`
const codeFilePath = path.join(session.path, 'code.sas') const codeFilePath = path.join(session.path, 'code.sas')
// TODO: don't wait forever // TODO: don't wait forever
while ((await fileExists(codeFilePath)) && !session.crashed) {} while (
(await fileExists(codeFilePath)) &&
session.state !== SessionState.failed
) {}
if (session.crashed) if (session.state === SessionState.failed) {
process.logger.error( process.logger.error(
'session crashed! while waiting to be ready', 'session crashed! while waiting to be ready',
session.crashed session.failureReason
) )
} else {
session.ready = true session.state = SessionState.pending
}
} }
private async deleteSession(session: Session) { private async deleteSession(session: Session) {
@@ -190,17 +202,37 @@ ${autoExecContent}`
} }
private scheduleSessionDestroy(session: Session) { private scheduleSessionDestroy(session: Session) {
setTimeout(async () => { setTimeout(
if (session.inUse) { async () => {
// adding 10 more minutes if (session.state === SessionState.running) {
const newDeathTimeStamp = parseInt(session.deathTimeStamp) + 10 * 1000 // adding 10 more minutes
session.deathTimeStamp = newDeathTimeStamp.toString() const newDeathTimeStamp =
parseInt(session.deathTimeStamp) + 10 * 60 * 1000
session.deathTimeStamp = newDeathTimeStamp.toString()
this.scheduleSessionDestroy(session) this.scheduleSessionDestroy(session)
} else { } else {
await this.deleteSession(session) const { expiresAfterMins } = session
}
}, parseInt(session.deathTimeStamp) - new Date().getTime() - 100) // delay session destroy if expiresAfterMins present
if (expiresAfterMins && session.state !== SessionState.completed) {
// calculate session death time using expiresAfterMins
const newDeathTimeStamp =
parseInt(session.deathTimeStamp) +
expiresAfterMins.mins * 60 * 1000
session.deathTimeStamp = newDeathTimeStamp.toString()
// set expiresAfterMins to true to avoid using it again
session.expiresAfterMins!.used = true
this.scheduleSessionDestroy(session)
} else {
await this.deleteSession(session)
}
}
},
parseInt(session.deathTimeStamp) - new Date().getTime() - 100
)
} }
} }
@@ -228,9 +260,16 @@ data _null_;
rc=filename(fname,getoption('SYSIN') ); rc=filename(fname,getoption('SYSIN') );
if rc = 0 and fexist(fname) then rc=fdelete(fname); if rc = 0 and fexist(fname) then rc=fdelete(fname);
rc=filename(fname); rc=filename(fname);
/* now wait for the real SYSIN */ /* now wait for the real SYSIN (location of code.sas) */
slept=0; slept=0;fname='';
do until ( fileexist(getoption('SYSIN')) or slept>(60*15) ); do until (slept>(60*15));
rc=filename(fname,getoption('SYSIN'));
if rc = 0 and fexist(fname) then do;
putlog fname=;
rc=filename(fname);
rc=sleep(0.01,1); /* wait just a little more */
stop;
end;
slept=slept+sleep(0.01,1); slept=slept+sleep(0.01,1);
end; end;
stop; stop;

View File

@@ -15,7 +15,7 @@ export const createJSProgram = async (
) => { ) => {
const varStatments = Object.keys(vars).reduce( const varStatments = Object.keys(vars).reduce(
(computed: string, key: string) => (computed: string, key: string) =>
`${computed}const ${key} = '${vars[key]}';\n`, `${computed}const ${key} = \`${vars[key]}\`;\n`,
'' ''
) )

View File

@@ -3,7 +3,7 @@ import { WriteStream, createWriteStream } from 'fs'
import { execFile } from 'child_process' import { execFile } from 'child_process'
import { once } from 'stream' import { once } from 'stream'
import { createFile, moveFile } from '@sasjs/utils' import { createFile, moveFile } from '@sasjs/utils'
import { PreProgramVars, Session } from '../../types' import { PreProgramVars, Session, SessionState } from '../../types'
import { RunTimeType } from '../../utils' import { RunTimeType } from '../../utils'
import { import {
ExecutionVars, ExecutionVars,
@@ -49,7 +49,7 @@ export const processProgram = async (
await moveFile(codePath + '.bkp', codePath) await moveFile(codePath + '.bkp', codePath)
// we now need to poll the session status // we now need to poll the session status
while (!session.completed) { while (session.state !== SessionState.completed) {
await delay(50) await delay(50)
} }
} else { } else {
@@ -114,13 +114,20 @@ export const processProgram = async (
await execFilePromise(executablePath, [codePath], writeStream) await execFilePromise(executablePath, [codePath], writeStream)
.then(() => { .then(() => {
session.completed = true session.state = SessionState.completed
process.logger.info('session completed', session) process.logger.info('session completed', session)
}) })
.catch((err) => { .catch((err) => {
session.completed = true session.state = SessionState.failed
session.crashed = err.toString()
process.logger.error('session crashed', session.id, session.crashed) session.failureReason = err.toString()
process.logger.error(
'session crashed',
session.id,
session.failureReason
)
}) })
// copy the code file to log and end write stream // copy the code file to log and end write stream

View File

@@ -1,6 +1,8 @@
import express from 'express' import express from 'express'
import { Request, Security, Route, Tags, Example, Get } from 'tsoa' import { Request, Security, Route, Tags, Example, Get } from 'tsoa'
import { UserResponse } from './user' import { UserResponse } from './user'
import { getSessionController } from './internal'
import { SessionState } from '../types'
interface SessionResponse extends UserResponse { interface SessionResponse extends UserResponse {
needsToUpdatePassword: boolean needsToUpdatePassword: boolean
@@ -26,6 +28,18 @@ export class SessionController {
): Promise<SessionResponse> { ): Promise<SessionResponse> {
return session(request) return session(request)
} }
/**
* The polling endpoint is currently implemented for single-server deployments only.<br>
* Load balanced / grid topologies will be supported in a future release.<br>
* If your site requires this, please reach out to SASjs Support.
* @summary Get session state (initialising, pending, running, completed, failed).
* @example completed
*/
@Get('/:sessionId/state')
public async sessionState(sessionId: string): Promise<SessionState> {
return sessionState(sessionId)
}
} }
const session = (req: express.Request) => ({ const session = (req: express.Request) => ({
@@ -35,3 +49,23 @@ const session = (req: express.Request) => ({
isAdmin: req.user!.isAdmin, isAdmin: req.user!.isAdmin,
needsToUpdatePassword: req.user!.needsToUpdatePassword needsToUpdatePassword: req.user!.needsToUpdatePassword
}) })
const sessionState = (sessionId: string): SessionState => {
for (let runTime of process.runTimes) {
// get session controller for each available runTime
const sessionController = getSessionController(runTime)
// get session by sessionId
const session = sessionController.getSessionById(sessionId)
// return session state if session was found
if (session) {
return session.state
}
}
throw {
code: 404,
message: `Session with ID '${sessionId}' was not found.`
}
}

View File

@@ -1,10 +1,12 @@
import express from 'express' import express from 'express'
import { Request, Security, Route, Tags, Post, Body, Get, Query } from 'tsoa' import { Request, Security, Route, Tags, Post, Body, Get, Query } from 'tsoa'
import { ExecutionController, ExecutionVars } from './internal' import {
ExecutionController,
ExecutionVars,
getSessionController
} from './internal'
import { import {
getPreProgramVariables, getPreProgramVariables,
HTTPHeaders,
LogLine,
makeFilesNamesMap, makeFilesNamesMap,
getRunTimeAndFilePath getRunTimeAndFilePath
} from '../utils' } from '../utils'
@@ -18,6 +20,36 @@ interface ExecutePostRequestPayload {
_program?: string _program?: string
} }
interface TriggerProgramPayload {
/**
* Location of SAS program.
* @example "/Public/somefolder/some.file"
*/
_program: string
/**
* Amount of minutes after the completion of the program when the session must be
* destroyed.
* @example 15
*/
expiresAfterMins?: number
/**
* Query param for setting debug mode.
*/
_debug?: number
}
interface TriggerProgramResponse {
/**
* `sessionId` is the ID of the session and the name of the temporary folder
* used to store program outputs.<br><br>
* For SAS, this would be the location of the SASWORK folder.<br><br>
* `sessionId` can be used to poll session state using the
* GET /SASjsApi/session/{sessionId}/state endpoint.
* @example "20241028074744-54132-1730101664824"
*/
sessionId: string
}
@Security('bearerAuth') @Security('bearerAuth')
@Route('SASjsApi/stp') @Route('SASjsApi/stp')
@Tags('STP') @Tags('STP')
@@ -25,20 +57,31 @@ export class STPController {
/** /**
* Trigger a Stored Program using the _program URL parameter. * Trigger a Stored Program using the _program URL parameter.
* *
* Accepts URL parameters and file uploads. For more details, see docs: * Accepts additional URL parameters (converted to session variables)
* and file uploads. For more details, see docs:
* *
* https://server.sasjs.io/storedprograms * https://server.sasjs.io/storedprograms
* *
* @summary Execute a Stored Program, returns _webout and (optionally) log. * @summary Execute a Stored Program, returns _webout and (optionally) log.
* @param _program Location of code in SASjs Drive * @param _program Location of Stored Program in SASjs Drive.
* @param _debug Optional query param for setting debug mode (returns the session log in the response body).
* @example _program "/Projects/myApp/some/program" * @example _program "/Projects/myApp/some/program"
* @example _debug 131
*/ */
@Get('/execute') @Get('/execute')
public async executeGetRequest( public async executeGetRequest(
@Request() request: express.Request, @Request() request: express.Request,
@Query() _program: string @Query() _program: string,
@Query() _debug?: number
): Promise<string | Buffer> { ): Promise<string | Buffer> {
const vars = request.query as ExecutionVars let vars = request.query as ExecutionVars
if (_debug) {
vars = {
...vars,
_debug
}
}
return execute(request, _program, vars) return execute(request, _program, vars)
} }
@@ -69,6 +112,26 @@ export class STPController {
return execute(request, program!, vars, otherArgs) return execute(request, program!, vars, otherArgs)
} }
/**
* Trigger Program on the Specified Runtime.
* @summary Triggers program and returns SessionId immediately - does not wait for program completion.
* @param _program Location of code in SASjs Drive.
* @param expiresAfterMins Optional query param for setting amount of minutes after the completion of the program when the session must be destroyed.
* @param _debug Optional query param for setting debug mode.
* @example _program "/Projects/myApp/some/program"
* @example _debug 131
* @example expiresAfterMins 15
*/
@Post('/trigger')
public async triggerProgram(
@Request() request: express.Request,
@Query() _program: string,
@Query() _debug?: number,
@Query() expiresAfterMins?: number
): Promise<TriggerProgramResponse> {
return triggerProgram(request, { _program, _debug, expiresAfterMins })
}
} }
const execute = async ( const execute = async (
@@ -107,3 +170,52 @@ const execute = async (
} }
} }
} }
const triggerProgram = async (
req: express.Request,
{ _program, _debug, expiresAfterMins }: TriggerProgramPayload
): Promise<TriggerProgramResponse> => {
try {
// put _program query param into vars object
const vars: { [key: string]: string | number } = { _program }
// if present add _debug query param to vars object
if (_debug) {
vars._debug = _debug
}
// get code path and runTime
const { codePath, runTime } = await getRunTimeAndFilePath(_program)
// get session controller based on runTime
const sessionController = getSessionController(runTime)
// get session
const session = await sessionController.getSession()
// add expiresAfterMins to session if provided
if (expiresAfterMins) {
// expiresAfterMins.used is set initially to false
session.expiresAfterMins = { mins: expiresAfterMins, used: false }
}
// call executeFile method of ExecutionController without awaiting
new ExecutionController().executeFile({
programPath: codePath,
runTime,
preProgramVariables: getPreProgramVariables(req),
vars,
session
})
// return session id
return { sessionId: session.id }
} catch (err: any) {
throw {
code: 400,
status: 'failure',
message: 'Job execution failed.',
error: typeof err === 'object' ? err.toString() : err
}
}
}

View File

@@ -285,7 +285,7 @@ const getUser = async (
username: user.username, username: user.username,
isActive: user.isActive, isActive: user.isActive,
isAdmin: user.isAdmin, isAdmin: user.isAdmin,
autoExec: getAutoExec ? user.autoExec ?? '' : undefined, autoExec: getAutoExec ? (user.autoExec ?? '') : undefined,
groups: user.groups groups: user.groups
} }
} }

View File

@@ -106,7 +106,10 @@ const login = async (
const rateLimiter = RateLimiter.getInstance() const rateLimiter = RateLimiter.getInstance()
if (!validPass) { if (!validPass) {
const retrySecs = await rateLimiter.consume(req.ip, user?.username) const retrySecs = await rateLimiter.consume(
req.ip || 'unknown',
user?.username
)
if (retrySecs > 0) throw errors.tooManyRequests(retrySecs) if (retrySecs > 0) throw errors.tooManyRequests(retrySecs)
} }
@@ -114,7 +117,7 @@ const login = async (
if (!validPass) throw errors.invalidPassword if (!validPass) throw errors.invalidPassword
// Reset on successful authorization // Reset on successful authorization
rateLimiter.resetOnSuccess(req.ip, user.username) rateLimiter.resetOnSuccess(req.ip || 'unknown', user.username)
req.session.loggedIn = true req.session.loggedIn = true
req.session.user = { req.session.user = {

View File

@@ -37,10 +37,10 @@ export const authenticateAccessToken: RequestHandler = async (
if (user.isActive) { if (user.isActive) {
req.user = user req.user = user
return csrfProtection(req, res, nextFunction) return csrfProtection(req, res, nextFunction)
} else return res.sendStatus(401) } else return res.status(401).send('Unauthorized')
} }
} }
return res.sendStatus(401) return res.status(401).send('Unauthorized')
} }
await authenticateToken( await authenticateToken(
@@ -118,6 +118,6 @@ const authenticateToken = async (
return next() return next()
} }
res.sendStatus(401) res.status(401).send('Unauthorized')
} }
} }

View File

@@ -3,7 +3,7 @@ import { convertSecondsToHms } from '@sasjs/utils'
import { RateLimiter } from '../utils' import { RateLimiter } from '../utils'
export const bruteForceProtection: RequestHandler = async (req, res, next) => { export const bruteForceProtection: RequestHandler = async (req, res, next) => {
const ip = req.ip const ip = req.ip || 'unknown'
const username = req.body.username const username = req.body.username
const rateLimiter = RateLimiter.getInstance() const rateLimiter = RateLimiter.getInstance()

View File

@@ -76,7 +76,7 @@ groupSchema.post('save', function (group: IGroup, next: Function) {
}) })
// pre remove hook to remove all references of group from users // pre remove hook to remove all references of group from users
groupSchema.pre('remove', async function () { groupSchema.pre('remove', async function (this: IGroupDocument) {
const userIds = this.users const userIds = this.users
await Promise.all( await Promise.all(
userIds.map(async (userId) => { userIds.map(async (userId) => {

View File

@@ -1,5 +1,5 @@
import express from 'express' import express from 'express'
import { runCodeValidation } from '../../utils' import { runCodeValidation, triggerCodeValidation } from '../../utils'
import { CodeController } from '../../controllers/' import { CodeController } from '../../controllers/'
const runRouter = express.Router() const runRouter = express.Router()
@@ -28,4 +28,22 @@ runRouter.post('/execute', async (req, res) => {
} }
}) })
runRouter.post('/trigger', async (req, res) => {
const { error, value: body } = triggerCodeValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)
try {
const response = await controller.triggerCode(req, body)
res.status(200)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err)
}
})
export default runRouter export default runRouter

View File

@@ -1,16 +1,37 @@
import express from 'express' import express from 'express'
import { SessionController } from '../../controllers' import { SessionController } from '../../controllers'
import { sessionIdValidation } from '../../utils'
const sessionRouter = express.Router() const sessionRouter = express.Router()
const controller = new SessionController()
sessionRouter.get('/', async (req, res) => { sessionRouter.get('/', async (req, res) => {
const controller = new SessionController()
try { try {
const response = await controller.session(req) const response = await controller.session(req)
res.send(response) res.send(response)
} catch (err: any) { } catch (err: any) {
res.status(403).send(err.toString()) res.status(403).send(err.toString())
} }
}) })
sessionRouter.get('/:sessionId/state', async (req, res) => {
const { error, value: params } = sessionIdValidation(req.params)
if (error) return res.status(400).send(error.details[0].message)
try {
const response = await controller.sessionState(params.sessionId)
res.status(200)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err)
}
})
export default sessionRouter export default sessionRouter

View File

@@ -25,7 +25,7 @@ import {
SASSessionController SASSessionController
} from '../../../controllers/internal' } from '../../../controllers/internal'
import * as ProcessProgramModule from '../../../controllers/internal/processProgram' import * as ProcessProgramModule from '../../../controllers/internal/processProgram'
import { Session } from '../../../types' import { Session, SessionState } from '../../../types'
const clientId = 'someclientID' const clientId = 'someclientID'
@@ -493,10 +493,7 @@ const mockedGetSession = async () => {
const session: Session = { const session: Session = {
id: sessionId, id: sessionId,
ready: true, state: SessionState.pending,
inUse: true,
consumed: false,
completed: false,
creationTimeStamp, creationTimeStamp,
deathTimeStamp, deathTimeStamp,
path: sessionFolder path: sessionFolder

View File

@@ -277,7 +277,10 @@ const performLogin = async (
.set('x-xsrf-token', csrfToken) .set('x-xsrf-token', csrfToken)
.send(credentials) .send(credentials)
return { authCookies: header['set-cookie'].join() } return {
authCookies:
(header['set-cookie'] as unknown as string[] | undefined)?.join() || ''
}
} }
const extractCSRF = (text: string) => const extractCSRF = (text: string) =>

View File

@@ -1,5 +1,8 @@
import express from 'express' import express from 'express'
import { executeProgramRawValidation } from '../../utils' import {
executeProgramRawValidation,
triggerProgramValidation
} from '../../utils'
import { STPController } from '../../controllers/' import { STPController } from '../../controllers/'
import { FileUploadController } from '../../controllers/internal' import { FileUploadController } from '../../controllers/internal'
@@ -13,7 +16,11 @@ stpRouter.get('/execute', async (req, res) => {
if (error) return res.status(400).send(error.details[0].message) if (error) return res.status(400).send(error.details[0].message)
try { try {
const response = await controller.executeGetRequest(req, query._program) const response = await controller.executeGetRequest(
req,
query._program,
query._debug
)
if (response instanceof Buffer) { if (response instanceof Buffer) {
res.writeHead(200, (req as any).sasHeaders) res.writeHead(200, (req as any).sasHeaders)
@@ -65,4 +72,28 @@ stpRouter.post(
} }
) )
stpRouter.post('/trigger', async (req, res) => {
const { error, value: query } = triggerProgramValidation(req.query)
if (error) return res.status(400).send(error.details[0].message)
try {
const response = await controller.triggerProgram(
req,
query._program,
query._debug,
query.expiresAfterMins
)
res.status(200)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err)
}
})
export default stpRouter export default stpRouter

View File

@@ -1,11 +1,16 @@
export enum SessionState {
initialising = 'initialising', // session is initialising and not ready to be used yet
pending = 'pending', // session is ready to be used
running = 'running', // session is in use
completed = 'completed', // session is completed and can be destroyed
failed = 'failed' // session failed
}
export interface Session { export interface Session {
id: string id: string
ready: boolean state: SessionState
creationTimeStamp: string creationTimeStamp: string
deathTimeStamp: string deathTimeStamp: string
path: string path: string
inUse: boolean expiresAfterMins?: { mins: number; used: boolean }
consumed: boolean failureReason?: string
completed: boolean
crashed?: string
} }

View File

@@ -1,5 +1,6 @@
import jwt from 'jsonwebtoken' import jwt from 'jsonwebtoken'
import User from '../model/User' import User from '../model/User'
import { InfoJWT } from '../types/InfoJWT'
const isValidToken = async ( const isValidToken = async (
token: string, token: string,
@@ -11,7 +12,8 @@ const isValidToken = async (
jwt.verify(token, key, (err, decoded) => { jwt.verify(token, key, (err, decoded) => {
if (err) return reject(false) if (err) return reject(false)
if (decoded?.userId === userId && decoded?.clientId === clientId) { const payload = decoded as InfoJWT
if (payload?.userId === userId && payload?.clientId === clientId) {
return resolve(true) return resolve(true)
} }

View File

@@ -1,9 +1,31 @@
import path from 'path' import path from 'path'
import { createFolder, getAbsolutePath, getRealPath } from '@sasjs/utils' import {
createFolder,
getAbsolutePath,
getRealPath,
fileExists
} from '@sasjs/utils'
import dotenv from 'dotenv'
import { connectDB, getDesktopFields, ModeType, RunTimeType, SECRETS } from '.' import { connectDB, getDesktopFields, ModeType, RunTimeType, SECRETS } from '.'
export const setProcessVariables = async () => { export const setProcessVariables = async () => {
const { execPath } = process
// Check if execPath ends with 'api-macos' to determine executable for MacOS.
// This is needed to fix picking .env file issue in MacOS executable.
if (execPath) {
const envPathSplitted = execPath.split(path.sep)
if (envPathSplitted.pop() === 'api-macos') {
const envPath = path.join(envPathSplitted.join(path.sep), '.env')
// Override environment variables from envPath if file exists
if (await fileExists(envPath)) {
dotenv.config({ path: envPath, override: true })
}
}
}
const { MODE, RUN_TIMES } = process.env const { MODE, RUN_TIMES } = process.env
if (MODE === ModeType.Server) { if (MODE === ModeType.Server) {
@@ -21,6 +43,7 @@ export const setProcessVariables = async () => {
if (process.env.NODE_ENV === 'test') { if (process.env.NODE_ENV === 'test') {
process.sasjsRoot = path.join(process.cwd(), 'sasjs_root') process.sasjsRoot = path.join(process.cwd(), 'sasjs_root')
process.driveLoc = path.join(process.cwd(), 'sasjs_root', 'drive') process.driveLoc = path.join(process.cwd(), 'sasjs_root', 'drive')
return return
} }
@@ -41,7 +64,9 @@ export const setProcessVariables = async () => {
const { SASJS_ROOT } = process.env const { SASJS_ROOT } = process.env
const absPath = getAbsolutePath(SASJS_ROOT ?? 'sasjs_root', process.cwd()) const absPath = getAbsolutePath(SASJS_ROOT ?? 'sasjs_root', process.cwd())
await createFolder(absPath) await createFolder(absPath)
process.sasjsRoot = getRealPath(absPath) process.sasjsRoot = getRealPath(absPath)
const { DRIVE_LOCATION } = process.env const { DRIVE_LOCATION } = process.env
@@ -49,6 +74,7 @@ export const setProcessVariables = async () => {
DRIVE_LOCATION ?? path.join(process.sasjsRoot, 'drive'), DRIVE_LOCATION ?? path.join(process.sasjsRoot, 'drive'),
process.cwd() process.cwd()
) )
await createFolder(absDrivePath) await createFolder(absDrivePath)
process.driveLoc = getRealPath(absDrivePath) process.driveLoc = getRealPath(absDrivePath)
@@ -57,7 +83,9 @@ export const setProcessVariables = async () => {
LOG_LOCATION ?? path.join(process.sasjsRoot, 'logs'), LOG_LOCATION ?? path.join(process.sasjsRoot, 'logs'),
process.cwd() process.cwd()
) )
await createFolder(absLogsPath) await createFolder(absLogsPath)
process.logsLoc = getRealPath(absLogsPath) process.logsLoc = getRealPath(absLogsPath)
process.logsUUID = 'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784' process.logsUUID = 'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784'

View File

@@ -51,9 +51,8 @@ export const generateFileUploadSasCode = async (
let fileCount = 0 let fileCount = 0
const uploadedFiles: UploadedFiles[] = [] const uploadedFiles: UploadedFiles[] = []
const sasSessionFolderList: string[] = await listFilesInFolder( const sasSessionFolderList: string[] =
sasSessionFolder await listFilesInFolder(sasSessionFolder)
)
sasSessionFolderList.forEach((fileName) => { sasSessionFolderList.forEach((fileName) => {
let fileCountString = fileCount < 100 ? '0' + fileCount : fileCount let fileCountString = fileCount < 100 ? '0' + fileCount : fileCount
fileCountString = fileCount < 10 ? '00' + fileCount : fileCount fileCountString = fileCount < 10 ? '00' + fileCount : fileCount

View File

@@ -178,9 +178,31 @@ export const runCodeValidation = (data: any): Joi.ValidationResult =>
runTime: Joi.string().valid(...process.runTimes) runTime: Joi.string().valid(...process.runTimes)
}).validate(data) }).validate(data)
export const triggerCodeValidation = (data: any): Joi.ValidationResult =>
Joi.object({
code: Joi.string().required(),
runTime: Joi.string().valid(...process.runTimes),
expiresAfterMins: Joi.number().greater(0)
}).validate(data)
export const executeProgramRawValidation = (data: any): Joi.ValidationResult => export const executeProgramRawValidation = (data: any): Joi.ValidationResult =>
Joi.object({ Joi.object({
_program: Joi.string().required() _program: Joi.string().required(),
_debug: Joi.number()
}) })
.pattern(/^/, Joi.alternatives(Joi.string(), Joi.number())) .pattern(/^/, Joi.alternatives(Joi.string(), Joi.number()))
.validate(data) .validate(data)
export const triggerProgramValidation = (data: any): Joi.ValidationResult =>
Joi.object({
_program: Joi.string().required(),
_debug: Joi.number(),
expiresAfterMins: Joi.number().greater(0)
})
.pattern(/^/, Joi.alternatives(Joi.string(), Joi.number()))
.validate(data)
export const sessionIdValidation = (data: any): Joi.ValidationResult =>
Joi.object({
sessionId: Joi.string().required()
}).validate(data)

12864
package-lock.json generated

File diff suppressed because it is too large Load Diff

1263
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -19,9 +19,8 @@
"@types/jest": "^26.0.24", "@types/jest": "^26.0.24",
"@types/node": "^12.20.28", "@types/node": "^12.20.28",
"@types/react": "^17.0.27", "@types/react": "^17.0.27",
"axios": "^0.24.0", "axios": "^1.12.2",
"monaco-editor": "^0.33.0", "monaco-editor": "^0.33.0",
"monaco-editor-webpack-plugin": "^7.0.1",
"react": "^17.0.2", "react": "^17.0.2",
"react-copy-to-clipboard": "^5.1.0", "react-copy-to-clipboard": "^5.1.0",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
@@ -54,6 +53,7 @@
"eslint-webpack-plugin": "^3.1.1", "eslint-webpack-plugin": "^3.1.1",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"html-webpack-plugin": "5.5.0", "html-webpack-plugin": "5.5.0",
"monaco-editor-webpack-plugin": "^7.0.1",
"path": "0.12.7", "path": "0.12.7",
"prettier": "^2.4.1", "prettier": "^2.4.1",
"sass": "^1.44.0", "sass": "^1.44.0",

View File

@@ -3,12 +3,11 @@ import Snackbar from '@mui/material/Snackbar'
import MuiAlert, { AlertProps } from '@mui/material/Alert' import MuiAlert, { AlertProps } from '@mui/material/Alert'
import Slide, { SlideProps } from '@mui/material/Slide' import Slide, { SlideProps } from '@mui/material/Slide'
const Alert = React.forwardRef<HTMLDivElement, AlertProps>(function Alert( const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
props, function Alert(props, ref) {
ref return <MuiAlert elevation={6} ref={ref} variant="filled" {...props} />
) { }
return <MuiAlert elevation={6} ref={ref} variant="filled" {...props} /> )
})
const Transition = (props: SlideProps) => { const Transition = (props: SlideProps) => {
return <Slide {...props} direction="up" /> return <Slide {...props} direction="up" />

View File

@@ -62,6 +62,7 @@ const SASjsEditor = ({
selectedRunTime, selectedRunTime,
showDiff, showDiff,
webout, webout,
printOutput,
Dialog, Dialog,
handleChangeRunTime, handleChangeRunTime,
handleDiffEditorDidMount, handleDiffEditorDidMount,
@@ -153,30 +154,35 @@ const SASjsEditor = ({
> >
<TabList onChange={handleTabChange} centered> <TabList onChange={handleTabChange} centered>
<StyledTab label="Code" value="code" /> <StyledTab label="Code" value="code" />
<StyledTab {log && (
label={logWithErrorsOrWarnings ? '' : 'log'} <StyledTab
value="log" label={logWithErrorsOrWarnings ? '' : 'log'}
icon={ value="log"
logWithErrorsOrWarnings ? ( icon={
<LogTabWithIcons log={log as LogObject} /> logWithErrorsOrWarnings ? (
) : ( <LogTabWithIcons log={log as LogObject} />
'' ) : (
) ''
} )
onClick={() => { }
const logWrapper = document.querySelector(`#logWrapper`) onClick={() => {
const logWrapper = document.querySelector(`#logWrapper`)
if (logWrapper) logWrapper.scrollTop = 0 if (logWrapper) logWrapper.scrollTop = 0
}} }}
/> />
<StyledTab )}
label={ {webout && (
<Tooltip title="Displays content from the _webout fileref"> <StyledTab
<Typography>Webout</Typography> label={
</Tooltip> <Tooltip title="Displays content from the _webout fileref">
} <Typography>Webout</Typography>
value="webout" </Tooltip>
/> }
value="webout"
/>
)}
{printOutput && <StyledTab label="print" value="printOutput" />}
</TabList> </TabList>
</Box> </Box>
@@ -222,11 +228,20 @@ const SASjsEditor = ({
<LogComponent log={log} selectedRunTime={selectedRunTime} /> <LogComponent log={log} selectedRunTime={selectedRunTime} />
)} )}
</StyledTabPanel> </StyledTabPanel>
<StyledTabPanel value="webout"> {webout && (
<div> <StyledTabPanel value="webout">
<pre>{webout}</pre> <div>
</div> <pre>{webout}</pre>
</StyledTabPanel> </div>
</StyledTabPanel>
)}
{printOutput && (
<StyledTabPanel value="printOutput">
<div>
<pre>{printOutput}</pre>
</div>
</StyledTabPanel>
)}
</TabContext> </TabContext>
)} )}
<Dialog /> <Dialog />

View File

@@ -7,8 +7,10 @@
border: none; border: none;
outline: none; outline: none;
transition: 0.4s; transition: 0.4s;
box-shadow: rgba(0, 0, 0, 0.2) 0px 2px 1px -1px, box-shadow:
rgba(0, 0, 0, 0.14) 0px 1px 1px 0px, rgba(0, 0, 0, 0.12) 0px 1px 3px 0px; rgba(0, 0, 0, 0.2) 0px 2px 1px -1px,
rgba(0, 0, 0, 0.14) 0px 1px 1px 0px,
rgba(0, 0, 0, 0.12) 0px 1px 3px 0px;
} }
.ChunkDetails { .ChunkDetails {

View File

@@ -39,14 +39,14 @@ const useEditor = ({
const { Snackbar, setOpenSnackbar, setSnackbarMessage, setSnackbarSeverity } = const { Snackbar, setOpenSnackbar, setSnackbarMessage, setSnackbarSeverity } =
useSnackbar() useSnackbar()
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [prevFileContent, setPrevFileContent] = useStateWithCallback('') const [prevFileContent, setPrevFileContent] = useStateWithCallback('')
const [fileContent, setFileContent] = useState('') const [fileContent, setFileContent] = useState('')
const [log, setLog] = useState<LogObject | string>() const [log, setLog] = useState<LogObject | string>()
const [webout, setWebout] = useState('') const [webout, setWebout] = useState<string>()
const [printOutput, setPrintOutput] = useState<string>()
const [runTimes, setRunTimes] = useState<string[]>([]) const [runTimes, setRunTimes] = useState<string[]>([])
const [selectedRunTime, setSelectedRunTime] = useState<RunTimeType | string>( const [selectedRunTime, setSelectedRunTime] = useState<RunTimeType>(
'' RunTimeType.SAS
) )
const [selectedFileExtension, setSelectedFileExtension] = useState('') const [selectedFileExtension, setSelectedFileExtension] = useState('')
const [openFilePathInputModal, setOpenFilePathInputModal] = useState(false) const [openFilePathInputModal, setOpenFilePathInputModal] = useState(false)
@@ -169,25 +169,30 @@ const useEditor = ({
), ),
runTime: selectedRunTime runTime: selectedRunTime
}) })
.then((res: any) => { .then((res: { data: string }) => {
if (selectedRunTime === RunTimeType.SAS) { // INFO: the order of payload parts is set in @sasjs/server/api/src/controllers/internal/Execution.ts
const { errors, warnings, logLines } = parseErrorsAndWarnings( const resDataSplitted = res.data.split(SASJS_LOGS_SEPARATOR)
res.data.split(SASJS_LOGS_SEPARATOR)[1] const webout = resDataSplitted[0]
) const log = resDataSplitted[1]
const printOutput = resDataSplitted[2]
const log: LogObject = { if (selectedRunTime === RunTimeType.SAS) {
const { errors, warnings, logLines } = parseErrorsAndWarnings(log)
const logObject: LogObject = {
body: logLines.join(`\n`), body: logLines.join(`\n`),
errors, errors,
warnings, warnings,
linesCount: logLines.length linesCount: logLines.length
} }
setLog(log) setLog(logObject)
} else { } else {
setLog(res.data.split(SASJS_LOGS_SEPARATOR)[1] ?? '') setLog(log)
} }
setWebout(res.data.split(SASJS_LOGS_SEPARATOR)[0] ?? '') setWebout(webout)
setPrintOutput(printOutput)
setTab('log') setTab('log')
// Scroll to bottom of log // Scroll to bottom of log
@@ -335,6 +340,7 @@ const useEditor = ({
selectedRunTime, selectedRunTime,
showDiff, showDiff,
webout, webout,
printOutput,
Dialog, Dialog,
handleChangeRunTime, handleChangeRunTime,
handleDiffEditorDidMount, handleDiffEditorDidMount,

View File

@@ -1,15 +1,15 @@
body { body {
margin: 0; margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', font-family:
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',
sans-serif; 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
code { code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', font-family:
monospace; source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
} }
.container { .container {

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />