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

Compare commits

...

64 Commits

Author SHA1 Message Date
semantic-release-bot
620eddb713 chore(release): 0.7.2 [skip ci]
## [0.7.2](https://github.com/sasjs/server/compare/v0.7.1...v0.7.2) (2022-06-20)

### Bug Fixes

* removing UTF-8 options from commandline.  There appears to be no reliable way to enforce ([f6dc74f](f6dc74f16b))
2022-06-20 15:03:27 +00:00
Allan Bowe
3c92034da3 Merge pull request #206 from sasjs/allanbowe/sasjs-server-fails-to-205
fix: removing UTF-8 options from commandline.
2022-06-20 16:57:58 +02:00
Allan Bowe
f6dc74f16b fix: removing UTF-8 options from commandline. There appears to be no reliable way to enforce
UTF-8 without additional modifications to the PATH variable to ensure a DBCS instance of SAS.
2022-06-20 14:38:19 +00:00
semantic-release-bot
8c48d00d21 chore(release): 0.7.1 [skip ci]
## [0.7.1](https://github.com/sasjs/server/compare/v0.7.0...v0.7.1) (2022-06-20)

### Bug Fixes

* default runtime should be sas ([91d29cb](91d29cb127))
* **Studio:** default selection of runtime fixed ([eb569c7](eb569c7b82))
* webout path fixed in code.js when running on windows ([99a1107](99a1107364))
2022-06-20 12:44:59 +00:00
Allan Bowe
48ff8d73d4 Merge pull request #204 from sasjs/fix-runtime-feature
fix: webout path in code.js fixed when running on windows
2022-06-20 14:40:55 +02:00
eb397b15c2 chore: lint fixes 2022-06-20 17:32:28 +05:00
eb569c7b82 fix(Studio): default selection of runtime fixed 2022-06-20 17:29:19 +05:00
99a1107364 fix: webout path fixed in code.js when running on windows 2022-06-20 17:28:25 +05:00
91d29cb127 fix: default runtime should be sas 2022-06-20 17:12:32 +05:00
semantic-release-bot
1e2c08a8d3 chore(release): 0.7.0 [skip ci]
# [0.7.0](https://github.com/sasjs/server/compare/v0.6.1...v0.7.0) (2022-06-19)

### Bug Fixes

* add runtimes to global process object ([194eaec](194eaec7d4))
* code fixes for executing program from program path including file extension ([53854d0](53854d0012))
* code/execute controller logic to handle different runtimes ([23b6692](23b6692f02))
* convert single executeProgram method to two methods i.e. executeSASProgram and executeJSProgram ([c58666e](c58666eb81))
* no need to stringify _webout in preProgramVarStatements, developer should have _webout as string in actual code ([9d5a5e0](9d5a5e051f))
* pass _program to execute file without extension ([5df619b](5df619b3f6))
* refactor code for session selection in preUploadMiddleware function ([b444381](b4443819d4))
* refactor code in executeFile method of session controller ([dffe6d7](dffe6d7121))
* refactor code in preUploadMiddleware function ([6d6bda5](6d6bda5626))
* refactor sas/js session controller classes to inherit from base session controller class ([2c704a5](2c704a544f))
* **Studio:** style fix for runtime dropdown ([9023cf3](9023cf33b5))

### Features

* configure child process with writeStream to write logs to log file ([058b3b0](058b3b0081))
* conver single session controller to two controller i.e. SASSessionController and JSSessionController ([07295aa](07295aa151))
* create and inject code for uploaded files to code.js ([1685616](16856165fb))
* validate sasjs_runtimes env var ([596ada7](596ada7ca8))
2022-06-19 13:44:02 +00:00
Saad Jutt
473fbd62c0 Merge pull request #186 from sasjs/issue-184
feat: Support JS runtime for services
2022-06-19 06:39:11 -07:00
Saad Jutt
b1a0fe7060 chore: code fixes 2022-06-19 18:35:10 +05:00
dde293c852 chore: write specs for get stp/execute 2022-06-19 07:08:36 +05:00
f738a6d7a3 chore: splitted functions into different files 2022-06-19 07:07:39 +05:00
Saad Jutt
3e0a2de2ad chore: nvmrc version updated to LTS 2022-06-18 01:49:47 +05:00
Saad Jutt
91cb7bd946 chore(workflow): CI/CD env variables updated 2022-06-18 01:13:56 +05:00
Saad Jutt
a501a300dc chore: lint fixes 2022-06-18 00:53:05 +05:00
Saad Jutt
b446baa822 chore: Merge branch 'main' into issue-184 2022-06-18 00:51:48 +05:00
9023cf33b5 fix(Studio): style fix for runtime dropdown 2022-06-17 23:17:23 +05:00
23b6692f02 fix: code/execute controller logic to handle different runtimes 2022-06-17 20:01:50 +05:00
Allan Bowe
6de91618ff Merge pull request #201 from sasjs/all-contributors/add-kknapen
docs: add kknapen as a contributor for userTesting
2022-06-17 16:33:41 +02:00
allcontributors[bot]
e06d66f312 docs: update .all-contributorsrc [skip ci] 2022-06-17 14:33:12 +00:00
allcontributors[bot]
1ffaf2e0ef docs: update README.md [skip ci] 2022-06-17 14:33:11 +00:00
Allan Bowe
393d3327db docs: add @VladislavParhomchik as a contributor 2022-06-17 14:32:52 +00:00
Allan Bowe
14cfb9a663 docs: add @allanbowe as a contributor 2022-06-17 14:29:14 +00:00
Allan Bowe
dd1f2b3ed7 docs: add @medjedovicm as a contributor 2022-06-17 14:29:06 +00:00
Allan Bowe
9f5dbbc8da docs: add @yuryshkoda as a contributor 2022-06-17 14:28:48 +00:00
Allan Bowe
9423bb2b23 docs: add @sabhas as a contributor 2022-06-17 14:28:39 +00:00
Allan Bowe
5bfcdc4dbb docs: add @saadjutt01 as a contributor 2022-06-17 14:27:53 +00:00
semantic-release-bot
ecd8ed9032 chore(release): 0.6.1 [skip ci]
## [0.6.1](https://github.com/sasjs/server/compare/v0.6.0...v0.6.1) (2022-06-17)

### Bug Fixes

* home page wording.  Using fix to force previous change through.. ([8702a4e](8702a4e8fd))
2022-06-17 13:19:51 +00:00
Allan Bowe
a8d89ff1d6 Merge pull request #200 from sasjs/nit
fix: home page wording.  Using fix to force previous change through..
2022-06-17 15:14:59 +02:00
Allan Bowe
8702a4e8fd fix: home page wording. Using fix to force previous change through.. 2022-06-17 13:14:02 +00:00
ab222cbaab chore: verify executable paths 2022-06-17 18:12:03 +05:00
Allan Bowe
5f06132ece Merge pull request #199 from sasjs/allanbowe-patch-1
Update Session.ts
2022-06-17 14:55:22 +02:00
Allan Bowe
56c80b0979 Update Session.ts 2022-06-17 13:46:08 +01:00
158acf1f97 chore: set sas,js as default run times 2022-06-17 04:02:40 +05:00
semantic-release-bot
c19a20c1d4 chore(release): 0.6.0 [skip ci]
# [0.6.0](https://github.com/sasjs/server/compare/v0.5.0...v0.6.0) (2022-06-16)

### Features

* get group by group name ([6b0b94a](6b0b94ad38))
2022-06-16 18:43:40 +00:00
Allan Bowe
f8eaadae7b Merge pull request #197 from sasjs/fetch-group-by-name
feat: get group by group name
2022-06-16 20:39:23 +02:00
90e0973a7f chore: add test for group name validation 2022-06-16 23:36:13 +05:00
869a13fc69 chore: error code fixes 2022-06-16 23:27:56 +05:00
1790e10fc1 chore: code fixes 2022-06-16 22:14:47 +05:00
6b0b94ad38 feat: get group by group name 2022-06-16 13:06:33 +05:00
53854d0012 fix: code fixes for executing program from program path including file extension 2022-06-15 16:18:07 +05:00
Saad Jutt
81501d17ab chore: code fixes 2022-06-15 16:03:04 +05:00
Saad Jutt
11a7f920f1 chore: Merge branch 'main' into issue-184 2022-06-15 15:56:17 +05:00
Saad Jutt
e359265c4b chore: quick fix 2022-06-14 17:05:40 +05:00
Saad Jutt
8e7c9e671c chore: quick fix 2022-06-14 17:05:13 +05:00
Saad Jutt
c830f44e29 chore: code fixes 2022-06-14 16:48:58 +05:00
de9ed15286 chore: update error message when stored program not found 2022-06-13 20:51:44 +05:00
325285f447 Merge branch 'main' into issue-184 2022-06-13 20:42:28 +05:00
16856165fb feat: create and inject code for uploaded files to code.js 2022-06-09 14:54:11 +05:00
058b3b0081 feat: configure child process with writeStream to write logs to log file 2022-06-08 02:01:31 +05:00
9d5a5e051f fix: no need to stringify _webout in preProgramVarStatements, developer should have _webout as string in actual code 2022-06-07 13:27:18 +05:00
2c704a544f fix: refactor sas/js session controller classes to inherit from base session controller class 2022-06-06 17:24:19 +05:00
6d6bda5626 fix: refactor code in preUploadMiddleware function 2022-06-06 17:23:09 +05:00
dffe6d7121 fix: refactor code in executeFile method of session controller 2022-06-06 15:23:42 +05:00
b4443819d4 fix: refactor code for session selection in preUploadMiddleware function 2022-06-06 15:19:39 +05:00
e5a7674fa1 chore: refactor ExecutionController class to remove code duplications 2022-06-06 09:09:21 +05:00
596ada7ca8 feat: validate sasjs_runtimes env var 2022-06-04 03:16:07 +05:00
f561ba4bf0 chore: documented sasjs_runtimes env variable in readme file 2022-06-04 03:12:51 +05:00
c58666eb81 fix: convert single executeProgram method to two methods i.e. executeSASProgram and executeJSProgram 2022-06-03 17:26:21 +05:00
5df619b3f6 fix: pass _program to execute file without extension 2022-06-03 17:24:29 +05:00
07295aa151 feat: conver single session controller to two controller i.e. SASSessionController and JSSessionController 2022-06-03 17:23:28 +05:00
194eaec7d4 fix: add runtimes to global process object 2022-06-03 17:19:12 +05:00
39 changed files with 1665 additions and 293 deletions

84
.all-contributorsrc Normal file
View File

@@ -0,0 +1,84 @@
{
"projectName": "server",
"projectOwner": "sasjs",
"repoType": "github",
"repoHost": "https://github.com",
"files": [
"README.md"
],
"imageSize": 100,
"commit": true,
"commitConvention": "angular",
"contributors": [
{
"login": "saadjutt01",
"name": "Saad Jutt",
"avatar_url": "https://avatars.githubusercontent.com/u/8914650?v=4",
"profile": "https://github.com/saadjutt01",
"contributions": [
"code",
"test"
]
},
{
"login": "sabhas",
"name": "Sabir Hassan",
"avatar_url": "https://avatars.githubusercontent.com/u/82647447?v=4",
"profile": "https://github.com/sabhas",
"contributions": [
"code",
"test"
]
},
{
"login": "YuryShkoda",
"name": "Yury Shkoda",
"avatar_url": "https://avatars.githubusercontent.com/u/25773492?v=4",
"profile": "https://www.erudicat.com/",
"contributions": [
"code",
"test"
]
},
{
"login": "medjedovicm",
"name": "Mihajlo Medjedovic",
"avatar_url": "https://avatars.githubusercontent.com/u/18329105?v=4",
"profile": "https://github.com/medjedovicm",
"contributions": [
"code",
"test"
]
},
{
"login": "allanbowe",
"name": "Allan Bowe",
"avatar_url": "https://avatars.githubusercontent.com/u/4420615?v=4",
"profile": "https://4gl.io/",
"contributions": [
"code",
"doc"
]
},
{
"login": "VladislavParhomchik",
"name": "Vladislav Parhomchik",
"avatar_url": "https://avatars.githubusercontent.com/u/83717836?v=4",
"profile": "https://github.com/VladislavParhomchik",
"contributions": [
"test"
]
},
{
"login": "kknapen",
"name": "Koen Knapen",
"avatar_url": "https://avatars.githubusercontent.com/u/78609432?v=4",
"profile": "https://github.com/kknapen",
"contributions": [
"userTesting"
]
}
],
"contributorsPerLine": 7,
"skipCi": true
}

View File

@@ -55,6 +55,9 @@ jobs:
REFRESH_TOKEN_SECRET: ${{secrets.REFRESH_TOKEN_SECRET}} REFRESH_TOKEN_SECRET: ${{secrets.REFRESH_TOKEN_SECRET}}
AUTH_CODE_SECRET: ${{secrets.AUTH_CODE_SECRET}} AUTH_CODE_SECRET: ${{secrets.AUTH_CODE_SECRET}}
SESSION_SECRET: ${{secrets.SESSION_SECRET}} SESSION_SECRET: ${{secrets.SESSION_SECRET}}
RUN_TIMES: 'sas,js'
SAS_PATH: '/some/path/to/sas'
NODE_PATH: '/some/path/to/node'
- name: Build Package - name: Build Package
working-directory: ./api working-directory: ./api

2
.nvmrc
View File

@@ -1 +1 @@
v16.14.0 v16.15.1

View File

@@ -1,3 +1,58 @@
## [0.7.2](https://github.com/sasjs/server/compare/v0.7.1...v0.7.2) (2022-06-20)
### Bug Fixes
* removing UTF-8 options from commandline. There appears to be no reliable way to enforce ([f6dc74f](https://github.com/sasjs/server/commit/f6dc74f16bddafa1de9c83c2f27671a241abdad4))
## [0.7.1](https://github.com/sasjs/server/compare/v0.7.0...v0.7.1) (2022-06-20)
### Bug Fixes
* default runtime should be sas ([91d29cb](https://github.com/sasjs/server/commit/91d29cb1272c28afbceaf39d1e0a87e17fbfdcd6))
* **Studio:** default selection of runtime fixed ([eb569c7](https://github.com/sasjs/server/commit/eb569c7b827c872ed2c4bc114559b97d87fd2aa0))
* webout path fixed in code.js when running on windows ([99a1107](https://github.com/sasjs/server/commit/99a110736448f66f99a512396b268fc31a3feef0))
# [0.7.0](https://github.com/sasjs/server/compare/v0.6.1...v0.7.0) (2022-06-19)
### Bug Fixes
* add runtimes to global process object ([194eaec](https://github.com/sasjs/server/commit/194eaec7d4a561468f83bf6efce484909ee532eb))
* code fixes for executing program from program path including file extension ([53854d0](https://github.com/sasjs/server/commit/53854d001279462104b24c0e59a8c94ab4938a94))
* code/execute controller logic to handle different runtimes ([23b6692](https://github.com/sasjs/server/commit/23b6692f02e4afa33c9dc95d242eb8645c19d546))
* convert single executeProgram method to two methods i.e. executeSASProgram and executeJSProgram ([c58666e](https://github.com/sasjs/server/commit/c58666eb81514de500519e7b96c1981778ec149b))
* no need to stringify _webout in preProgramVarStatements, developer should have _webout as string in actual code ([9d5a5e0](https://github.com/sasjs/server/commit/9d5a5e051fd821295664ddb3a1fd64629894a44c))
* pass _program to execute file without extension ([5df619b](https://github.com/sasjs/server/commit/5df619b3f63571e8e326261d8114869d33881d91))
* refactor code for session selection in preUploadMiddleware function ([b444381](https://github.com/sasjs/server/commit/b4443819d42afecebc0f382c58afb9010d4775ef))
* refactor code in executeFile method of session controller ([dffe6d7](https://github.com/sasjs/server/commit/dffe6d7121d569e5c7d13023c6ca68d8c901c88e))
* refactor code in preUploadMiddleware function ([6d6bda5](https://github.com/sasjs/server/commit/6d6bda56267babde7b98cf69e32973d56d719f75))
* refactor sas/js session controller classes to inherit from base session controller class ([2c704a5](https://github.com/sasjs/server/commit/2c704a544f4e31a8e8e833a9a62ba016bcfa6c7c))
* **Studio:** style fix for runtime dropdown ([9023cf3](https://github.com/sasjs/server/commit/9023cf33b5fa4b13c2d5e9b80ae307df69c7fc02))
### Features
* configure child process with writeStream to write logs to log file ([058b3b0](https://github.com/sasjs/server/commit/058b3b00816e582e143953c2f0b8330bde2181b8))
* conver single session controller to two controller i.e. SASSessionController and JSSessionController ([07295aa](https://github.com/sasjs/server/commit/07295aa151175db8c93eeef806fc3b7fde40ac72))
* create and inject code for uploaded files to code.js ([1685616](https://github.com/sasjs/server/commit/16856165fb292dc9ffa897189ba105bd9f362267))
* validate sasjs_runtimes env var ([596ada7](https://github.com/sasjs/server/commit/596ada7ca88798d6d71f6845633a006fd22438ea))
## [0.6.1](https://github.com/sasjs/server/compare/v0.6.0...v0.6.1) (2022-06-17)
### Bug Fixes
* home page wording. Using fix to force previous change through.. ([8702a4e](https://github.com/sasjs/server/commit/8702a4e8fd1bbfaf4f426b75e8b85a87ede0e0b0))
# [0.6.0](https://github.com/sasjs/server/compare/v0.5.0...v0.6.0) (2022-06-16)
### Features
* get group by group name ([6b0b94a](https://github.com/sasjs/server/commit/6b0b94ad38215ae58e62279a4f73ac3ed2d9d0e8))
# [0.5.0](https://github.com/sasjs/server/compare/v0.4.2...v0.5.0) (2022-06-16) # [0.5.0](https://github.com/sasjs/server/compare/v0.4.2...v0.5.0) (2022-06-16)

View File

@@ -1,5 +1,11 @@
# SASjs Server # SASjs Server
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[![All Contributors](https://img.shields.io/badge/all_contributors-7-orange.svg?style=flat-square)](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->
SASjs Server provides a NodeJS wrapper for calling the SAS binary executable. It can be installed on an actual SAS server, or locally on your desktop. It provides: SASjs Server provides a NodeJS wrapper for calling the SAS binary executable. It can be installed on an actual SAS server, or locally on your desktop. It provides:
- Virtual filesystem for storing SAS programs and other content - Virtual filesystem for storing SAS programs and other content
@@ -61,10 +67,14 @@ MODE=
# Path to SAS executable (sas.exe / sas.sh) # Path to SAS executable (sas.exe / sas.sh)
SAS_PATH=/path/to/sas/executable.exe SAS_PATH=/path/to/sas/executable.exe
# Path to Node.js executable
NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node
# Path to working directory # Path to working directory
# This location is for SAS WORK, staged files, DRIVE, configuration etc # This location is for SAS WORK, staged files, DRIVE, configuration etc
SASJS_ROOT=./sasjs_root SASJS_ROOT=./sasjs_root
# options: [http|https] default: http # options: [http|https] default: http
PROTOCOL= PROTOCOL=
@@ -129,6 +139,13 @@ HELMET_CSP_CONFIG_PATH=./csp.config.json
# Docs: https://www.npmjs.com/package/morgan#predefined-formats # Docs: https://www.npmjs.com/package/morgan#predefined-formats
LOG_FORMAT_MORGAN= LOG_FORMAT_MORGAN=
# A comma separated string that defines the available runTimes.
# Priority is given to the runtime that comes first in the string.
# Possible options at the moment are sas and js
# options: [sas,js|js,sas|sas|js] default:sas
RUN_TIMES=
``` ```
## Persisting the Session ## Persisting the Session
@@ -185,3 +202,29 @@ The following credentials can be used for the initial connection to SASjs/server
- CLIENTID: `clientID1` - CLIENTID: `clientID1`
- USERNAME: `secretuser` - USERNAME: `secretuser`
- PASSWORD: `secretpassword` - PASSWORD: `secretpassword`
## Contributors ✨
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="https://github.com/saadjutt01"><img src="https://avatars.githubusercontent.com/u/8914650?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Saad Jutt</b></sub></a><br /><a href="https://github.com/sasjs/server/commits?author=saadjutt01" title="Code">💻</a> <a href="https://github.com/sasjs/server/commits?author=saadjutt01" title="Tests">⚠️</a></td>
<td align="center"><a href="https://github.com/sabhas"><img src="https://avatars.githubusercontent.com/u/82647447?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Sabir Hassan</b></sub></a><br /><a href="https://github.com/sasjs/server/commits?author=sabhas" title="Code">💻</a> <a href="https://github.com/sasjs/server/commits?author=sabhas" title="Tests">⚠️</a></td>
<td align="center"><a href="https://www.erudicat.com/"><img src="https://avatars.githubusercontent.com/u/25773492?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Yury Shkoda</b></sub></a><br /><a href="https://github.com/sasjs/server/commits?author=YuryShkoda" title="Code">💻</a> <a href="https://github.com/sasjs/server/commits?author=YuryShkoda" title="Tests">⚠️</a></td>
<td align="center"><a href="https://github.com/medjedovicm"><img src="https://avatars.githubusercontent.com/u/18329105?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Mihajlo Medjedovic</b></sub></a><br /><a href="https://github.com/sasjs/server/commits?author=medjedovicm" title="Code">💻</a> <a href="https://github.com/sasjs/server/commits?author=medjedovicm" title="Tests">⚠️</a></td>
<td align="center"><a href="https://4gl.io/"><img src="https://avatars.githubusercontent.com/u/4420615?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Allan Bowe</b></sub></a><br /><a href="https://github.com/sasjs/server/commits?author=allanbowe" title="Code">💻</a> <a href="https://github.com/sasjs/server/commits?author=allanbowe" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/VladislavParhomchik"><img src="https://avatars.githubusercontent.com/u/83717836?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Vladislav Parhomchik</b></sub></a><br /><a href="https://github.com/sasjs/server/commits?author=VladislavParhomchik" title="Tests">⚠️</a></td>
<td align="center"><a href="https://github.com/kknapen"><img src="https://avatars.githubusercontent.com/u/78609432?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Koen Knapen</b></sub></a><br /><a href="#userTesting-kknapen" title="User Testing">📓</a></td>
</tr>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!

View File

@@ -17,7 +17,10 @@ AUTH_CODE_SECRET=<secret>
SESSION_SECRET=<secret> SESSION_SECRET=<secret>
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
RUN_TIMES=[sas|js|sas,js|js,sas] default considered as sas
SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas
NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node
SASJS_ROOT=./sasjs_root SASJS_ROOT=./sasjs_root
LOG_FORMAT_MORGAN=common LOG_FORMAT_MORGAN=common

View File

@@ -1 +1 @@
v16.14.0 v16.15.1

View File

@@ -139,14 +139,24 @@ components:
- httpHeaders - httpHeaders
type: object type: object
additionalProperties: false additionalProperties: false
ExecuteSASCodePayload: RunTimeType:
enum:
- sas
- js
type: string
ExecuteCodePayload:
properties: properties:
code: code:
type: string type: string
description: 'Code of SAS program' description: 'Code of program'
example: '* SAS Code HERE;' example: '* Code HERE;'
runTime:
$ref: '#/components/schemas/RunTimeType'
description: 'runtime for program'
example: js
required: required:
- code - code
- runTime
type: object type: object
additionalProperties: false additionalProperties: false
MemberType.folder: MemberType.folder:
@@ -425,6 +435,27 @@ components:
- description - description
type: object type: object
additionalProperties: false additionalProperties: false
_LeanDocument__LeanDocument_T__:
properties: {}
type: object
Pick__LeanDocument_T_.Exclude_keyof_LeanDocument_T_.Exclude_keyofDocument._id-or-id-or-__v_-or-%24isSingleNested__:
properties:
id:
description: 'The string version of this documents _id.'
_id:
$ref: '#/components/schemas/_LeanDocument__LeanDocument_T__'
description: 'This documents _id.'
__v:
description: 'This documents __v.'
type: object
description: 'From T, pick a set of properties whose keys are in the union K'
Omit__LeanDocument_this_.Exclude_keyofDocument._id-or-id-or-__v_-or-%24isSingleNested_:
$ref: '#/components/schemas/Pick__LeanDocument_T_.Exclude_keyof_LeanDocument_T_.Exclude_keyofDocument._id-or-id-or-__v_-or-%24isSingleNested__'
description: 'Construct a type with the properties of T except for those in type K.'
LeanDocument_this_:
$ref: '#/components/schemas/Omit__LeanDocument_this_.Exclude_keyofDocument._id-or-id-or-__v_-or-%24isSingleNested_'
IGroup:
$ref: '#/components/schemas/LeanDocument_this_'
InfoResponse: InfoResponse:
properties: properties:
mode: mode:
@@ -437,11 +468,16 @@ components:
type: array type: array
protocol: protocol:
type: string type: string
runTimes:
items:
type: string
type: array
required: required:
- mode - mode
- cors - cors
- whiteList - whiteList
- protocol - protocol
- runTimes
type: object type: object
additionalProperties: false additionalProperties: false
ExecuteReturnJsonPayload: ExecuteReturnJsonPayload:
@@ -630,7 +666,7 @@ paths:
$ref: '#/components/schemas/ClientPayload' $ref: '#/components/schemas/ClientPayload'
/SASjsApi/code/execute: /SASjsApi/code/execute:
post: post:
operationId: ExecuteSASCode operationId: ExecuteCode
responses: responses:
'200': '200':
description: Ok description: Ok
@@ -651,7 +687,7 @@ paths:
content: content:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/ExecuteSASCodePayload' $ref: '#/components/schemas/ExecuteCodePayload'
/SASjsApi/drive/deploy: /SASjsApi/drive/deploy:
post: post:
operationId: Deploy operationId: Deploy
@@ -727,7 +763,7 @@ paths:
examples: examples:
'Example 1': 'Example 1':
value: {status: failure, message: 'Deployment failed!'} value: {status: failure, message: 'Deployment failed!'}
description: "Accepts JSON file and zipped compressed JSON file as well.\nCompressed file should only contain one JSON file and should have same name\nas of compressed file e.g. deploy.JSON should be compressed to deploy.JSON.zip\nAny other file or JSON file in zipped will be ignored!" description: "Accepts JSON file and zipped compressed JSON file as well.\r\nCompressed file should only contain one JSON file and should have same name\r\nas of compressed file e.g. deploy.JSON should be compressed to deploy.JSON.zip\r\nAny other file or JSON file in zipped will be ignored!"
summary: 'Creates/updates files within SASjs Drive using uploaded JSON/compressed JSON file.' summary: 'Creates/updates files within SASjs Drive using uploaded JSON/compressed JSON file.'
tags: tags:
- Drive - Drive
@@ -815,7 +851,7 @@ paths:
examples: examples:
'Example 1': 'Example 1':
value: {status: failure, message: 'File request failed.'} value: {status: failure, message: 'File request failed.'}
description: "It's optional to either provide `_filePath` in url as query parameter\nOr provide `filePath` in body as form field.\nBut it's required to provide else API will respond with Bad Request." description: "It's optional to either provide `_filePath` in url as query parameter\r\nOr provide `filePath` in body as form field.\r\nBut it's required to provide else API will respond with Bad Request."
summary: 'Create a file in SASjs Drive' summary: 'Create a file in SASjs Drive'
tags: tags:
- Drive - Drive
@@ -866,7 +902,7 @@ paths:
examples: examples:
'Example 1': 'Example 1':
value: {status: failure, message: 'File request failed.'} value: {status: failure, message: 'File request failed.'}
description: "It's optional to either provide `_filePath` in url as query parameter\nOr provide `filePath` in body as form field.\nBut it's required to provide else API will respond with Bad Request." description: "It's optional to either provide `_filePath` in url as query parameter\r\nOr provide `filePath` in body as form field.\r\nBut it's required to provide else API will respond with Bad Request."
summary: 'Modify a file in SASjs Drive' summary: 'Modify a file in SASjs Drive'
tags: tags:
- Drive - Drive
@@ -1216,6 +1252,30 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/GroupPayload' $ref: '#/components/schemas/GroupPayload'
'/SASjsApi/group/by/groupname/{name}':
get:
operationId: GetGroupByGroupName
responses:
'200':
description: Ok
content:
application/json:
schema:
$ref: '#/components/schemas/GroupDetailsResponse'
summary: 'Get list of members of a group (userName). All users can request this.'
tags:
- Group
security:
-
bearerAuth: []
parameters:
-
description: 'The group''s name'
in: path
name: name
required: true
schema:
type: string
'/SASjsApi/group/{groupId}': '/SASjsApi/group/{groupId}':
get: get:
operationId: GetGroup operationId: GetGroup
@@ -1245,8 +1305,14 @@ paths:
delete: delete:
operationId: DeleteGroup operationId: DeleteGroup
responses: responses:
'204': '200':
description: 'No content' description: Ok
content:
application/json:
schema:
allOf:
- {$ref: '#/components/schemas/IGroup'}
- {properties: {_id: {}}, required: [_id], type: object}
summary: 'Delete a group. Admin task only.' summary: 'Delete a group. Admin task only.'
tags: tags:
- Group - Group
@@ -1350,7 +1416,7 @@ paths:
$ref: '#/components/schemas/InfoResponse' $ref: '#/components/schemas/InfoResponse'
examples: examples:
'Example 1': 'Example 1':
value: {mode: desktop, cors: enable, whiteList: ['http://example.com', 'http://example2.com'], protocol: http} value: {mode: desktop, cors: enable, whiteList: ['http://example.com', 'http://example2.com'], protocol: http, runTimes: [sas, js]}
summary: 'Get server info (mode, cors, whiteList, protocol).' summary: 'Get server info (mode, cors, whiteList, protocol).'
tags: tags:
- Info - Info
@@ -1388,8 +1454,8 @@ paths:
anyOf: anyOf:
- {type: string} - {type: string}
- {type: string, format: byte} - {type: string, format: byte}
description: "Trigger a SAS program using it's location in the _program URL parameter.\nEnable debugging using the _debug URL parameter. Setting _debug=131 will\ncause the log to be streamed in the output.\n\nAdditional URL parameters are turned into SAS macro variables.\n\nAny files provided in the request body are placed into the SAS session with\ncorresponding _WEBIN_XXX variables created.\n\nThe response headers can be adjusted using the mfs_httpheader() macro. Any\nfile type can be returned, including binary files such as zip or xls.\n\nIf _debug is >= 131, response headers will contain Content-Type: 'text/plain'\n\nThis behaviour differs for POST requests, in which case the response is\nalways JSON." description: "Trigger a SAS or JS program using the _program URL parameter.\r\n\r\nAccepts URL parameters and file uploads. For more details, see docs:\r\n\r\nhttps://server.sasjs.io/storedprograms"
summary: 'Execute Stored Program, return raw _webout content.' summary: 'Execute a Stored Program, returns raw _webout content.'
tags: tags:
- STP - STP
security: security:
@@ -1397,13 +1463,13 @@ paths:
bearerAuth: [] bearerAuth: []
parameters: parameters:
- -
description: 'Location of SAS program' description: 'Location of SAS or JS code'
in: query in: query
name: _program name: _program
required: true required: true
schema: schema:
type: string type: string
example: /Public/somefolder/some.file example: /Projects/myApp/some/program
post: post:
operationId: ExecuteReturnJson operationId: ExecuteReturnJson
responses: responses:
@@ -1416,8 +1482,8 @@ paths:
examples: examples:
'Example 1': 'Example 1':
value: {status: success, _webout: 'webout content', log: [], httpHeaders: {Content-type: application/zip, Cache-Control: 'public, max-age=1000'}} value: {status: success, _webout: 'webout content', log: [], httpHeaders: {Content-type: application/zip, Cache-Control: 'public, max-age=1000'}}
description: "Trigger a SAS program using it's location in the _program URL parameter.\nEnable debugging using the _debug URL parameter. In any case, the log is\nalways returned in the log object.\n\nAdditional URL parameters are turned into SAS macro variables.\n\nAny files provided in the request body are placed into the SAS session with\ncorresponding _WEBIN_XXX variables created.\n\nThe response will be a JSON object with the following root attributes: log,\nwebout, headers.\n\nThe webout will be a nested JSON object ONLY if the response-header\ncontains a content-type of application/json AND it is valid JSON.\nOtherwise it will be a stringified version of the webout content.\n\nResponse headers from the mfs_httpheader macro are simply listed in the\nheaders object, for POST requests they have no effect on the actual\nresponse header." description: "Trigger a SAS or JS program using the _program URL parameter.\r\n\r\nAccepts URL parameters and file uploads. For more details, see docs:\r\n\r\nhttps://server.sasjs.io/storedprograms\r\n\r\nThe response will be a JSON object with the following root attributes:\r\nlog, webout, headers.\r\n\r\nThe webout attribute will be nested JSON ONLY if the response-header\r\ncontains a content-type of application/json AND it is valid JSON.\r\nOtherwise it will be a stringified version of the webout content."
summary: 'Execute Stored Program, return JSON' summary: 'Execute a Stored Program, return a JSON object'
tags: tags:
- STP - STP
security: security:
@@ -1425,13 +1491,13 @@ paths:
bearerAuth: [] bearerAuth: []
parameters: parameters:
- -
description: 'Location of SAS program' description: 'Location of SAS or JS code'
in: query in: query
name: _program name: _program
required: false required: false
schema: schema:
type: string type: string
example: /Public/somefolder/some.file example: /Projects/myApp/some/program
requestBody: requestBody:
required: false required: false
content: content:

View File

@@ -6,15 +6,21 @@ import {
getPreProgramVariables, getPreProgramVariables,
getUserAutoExec, getUserAutoExec,
ModeType, ModeType,
parseLogToArray parseLogToArray,
RunTimeType
} from '../utils' } from '../utils'
interface ExecuteSASCodePayload { interface ExecuteCodePayload {
/** /**
* Code of SAS program * Code of program
* @example "* SAS Code HERE;" * @example "* Code HERE;"
*/ */
code: string code: string
/**
* runtime for program
* @example "js"
*/
runTime: RunTimeType
} }
@Security('bearerAuth') @Security('bearerAuth')
@@ -26,17 +32,17 @@ export class CodeController {
* @summary Run SAS Code and returns log * @summary Run SAS Code and returns log
*/ */
@Post('/execute') @Post('/execute')
public async executeSASCode( public async executeCode(
@Request() request: express.Request, @Request() request: express.Request,
@Body() body: ExecuteSASCodePayload @Body() body: ExecuteCodePayload
): Promise<ExecuteReturnJsonResponse> { ): Promise<ExecuteReturnJsonResponse> {
return executeSASCode(request, body) return executeCode(request, body)
} }
} }
const executeSASCode = async ( const executeCode = async (
req: express.Request, req: express.Request,
{ code }: ExecuteSASCodePayload { code, runTime }: ExecuteCodePayload
) => { ) => {
const { user } = req const { user } = req
const userAutoExec = const userAutoExec =
@@ -46,13 +52,14 @@ const executeSASCode = async (
try { try {
const { webout, log, httpHeaders } = const { webout, log, httpHeaders } =
(await new ExecutionController().executeProgram( (await new ExecutionController().executeProgram({
code, program: code,
getPreProgramVariables(req), preProgramVariables: getPreProgramVariables(req),
{ ...req.query, _debug: 131 }, vars: { ...req.query, _debug: 131 },
{ userAutoExec }, otherArgs: { userAutoExec },
true returnJson: true,
)) as ExecuteReturnJson runTime: runTime
})) as ExecuteReturnJson
return { return {
status: 'success', status: 'success',

View File

@@ -28,6 +28,11 @@ interface GroupDetailsResponse {
users: UserResponse[] users: UserResponse[]
} }
interface GetGroupBy {
groupId?: number
name?: string
}
@Security('bearerAuth') @Security('bearerAuth')
@Route('SASjsApi/group') @Route('SASjsApi/group')
@Tags('Group') @Tags('Group')
@@ -66,6 +71,18 @@ export class GroupController {
return createGroup(body) return createGroup(body)
} }
/**
* @summary Get list of members of a group (userName). All users can request this.
* @param name The group's name
* @example dcgroup
*/
@Get('by/groupname/{name}')
public async getGroupByGroupName(
@Path() name: string
): Promise<GroupDetailsResponse> {
return getGroup({ name })
}
/** /**
* @summary Get list of members of a group (userName). All users can request this. * @summary Get list of members of a group (userName). All users can request this.
* @param groupId The group's identifier * @param groupId The group's identifier
@@ -75,7 +92,7 @@ export class GroupController {
public async getGroup( public async getGroup(
@Path() groupId: number @Path() groupId: number
): Promise<GroupDetailsResponse> { ): Promise<GroupDetailsResponse> {
return getGroup(groupId) return getGroup({ groupId })
} }
/** /**
@@ -129,9 +146,13 @@ export class GroupController {
*/ */
@Delete('{groupId}') @Delete('{groupId}')
public async deleteGroup(@Path() groupId: number) { public async deleteGroup(@Path() groupId: number) {
const { deletedCount } = await Group.deleteOne({ groupId }) const group = await Group.findOne({ groupId })
if (deletedCount) return if (group) return await group.remove()
throw new Error('No Group deleted!') throw {
code: 404,
status: 'Not Found',
message: 'Group not found.'
}
} }
} }
@@ -145,6 +166,15 @@ const createGroup = async ({
description, description,
isActive isActive
}: GroupPayload): Promise<GroupDetailsResponse> => { }: GroupPayload): Promise<GroupDetailsResponse> => {
// Checking if user is already in the database
const groupnameExist = await Group.findOne({ name })
if (groupnameExist)
throw {
code: 409,
status: 'Conflict',
message: 'Group name already exists.'
}
const group = new Group({ const group = new Group({
name, name,
description, description,
@@ -162,15 +192,20 @@ const createGroup = async ({
} }
} }
const getGroup = async (groupId: number): Promise<GroupDetailsResponse> => { const getGroup = async (findBy: GetGroupBy): Promise<GroupDetailsResponse> => {
const group = (await Group.findOne( const group = (await Group.findOne(
{ groupId }, findBy,
'groupId name description isActive users -_id' 'groupId name description isActive users -_id'
).populate( ).populate(
'users', 'users',
'id username displayName -_id' 'id username displayName -_id'
)) as unknown as GroupDetailsResponse )) as unknown as GroupDetailsResponse
if (!group) throw new Error('Group not found.') if (!group)
throw {
code: 404,
status: 'Not Found',
message: 'Group not found.'
}
return { return {
groupId: group.groupId, groupId: group.groupId,
@@ -199,16 +234,31 @@ const updateUsersListInGroup = async (
action: 'addUser' | 'removeUser' action: 'addUser' | 'removeUser'
): Promise<GroupDetailsResponse> => { ): Promise<GroupDetailsResponse> => {
const group = await Group.findOne({ groupId }) const group = await Group.findOne({ groupId })
if (!group) throw new Error('Group not found.') if (!group)
throw {
code: 404,
status: 'Not Found',
message: 'Group not found.'
}
const user = await User.findOne({ id: userId }) const user = await User.findOne({ id: userId })
if (!user) throw new Error('User not found.') if (!user)
throw {
code: 404,
status: 'Not Found',
message: 'User not found.'
}
const updatedGroup = (action === 'addUser' const updatedGroup = (action === 'addUser'
? await group.addUser(user._id) ? await group.addUser(user._id)
: await group.removeUser(user._id)) as unknown as GroupDetailsResponse : await group.removeUser(user._id)) as unknown as GroupDetailsResponse
if (!updatedGroup) throw new Error('Unable to update group') if (!updatedGroup)
throw {
code: 400,
status: 'Bad Request',
message: 'Unable to update group.'
}
if (action === 'addUser') user.addGroup(group._id) if (action === 'addUser') user.addGroup(group._id)
else user.removeGroup(group._id) else user.removeGroup(group._id)

View File

@@ -5,6 +5,7 @@ export interface InfoResponse {
cors: string cors: string
whiteList: string[] whiteList: string[]
protocol: string protocol: string
runTimes: string[]
} }
@Route('SASjsApi/info') @Route('SASjsApi/info')
@@ -18,7 +19,8 @@ export class InfoController {
mode: 'desktop', mode: 'desktop',
cors: 'enable', cors: 'enable',
whiteList: ['http://example.com', 'http://example2.com'], whiteList: ['http://example.com', 'http://example2.com'],
protocol: 'http' protocol: 'http',
runTimes: ['sas', 'js']
}) })
@Get('/') @Get('/')
public info(): InfoResponse { public info(): InfoResponse {
@@ -29,7 +31,8 @@ export class InfoController {
(process.env.MODE === 'server' ? 'disable' : 'enable'), (process.env.MODE === 'server' ? 'disable' : 'enable'),
whiteList: whiteList:
process.env.WHITELIST?.split(' ')?.filter((url) => !!url) ?? [], process.env.WHITELIST?.split(' ')?.filter((url) => !!url) ?? [],
protocol: process.env.PROTOCOL ?? 'http' protocol: process.env.PROTOCOL ?? 'http',
runTimes: process.runTimes
} }
return response return response
} }

View File

@@ -1,21 +1,18 @@
import path from 'path' import path from 'path'
import fs from 'fs' import fs from 'fs'
import { getSessionController } from './'
import { import {
readFile, getSASSessionController,
fileExists, getJSSessionController,
createFile, processProgram
moveFile, } from './'
readFileBinary import { readFile, fileExists, createFile, readFileBinary } from '@sasjs/utils'
} from '@sasjs/utils'
import { PreProgramVars, Session, TreeNode } from '../../types' import { PreProgramVars, Session, TreeNode } from '../../types'
import { import {
extractHeaders, extractHeaders,
generateFileUploadSasCode,
getFilesFolder, getFilesFolder,
getMacrosFolder,
HTTPHeaders, HTTPHeaders,
isDebugOn isDebugOn,
RunTimeType
} from '../../utils' } from '../../utils'
export interface ExecutionVars { export interface ExecutionVars {
@@ -33,39 +30,56 @@ export interface ExecuteReturnJson {
log?: string log?: string
} }
export class ExecutionController { interface ExecuteFileParams {
async executeFile( programPath: string
programPath: string, preProgramVariables: PreProgramVars
preProgramVariables: PreProgramVars, vars: ExecutionVars
vars: ExecutionVars, otherArgs?: any
otherArgs?: any, returnJson?: boolean
returnJson?: boolean, session?: Session
session?: Session runTime: RunTimeType
) { }
if (!(await fileExists(programPath)))
throw `The Stored Program at (${vars._program}) does not exist, or you do not have permission to view it.`
interface ExecuteProgramParams extends Omit<ExecuteFileParams, 'programPath'> {
program: string
}
export class ExecutionController {
async executeFile({
programPath,
preProgramVariables,
vars,
otherArgs,
returnJson,
session,
runTime
}: ExecuteFileParams) {
const program = await readFile(programPath) const program = await readFile(programPath)
return this.executeProgram( return this.executeProgram({
program, program,
preProgramVariables, preProgramVariables,
vars, vars,
otherArgs, otherArgs,
returnJson, returnJson,
session session,
) runTime
})
} }
async executeProgram( async executeProgram({
program: string, program,
preProgramVariables: PreProgramVars, preProgramVariables,
vars: ExecutionVars, vars,
otherArgs?: any, otherArgs,
returnJson?: boolean, returnJson,
sessionByFileUpload?: Session session: sessionByFileUpload,
): Promise<ExecuteReturnRaw | ExecuteReturnJson> { runTime
const sessionController = getSessionController() }: ExecuteProgramParams): Promise<ExecuteReturnRaw | ExecuteReturnJson> {
const sessionController =
runTime === RunTimeType.SAS
? getSASSessionController()
: getJSSessionController()
const session = const session =
sessionByFileUpload ?? (await sessionController.getSession()) sessionByFileUpload ?? (await sessionController.getSession())
@@ -83,78 +97,18 @@ export class ExecutionController {
preProgramVariables?.httpHeaders.join('\n') ?? '' preProgramVariables?.httpHeaders.join('\n') ?? ''
) )
const varStatments = Object.keys(vars).reduce( await processProgram(
(computed: string, key: string) => program,
`${computed}%let ${key}=${vars[key]};\n`, preProgramVariables,
'' vars,
session,
weboutPath,
tokenFile,
runTime,
logPath,
otherArgs
) )
const preProgramVarStatments = `
%let _sasjs_tokenfile=${tokenFile};
%let _sasjs_username=${preProgramVariables?.username};
%let _sasjs_userid=${preProgramVariables?.userId};
%let _sasjs_displayname=${preProgramVariables?.displayName};
%let _sasjs_apiserverurl=${preProgramVariables?.serverUrl};
%let _sasjs_apipath=/SASjsApi/stp/execute;
%let _metaperson=&_sasjs_displayname;
%let _metauser=&_sasjs_username;
%let sasjsprocessmode=Stored Program;
%let sasjs_stpsrv_header_loc=%sysfunc(pathname(work))/../stpsrv_header.txt;
%global SYSPROCESSMODE SYSTCPIPHOSTNAME SYSHOSTINFOLONG;
%macro _sasjs_server_init();
%if "&SYSPROCESSMODE"="" %then %let SYSPROCESSMODE=&sasjsprocessmode;
%if "&SYSTCPIPHOSTNAME"="" %then %let SYSTCPIPHOSTNAME=&_sasjs_apiserverurl;
%mend;
%_sasjs_server_init()
`
program = `
options insert=(SASAUTOS="${getMacrosFolder()}");
/* runtime vars */
${varStatments}
filename _webout "${weboutPath}" mod;
/* dynamic user-provided vars */
${preProgramVarStatments}
/* user autoexec starts */
${otherArgs?.userAutoExec ?? ''}
/* user autoexec ends */
/* actual job code */
${program}`
// if no files are uploaded filesNamesMap will be undefined
if (otherArgs?.filesNamesMap) {
const uploadSasCode = await generateFileUploadSasCode(
otherArgs.filesNamesMap,
session.path
)
//If sas code for the file is generated it will be appended to the top of sasCode
if (uploadSasCode.length > 0) {
program = `${uploadSasCode}` + program
}
}
const codePath = path.join(session.path, 'code.sas')
// Creating this file in a RUNNING session will break out
// the autoexec loop and actually execute the program
// but - given it will take several milliseconds to create
// (which can mean SAS trying to run a partial program, or
// failing due to file lock) we first create the file THEN
// we rename it.
await createFile(codePath + '.bkp', program)
await moveFile(codePath + '.bkp', codePath)
// we now need to poll the session status
while (!session.completed) {
await delay(50)
}
const log = (await fileExists(logPath)) ? await readFile(logPath) : '' const log = (await fileExists(logPath)) ? await readFile(logPath) : ''
const headersContent = (await fileExists(headersPath)) const headersContent = (await fileExists(headersPath))
? await readFile(headersPath) ? await readFile(headersPath)
@@ -228,5 +182,3 @@ ${program}`
return root return root
} }
} }
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))

View File

@@ -1,13 +1,18 @@
import { Request, RequestHandler } from 'express' 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 { getSASSessionController, getJSSessionController } from '.'
import {
executeProgramRawValidation,
getRunTimeAndFilePath,
RunTimeType
} from '../../utils'
export class FileUploadController { export class FileUploadController {
private storage = multer.diskStorage({ private storage = multer.diskStorage({
destination: function (req: Request, file: any, cb: any) { destination: function (req: Request, file: any, cb: any) {
//Sending the intercepted files to the sessions subfolder //Sending the intercepted files to the sessions subfolder
cb(null, req.sasSession?.path) cb(null, req.sasjsSession?.path)
}, },
filename: function (req: Request, file: any, cb: any) { filename: function (req: Request, file: any, cb: any) {
//req_file prefix + unique hash added to sas request files //req_file prefix + unique hash added to sas request files
@@ -20,15 +25,36 @@ export class FileUploadController {
//It will intercept request and generate unique uuid to be used as a subfolder name //It will intercept request and generate unique uuid to be used as a subfolder name
//that will store the files uploaded //that will store the files uploaded
public preUploadMiddleware: RequestHandler = async (req, res, next) => { public preUploadMiddleware: RequestHandler = async (req, res, next) => {
let session const { error: errQ, value: query } = executeProgramRawValidation(req.query)
const { error: errB, value: body } = executeProgramRawValidation(req.body)
const sessionController = getSessionController() if (errQ && errB) return res.status(400).send(errB.details[0].message)
session = await sessionController.getSession()
const programPath = (query?._program ?? body?._program) as string
let runTime
try {
;({ runTime } = await getRunTimeAndFilePath(programPath))
} catch (err: any) {
res.status(400).send({
status: 'failure',
message: 'Job execution failed',
error: typeof err === 'object' ? err.toString() : err
})
}
const sessionController =
runTime === RunTimeType.SAS
? getSASSessionController()
: getJSSessionController()
const session = await sessionController.getSession()
// marking consumed true, so that it's not available // marking consumed true, so that it's not available
// as readySession for any other request // as readySession for any other request
session.consumed = true session.consumed = true
req.sasSession = session req.sasjsSession = session
next() next()
} }

View File

@@ -17,12 +17,14 @@ import {
const execFilePromise = promisify(execFile) const execFilePromise = promisify(execFile)
export class SessionController { abstract class SessionController {
private sessions: Session[] = [] protected sessions: Session[] = []
private getReadySessions = (): Session[] => protected getReadySessions = (): Session[] =>
this.sessions.filter((sess: Session) => sess.ready && !sess.consumed) this.sessions.filter((sess: Session) => sess.ready && !sess.consumed)
protected abstract createSession(): Promise<Session>
public async getSession() { public async getSession() {
const readySessions = this.getReadySessions() const readySessions = this.getReadySessions()
@@ -34,8 +36,10 @@ export class SessionController {
return session return session
} }
}
private async createSession(): Promise<Session> { export class SASSessionController extends SessionController {
protected async createSession(): Promise<Session> {
const sessionId = generateUniqueFileName(generateTimestamp()) const sessionId = generateUniqueFileName(generateTimestamp())
const sessionFolder = path.join(getSessionsFolder(), sessionId) const sessionFolder = path.join(getSessionsFolder(), sessionId)
@@ -82,6 +86,8 @@ ${autoExecContent}`
// however we also need a promise so that we can update the // however we also need a promise so that we can update the
// session array to say that it has (eventually) finished. // session array to say that it has (eventually) finished.
// Additional windows specific options to avoid the desktop popups.
execFilePromise(process.sasLoc, [ execFilePromise(process.sasLoc, [
'-SYSIN', '-SYSIN',
codePath, codePath,
@@ -93,9 +99,9 @@ ${autoExecContent}`
session.path, session.path,
'-AUTOEXEC', '-AUTOEXEC',
autoExecPath, autoExecPath,
'-ENCODING', process.platform === 'win32' ? '-nosplash' : '',
'UTF-8', process.platform === 'win32' ? '-icon' : '',
process.platform === 'win32' ? '-nosplash' : '' process.platform === 'win32' ? '-nologo' : ''
]) ])
.then(() => { .then(() => {
session.completed = true session.completed = true
@@ -154,12 +160,52 @@ ${autoExecContent}`
} }
} }
export const getSessionController = (): SessionController => { export class JSSessionController extends SessionController {
if (process.sessionController) return process.sessionController protected async createSession(): Promise<Session> {
const sessionId = generateUniqueFileName(generateTimestamp())
const sessionFolder = path.join(getSessionsFolder(), sessionId)
process.sessionController = new SessionController() const creationTimeStamp = sessionId.split('-').pop() as string
// death time of session is 15 mins from creation
const deathTimeStamp = (
parseInt(creationTimeStamp) +
15 * 60 * 1000 -
1000
).toString()
return process.sessionController const session: Session = {
id: sessionId,
ready: true,
inUse: true,
consumed: false,
completed: false,
creationTimeStamp,
deathTimeStamp,
path: sessionFolder
}
const headersPath = path.join(session.path, 'stpsrv_header.txt')
await createFile(headersPath, 'Content-type: application/json')
this.sessions.push(session)
return session
}
}
export const getSASSessionController = (): SASSessionController => {
if (process.sasSessionController) return process.sasSessionController
process.sasSessionController = new SASSessionController()
return process.sasSessionController
}
export const getJSSessionController = (): JSSessionController => {
if (process.jsSessionController) return process.jsSessionController
process.jsSessionController = new JSSessionController()
return process.jsSessionController
} }
const autoExecContent = ` const autoExecContent = `

View File

@@ -0,0 +1,68 @@
import { PreProgramVars, Session } from '../../types'
import { generateFileUploadJSCode } from '../../utils'
import { ExecutionVars } from './'
export const createJSProgram = async (
program: string,
preProgramVariables: PreProgramVars,
vars: ExecutionVars,
session: Session,
weboutPath: string,
tokenFile: string,
otherArgs?: any
) => {
const varStatments = Object.keys(vars).reduce(
(computed: string, key: string) =>
`${computed}const ${key} = '${vars[key]}';\n`,
''
)
const preProgramVarStatments = `
let _webout = '';
const weboutPath = '${
process.platform === 'win32'
? weboutPath.replace(/\\/g, '\\\\')
: weboutPath
}';
const _sasjs_tokenfile = '${tokenFile}';
const _sasjs_username = '${preProgramVariables?.username}';
const _sasjs_userid = '${preProgramVariables?.userId}';
const _sasjs_displayname = '${preProgramVariables?.displayName}';
const _metaperson = _sasjs_displayname;
const _metauser = _sasjs_username;
const sasjsprocessmode = 'Stored Program';
`
const requiredModules = `const fs = require('fs')`
program = `
/* runtime vars */
${varStatments}
/* dynamic user-provided vars */
${preProgramVarStatments}
/* actual job code */
${program}
/* write webout file only if webout exists*/
if (_webout) {
fs.writeFile(weboutPath, _webout, function (err) {
if (err) throw err;
})
}
`
// if no files are uploaded filesNamesMap will be undefined
if (otherArgs?.filesNamesMap) {
const uploadJSCode = await generateFileUploadJSCode(
otherArgs.filesNamesMap,
session.path
)
//If js code for the file is generated it will be appended to the top of jsCode
if (uploadJSCode.length > 0) {
program = `${uploadJSCode}\n` + program
}
}
return requiredModules + program
}

View File

@@ -0,0 +1,69 @@
import { PreProgramVars, Session } from '../../types'
import { generateFileUploadSasCode, getMacrosFolder } from '../../utils'
import { ExecutionVars } from './'
export const createSASProgram = async (
program: string,
preProgramVariables: PreProgramVars,
vars: ExecutionVars,
session: Session,
weboutPath: string,
tokenFile: string,
otherArgs?: any
) => {
const varStatments = Object.keys(vars).reduce(
(computed: string, key: string) => `${computed}%let ${key}=${vars[key]};\n`,
''
)
const preProgramVarStatments = `
%let _sasjs_tokenfile=${tokenFile};
%let _sasjs_username=${preProgramVariables?.username};
%let _sasjs_userid=${preProgramVariables?.userId};
%let _sasjs_displayname=${preProgramVariables?.displayName};
%let _sasjs_apiserverurl=${preProgramVariables?.serverUrl};
%let _sasjs_apipath=/SASjsApi/stp/execute;
%let _metaperson=&_sasjs_displayname;
%let _metauser=&_sasjs_username;
%let sasjsprocessmode=Stored Program;
%let sasjs_stpsrv_header_loc=%sysfunc(pathname(work))/../stpsrv_header.txt;
%global SYSPROCESSMODE SYSTCPIPHOSTNAME SYSHOSTINFOLONG;
%macro _sasjs_server_init();
%if "&SYSPROCESSMODE"="" %then %let SYSPROCESSMODE=&sasjsprocessmode;
%if "&SYSTCPIPHOSTNAME"="" %then %let SYSTCPIPHOSTNAME=&_sasjs_apiserverurl;
%mend;
%_sasjs_server_init()
`
program = `
options insert=(SASAUTOS="${getMacrosFolder()}");
/* runtime vars */
${varStatments}
filename _webout "${weboutPath}" mod;
/* dynamic user-provided vars */
${preProgramVarStatments}
/* user autoexec starts */
${otherArgs?.userAutoExec ?? ''}
/* user autoexec ends */
/* actual job code */
${program}`
// if no files are uploaded filesNamesMap will be undefined
if (otherArgs?.filesNamesMap) {
const uploadSasCode = await generateFileUploadSasCode(
otherArgs.filesNamesMap,
session.path
)
//If sas code for the file is generated it will be appended to the top of sasCode
if (uploadSasCode.length > 0) {
program = `${uploadSasCode}` + program
}
}
return program
}

View File

@@ -2,3 +2,6 @@ export * from './deploy'
export * from './Session' export * from './Session'
export * from './Execution' export * from './Execution'
export * from './FileUploadController' export * from './FileUploadController'
export * from './createSASProgram'
export * from './createJSProgram'
export * from './processProgram'

View File

@@ -0,0 +1,86 @@
import path from 'path'
import fs from 'fs'
import { execFileSync } from 'child_process'
import { once } from 'stream'
import { createFile, moveFile } from '@sasjs/utils'
import { PreProgramVars, Session } from '../../types'
import { RunTimeType } from '../../utils'
import { ExecutionVars, createSASProgram, createJSProgram } from './'
export const processProgram = async (
program: string,
preProgramVariables: PreProgramVars,
vars: ExecutionVars,
session: Session,
weboutPath: string,
tokenFile: string,
runTime: RunTimeType,
logPath: string,
otherArgs?: any
) => {
if (runTime === RunTimeType.JS) {
program = await createJSProgram(
program,
preProgramVariables,
vars,
session,
weboutPath,
tokenFile,
otherArgs
)
const codePath = path.join(session.path, 'code.js')
try {
await createFile(codePath, program)
// create a stream that will write to console outputs to log file
const writeStream = fs.createWriteStream(logPath)
// waiting for the open event so that we can have underlying file descriptor
await once(writeStream, 'open')
execFileSync(process.nodeLoc, [codePath], {
stdio: ['ignore', writeStream, writeStream]
})
// copy the code.js program to log and end write stream
writeStream.end(program)
session.completed = true
console.log('session completed', session)
} catch (err: any) {
session.completed = true
session.crashed = err.toString()
console.log('session crashed', session.id, session.crashed)
}
} else {
program = await createSASProgram(
program,
preProgramVariables,
vars,
session,
weboutPath,
tokenFile,
otherArgs
)
const codePath = path.join(session.path, 'code.sas')
// Creating this file in a RUNNING session will break out
// the autoexec loop and actually execute the program
// but - given it will take several milliseconds to create
// (which can mean SAS trying to run a partial program, or
// failing due to file lock) we first create the file THEN
// we rename it.
await createFile(codePath + '.bkp', program)
await moveFile(codePath + '.bkp', codePath)
// we now need to poll the session status
while (!session.completed) {
await delay(50)
}
}
}
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))

View File

@@ -1,5 +1,4 @@
import express from 'express' import express from 'express'
import path from 'path'
import { import {
Request, Request,
Security, Security,
@@ -19,12 +18,12 @@ import {
} from './internal' } from './internal'
import { import {
getPreProgramVariables, getPreProgramVariables,
getFilesFolder,
HTTPHeaders, HTTPHeaders,
isDebugOn, isDebugOn,
LogLine, LogLine,
makeFilesNamesMap, makeFilesNamesMap,
parseLogToArray parseLogToArray,
getRunTimeAndFilePath
} from '../utils' } from '../utils'
import { MulterFile } from '../types/Upload' import { MulterFile } from '../types/Upload'
@@ -52,26 +51,15 @@ export interface ExecuteReturnJsonResponse {
@Tags('STP') @Tags('STP')
export class STPController { export class STPController {
/** /**
* Trigger a SAS program using it's location in the _program URL parameter. * Trigger a SAS or JS program using the _program URL parameter.
* Enable debugging using the _debug URL parameter. Setting _debug=131 will
* cause the log to be streamed in the output.
* *
* Additional URL parameters are turned into SAS macro variables. * Accepts URL parameters and file uploads. For more details, see docs:
* *
* Any files provided in the request body are placed into the SAS session with * https://server.sasjs.io/storedprograms
* corresponding _WEBIN_XXX variables created.
* *
* The response headers can be adjusted using the mfs_httpheader() macro. Any * @summary Execute a Stored Program, returns raw _webout content.
* file type can be returned, including binary files such as zip or xls. * @param _program Location of SAS or JS code
* * @example _program "/Projects/myApp/some/program"
* If _debug is >= 131, response headers will contain Content-Type: 'text/plain'
*
* This behaviour differs for POST requests, in which case the response is
* always JSON.
*
* @summary Execute Stored Program, return raw _webout content.
* @param _program Location of SAS program
* @example _program "/Public/somefolder/some.file"
*/ */
@Get('/execute') @Get('/execute')
public async executeReturnRaw( public async executeReturnRaw(
@@ -82,29 +70,22 @@ export class STPController {
} }
/** /**
* Trigger a SAS program using it's location in the _program URL parameter. * Trigger a SAS or JS program using the _program URL parameter.
* Enable debugging using the _debug URL parameter. In any case, the log is
* always returned in the log object.
* *
* Additional URL parameters are turned into SAS macro variables. * Accepts URL parameters and file uploads. For more details, see docs:
* *
* Any files provided in the request body are placed into the SAS session with * https://server.sasjs.io/storedprograms
* corresponding _WEBIN_XXX variables created.
* *
* The response will be a JSON object with the following root attributes: log, * The response will be a JSON object with the following root attributes:
* webout, headers. * log, webout, headers.
* *
* The webout will be a nested JSON object ONLY if the response-header * The webout attribute will be nested JSON ONLY if the response-header
* contains a content-type of application/json AND it is valid JSON. * contains a content-type of application/json AND it is valid JSON.
* Otherwise it will be a stringified version of the webout content. * Otherwise it will be a stringified version of the webout content.
* *
* Response headers from the mfs_httpheader macro are simply listed in the * @summary Execute a Stored Program, return a JSON object
* headers object, for POST requests they have no effect on the actual * @param _program Location of SAS or JS code
* response header. * @example _program "/Projects/myApp/some/program"
*
* @summary Execute Stored Program, return JSON
* @param _program Location of SAS program
* @example _program "/Public/somefolder/some.file"
*/ */
@Example<ExecuteReturnJsonResponse>({ @Example<ExecuteReturnJsonResponse>({
status: 'success', status: 'success',
@@ -131,18 +112,17 @@ const executeReturnRaw = async (
_program: string _program: string
): Promise<string | Buffer> => { ): Promise<string | Buffer> => {
const query = req.query as ExecutionVars const query = req.query as ExecutionVars
const sasCodePath =
path
.join(getFilesFolder(), _program)
.replace(new RegExp('/', 'g'), path.sep) + '.sas'
try { try {
const { codePath, runTime } = await getRunTimeAndFilePath(_program)
const { result, httpHeaders } = const { result, httpHeaders } =
(await new ExecutionController().executeFile( (await new ExecutionController().executeFile({
sasCodePath, programPath: codePath,
getPreProgramVariables(req), preProgramVariables: getPreProgramVariables(req),
query vars: query,
)) as ExecuteReturnRaw runTime
})) as ExecuteReturnRaw
// Should over-ride response header for debug // Should over-ride response header for debug
// on GET request to see entire log rendering on browser. // on GET request to see entire log rendering on browser.
@@ -171,25 +151,23 @@ const executeReturnJson = async (
req: express.Request, req: express.Request,
_program: string _program: string
): Promise<ExecuteReturnJsonResponse> => { ): Promise<ExecuteReturnJsonResponse> => {
const sasCodePath =
path
.join(getFilesFolder(), _program)
.replace(new RegExp('/', 'g'), path.sep) + '.sas'
const filesNamesMap = req.files?.length const filesNamesMap = req.files?.length
? makeFilesNamesMap(req.files as MulterFile[]) ? makeFilesNamesMap(req.files as MulterFile[])
: null : null
try { try {
const { codePath, runTime } = await getRunTimeAndFilePath(_program)
const { webout, log, httpHeaders } = const { webout, log, httpHeaders } =
(await new ExecutionController().executeFile( (await new ExecutionController().executeFile({
sasCodePath, programPath: codePath,
getPreProgramVariables(req), preProgramVariables: getPreProgramVariables(req),
{ ...req.query, ...req.body }, vars: { ...req.query, ...req.body },
{ filesNamesMap: filesNamesMap }, otherArgs: { filesNamesMap: filesNamesMap },
true, returnJson: true,
req.sasSession session: req.sasjsSession,
)) as ExecuteReturnJson runTime
})) as ExecuteReturnJson
let weboutRes: string | IRecordOfAny = webout let weboutRes: string | IRecordOfAny = webout
if (httpHeaders['content-type']?.toLowerCase() === 'application/json') { if (httpHeaders['content-type']?.toLowerCase() === 'application/json') {

View File

@@ -1,4 +1,5 @@
import mongoose, { Schema, model, Document, Model } from 'mongoose' import mongoose, { Schema, model, Document, Model } from 'mongoose'
import User from './User'
const AutoIncrement = require('mongoose-sequence')(mongoose) const AutoIncrement = require('mongoose-sequence')(mongoose)
export interface GroupPayload { export interface GroupPayload {
@@ -34,7 +35,8 @@ interface IGroupModel extends Model<IGroup> {}
const groupSchema = new Schema<IGroupDocument>({ const groupSchema = new Schema<IGroupDocument>({
name: { name: {
type: String, type: String,
required: true required: true,
unique: true
}, },
description: { description: {
type: String, type: String,
@@ -46,6 +48,7 @@ const groupSchema = new Schema<IGroupDocument>({
}, },
users: [{ type: Schema.Types.ObjectId, ref: 'User' }] users: [{ type: Schema.Types.ObjectId, ref: 'User' }]
}) })
groupSchema.plugin(AutoIncrement, { inc_field: 'groupId' }) groupSchema.plugin(AutoIncrement, { inc_field: 'groupId' })
// Hooks // Hooks
@@ -55,6 +58,17 @@ groupSchema.post('save', function (group: IGroup, next: Function) {
}) })
}) })
// pre remove hook to remove all references of group from users
groupSchema.pre('remove', async function () {
const userIds = this.users
await Promise.all(
userIds.map(async (userId) => {
const user = await User.findById(userId)
user?.removeGroup(this._id)
})
)
})
// Instance Methods // Instance Methods
groupSchema.method( groupSchema.method(
'addUser', 'addUser',

View File

@@ -1,5 +1,5 @@
import express from 'express' import express from 'express'
import { runSASValidation } from '../../utils' import { runCodeValidation } from '../../utils'
import { CodeController } from '../../controllers/' import { CodeController } from '../../controllers/'
const runRouter = express.Router() const runRouter = express.Router()
@@ -7,11 +7,11 @@ const runRouter = express.Router()
const controller = new CodeController() const controller = new CodeController()
runRouter.post('/execute', async (req, res) => { runRouter.post('/execute', async (req, res) => {
const { error, value: body } = runSASValidation(req.body) const { error, value: body } = runCodeValidation(req.body)
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.executeSASCode(req, body) const response = await controller.executeCode(req, body)
if (response instanceof Buffer) { if (response instanceof Buffer) {
res.writeHead(200, (req as any).sasHeaders) res.writeHead(200, (req as any).sasHeaders)

View File

@@ -1,7 +1,7 @@
import express from 'express' import express from 'express'
import { GroupController } from '../../controllers/' import { GroupController } from '../../controllers/'
import { authenticateAccessToken, verifyAdmin } from '../../middlewares' import { authenticateAccessToken, verifyAdmin } from '../../middlewares'
import { registerGroupValidation } from '../../utils' import { getGroupValidation, registerGroupValidation } from '../../utils'
const groupRouter = express.Router() const groupRouter = express.Router()
@@ -18,7 +18,11 @@ groupRouter.post(
const response = await controller.createGroup(body) const response = await controller.createGroup(body)
res.send(response) res.send(response)
} catch (err: any) { } catch (err: any) {
res.status(403).send(err.toString()) const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
} }
} }
) )
@@ -29,7 +33,11 @@ groupRouter.get('/', authenticateAccessToken, async (req, res) => {
const response = await controller.getAllGroups() const response = await controller.getAllGroups()
res.send(response) res.send(response)
} catch (err: any) { } catch (err: any) {
res.status(403).send(err.toString()) const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
} }
}) })
@@ -41,10 +49,37 @@ groupRouter.get('/:groupId', authenticateAccessToken, async (req, res) => {
const response = await controller.getGroup(parseInt(groupId)) const response = await controller.getGroup(parseInt(groupId))
res.send(response) res.send(response)
} catch (err: any) { } catch (err: any) {
res.status(403).send(err.toString()) const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
} }
}) })
groupRouter.get(
'/by/groupname/:name',
authenticateAccessToken,
async (req, res) => {
const { error, value: params } = getGroupValidation(req.params)
if (error) return res.status(400).send(error.details[0].message)
const { name } = params
const controller = new GroupController()
try {
const response = await controller.getGroupByGroupName(name)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
}
}
)
groupRouter.post( groupRouter.post(
'/:groupId/:userId', '/:groupId/:userId',
authenticateAccessToken, authenticateAccessToken,
@@ -60,7 +95,11 @@ groupRouter.post(
) )
res.send(response) res.send(response)
} catch (err: any) { } catch (err: any) {
res.status(403).send(err.toString()) const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
} }
} }
) )
@@ -80,7 +119,11 @@ groupRouter.delete(
) )
res.send(response) res.send(response)
} catch (err: any) { } catch (err: any) {
res.status(403).send(err.toString()) const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
} }
} }
) )
@@ -97,7 +140,11 @@ groupRouter.delete(
await controller.deleteGroup(parseInt(groupId)) await controller.deleteGroup(parseInt(groupId))
res.status(200).send('Group Deleted!') res.status(200).send('Group Deleted!')
} catch (err: any) { } catch (err: any) {
res.status(403).send(err.toString()) const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
} }
} }
) )

View File

@@ -23,7 +23,7 @@ const user = {
} }
const group = { const group = {
name: 'DCGroup1', name: 'dcgroup1',
description: 'DC group for testing purposes.' description: 'DC group for testing purposes.'
} }
@@ -70,6 +70,32 @@ describe('group', () => {
expect(res.body.users).toEqual([]) expect(res.body.users).toEqual([])
}) })
it('should respond with Conflict when group already exists with same name', async () => {
await groupController.createGroup(group)
const res = await request(app)
.post('/SASjsApi/group')
.auth(adminAccessToken, { type: 'bearer' })
.send(group)
.expect(409)
expect(res.text).toEqual('Group name already exists.')
expect(res.body).toEqual({})
})
it('should respond with Bad Request when group name does not match the group name schema', async () => {
const res = await request(app)
.post('/SASjsApi/group')
.auth(adminAccessToken, { type: 'bearer' })
.send({ ...group, name: 'Wrong Group Name' })
.expect(400)
expect(res.text).toEqual(
'"name" must only contain alpha-numeric characters'
)
expect(res.body).toEqual({})
})
it('should respond with Unauthorized if access token is not present', async () => { it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app).post('/SASjsApi/group').send().expect(401) const res = await request(app).post('/SASjsApi/group').send().expect(401)
@@ -125,14 +151,51 @@ describe('group', () => {
expect(res.body).toEqual({}) expect(res.body).toEqual({})
}) })
it('should respond with Forbidden if groupId is incorrect', async () => { it(`should delete group's reference from users' groups array`, async () => {
const dbGroup = await groupController.createGroup(group)
const dbUser1 = await userController.createUser({
...user,
username: 'deletegroup1'
})
const dbUser2 = await userController.createUser({
...user,
username: 'deletegroup2'
})
await groupController.addUserToGroup(dbGroup.groupId, dbUser1.id)
await groupController.addUserToGroup(dbGroup.groupId, dbUser2.id)
await request(app)
.delete(`/SASjsApi/group/${dbGroup.groupId}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
const res1 = await request(app)
.get(`/SASjsApi/user/${dbUser1.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
expect(res1.body.groups).toEqual([])
const res2 = await request(app)
.get(`/SASjsApi/user/${dbUser2.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
expect(res2.body.groups).toEqual([])
})
it('should respond with Not Found if groupId is incorrect', async () => {
const res = await request(app) const res = await request(app)
.delete(`/SASjsApi/group/1234`) .delete(`/SASjsApi/group/1234`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(403) .expect(404)
expect(res.text).toEqual('Error: No Group deleted!') expect(res.text).toEqual('Group not found.')
expect(res.body).toEqual({}) expect(res.body).toEqual({})
}) })
@@ -216,16 +279,76 @@ describe('group', () => {
expect(res.body).toEqual({}) expect(res.body).toEqual({})
}) })
it('should respond with Forbidden if groupId is incorrect', async () => { it('should respond with Not Found if groupId is incorrect', async () => {
const res = await request(app) const res = await request(app)
.get('/SASjsApi/group/1234') .get('/SASjsApi/group/1234')
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(403) .expect(404)
expect(res.text).toEqual('Error: Group not found.') expect(res.text).toEqual('Group not found.')
expect(res.body).toEqual({}) expect(res.body).toEqual({})
}) })
describe('by group name', () => {
it('should respond with group', async () => {
const { name } = await groupController.createGroup(group)
const res = await request(app)
.get(`/SASjsApi/group/by/groupname/${name}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
expect(res.body.groupId).toBeTruthy()
expect(res.body.name).toEqual(group.name)
expect(res.body.description).toEqual(group.description)
expect(res.body.isActive).toEqual(true)
expect(res.body.users).toEqual([])
})
it('should respond with group when access token is not of an admin account', async () => {
const accessToken = await generateSaveTokenAndCreateUser({
...user,
username: 'getbyname' + user.username
})
const { name } = await groupController.createGroup(group)
const res = await request(app)
.get(`/SASjsApi/group/by/groupname/${name}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(200)
expect(res.body.groupId).toBeTruthy()
expect(res.body.name).toEqual(group.name)
expect(res.body.description).toEqual(group.description)
expect(res.body.isActive).toEqual(true)
expect(res.body.users).toEqual([])
})
it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app)
.get('/SASjsApi/group/by/groupname/dcgroup')
.send()
.expect(401)
expect(res.text).toEqual('Unauthorized')
expect(res.body).toEqual({})
})
it('should respond with Not Found if groupname is incorrect', async () => {
const res = await request(app)
.get('/SASjsApi/group/by/groupname/randomCharacters')
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(404)
expect(res.text).toEqual('Group not found.')
expect(res.body).toEqual({})
})
})
}) })
describe('getAll', () => { describe('getAll', () => {
@@ -245,8 +368,8 @@ describe('group', () => {
expect(res.body).toEqual([ expect(res.body).toEqual([
{ {
groupId: expect.anything(), groupId: expect.anything(),
name: 'DCGroup1', name: group.name,
description: 'DC group for testing purposes.' description: group.description
} }
]) ])
}) })
@@ -267,8 +390,8 @@ describe('group', () => {
expect(res.body).toEqual([ expect(res.body).toEqual([
{ {
groupId: expect.anything(), groupId: expect.anything(),
name: 'DCGroup1', name: group.name,
description: 'DC group for testing purposes.' description: group.description
} }
]) ])
}) })
@@ -309,6 +432,34 @@ describe('group', () => {
]) ])
}) })
it(`should add group to user's groups array`, async () => {
const dbGroup = await groupController.createGroup(group)
const dbUser = await userController.createUser({
...user,
username: 'addUserToGroup'
})
await request(app)
.post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
const res = await request(app)
.get(`/SASjsApi/user/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
expect(res.body.groups).toEqual([
{
groupId: expect.anything(),
name: group.name,
description: group.description
}
])
})
it('should respond with group without duplicating user', async () => { it('should respond with group without duplicating user', async () => {
const dbGroup = await groupController.createGroup(group) const dbGroup = await groupController.createGroup(group)
const dbUser = await userController.createUser({ const dbUser = await userController.createUser({
@@ -362,26 +513,26 @@ describe('group', () => {
expect(res.body).toEqual({}) expect(res.body).toEqual({})
}) })
it('should respond with Forbidden if groupId is incorrect', async () => { it('should respond with Not Found if groupId is incorrect', async () => {
const res = await request(app) const res = await request(app)
.post('/SASjsApi/group/123/123') .post('/SASjsApi/group/123/123')
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(403) .expect(404)
expect(res.text).toEqual('Error: Group not found.') expect(res.text).toEqual('Group not found.')
expect(res.body).toEqual({}) expect(res.body).toEqual({})
}) })
it('should respond with Forbidden if userId is incorrect', async () => { it('should respond with Not Found if userId is incorrect', async () => {
const dbGroup = await groupController.createGroup(group) const dbGroup = await groupController.createGroup(group)
const res = await request(app) const res = await request(app)
.post(`/SASjsApi/group/${dbGroup.groupId}/123`) .post(`/SASjsApi/group/${dbGroup.groupId}/123`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(403) .expect(404)
expect(res.text).toEqual('Error: User not found.') expect(res.text).toEqual('User not found.')
expect(res.body).toEqual({}) expect(res.body).toEqual({})
}) })
}) })
@@ -412,6 +563,29 @@ describe('group', () => {
expect(res.body.users).toEqual([]) expect(res.body.users).toEqual([])
}) })
it(`should remove group from user's groups array`, async () => {
const dbGroup = await groupController.createGroup(group)
const dbUser = await userController.createUser({
...user,
username: 'removeGroupFromUser'
})
await groupController.addUserToGroup(dbGroup.groupId, dbUser.id)
await request(app)
.delete(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
const res = await request(app)
.get(`/SASjsApi/user/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
expect(res.body.groups).toEqual([])
})
it('should respond with Unauthorized if access token is not present', async () => { it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app) const res = await request(app)
.delete('/SASjsApi/group/123/123') .delete('/SASjsApi/group/123/123')
@@ -438,26 +612,26 @@ describe('group', () => {
expect(res.body).toEqual({}) expect(res.body).toEqual({})
}) })
it('should respond with Forbidden if groupId is incorrect', async () => { it('should respond with Not Found if groupId is incorrect', async () => {
const res = await request(app) const res = await request(app)
.delete('/SASjsApi/group/123/123') .delete('/SASjsApi/group/123/123')
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(403) .expect(404)
expect(res.text).toEqual('Error: Group not found.') expect(res.text).toEqual('Group not found.')
expect(res.body).toEqual({}) expect(res.body).toEqual({})
}) })
it('should respond with Forbidden if userId is incorrect', async () => { it('should respond with Not Found if userId is incorrect', async () => {
const dbGroup = await groupController.createGroup(group) const dbGroup = await groupController.createGroup(group)
const res = await request(app) const res = await request(app)
.delete(`/SASjsApi/group/${dbGroup.groupId}/123`) .delete(`/SASjsApi/group/${dbGroup.groupId}/123`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(403) .expect(404)
expect(res.text).toEqual('Error: User not found.') expect(res.text).toEqual('User not found.')
expect(res.body).toEqual({}) expect(res.body).toEqual({})
}) })
}) })

View File

@@ -0,0 +1,383 @@
import path from 'path'
import { Express } from 'express'
import mongoose, { Mongoose } from 'mongoose'
import { MongoMemoryServer } from 'mongodb-memory-server'
import request from 'supertest'
import appPromise from '../../../app'
import { UserController } from '../../../controllers/'
import {
generateAccessToken,
saveTokensInDB,
getFilesFolder,
RunTimeType,
generateUniqueFileName,
getSessionsFolder
} from '../../../utils'
import { createFile, generateTimestamp, deleteFolder } from '@sasjs/utils'
import {
SASSessionController,
JSSessionController
} from '../../../controllers/internal'
import * as ProcessProgramModule from '../../../controllers/internal/processProgram'
import { Session } from '../../../types'
const clientId = 'someclientID'
const user = {
displayName: 'Test User',
username: 'testUsername',
password: '87654321',
isAdmin: false,
isActive: true
}
const sampleSasProgram = '%put hello world!;'
const sampleJsProgram = `console.log('hello world!/')`
const filesFolder = getFilesFolder()
describe('stp', () => {
let app: Express
let con: Mongoose
let mongoServer: MongoMemoryServer
let accessToken: string
beforeAll(async () => {
app = await appPromise
mongoServer = await MongoMemoryServer.create()
con = await mongoose.connect(mongoServer.getUri())
accessToken = await generateSaveTokenAndCreateUser(user)
})
afterAll(async () => {
await con.connection.dropDatabase()
await con.connection.close()
await mongoServer.stop()
})
describe('execute', () => {
const testFilesFolder = `test-stp-${generateTimestamp()}`
describe('get', () => {
describe('with runtime js', () => {
const testFilesFolder = `test-stp-${generateTimestamp()}`
beforeAll(() => {
process.runTimes = [RunTimeType.JS]
})
beforeEach(() => {
jest.resetModules() // it clears the cache
setupMocks()
})
afterEach(async () => {
jest.resetAllMocks()
await deleteFolder(path.join(filesFolder, testFilesFolder))
})
it('should execute js program when both js and sas program are present', async () => {
const programPath = path.join(testFilesFolder, 'program')
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
await createFile(sasProgramPath, sampleSasProgram)
await createFile(jsProgramPath, sampleJsProgram)
await request(app)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(200)
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
RunTimeType.JS,
expect.anything(),
undefined
)
})
it('should throw error when js program is not present but sas program exists', async () => {
const programPath = path.join(testFilesFolder, 'program')
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
await createFile(sasProgramPath, sampleSasProgram)
await request(app)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(400)
})
})
describe('with runtime sas', () => {
beforeAll(() => {
process.runTimes = [RunTimeType.SAS]
})
beforeEach(() => {
jest.resetModules() // it clears the cache
setupMocks()
})
afterEach(async () => {
jest.resetAllMocks()
await deleteFolder(path.join(filesFolder, testFilesFolder))
})
it('should execute sas program when both sas and js programs are present', async () => {
const programPath = path.join(testFilesFolder, 'program')
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
await createFile(sasProgramPath, sampleSasProgram)
await createFile(jsProgramPath, sampleJsProgram)
await request(app)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(200)
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
RunTimeType.SAS,
expect.anything(),
undefined
)
})
it('should throw error when sas program do not exit but js exists', async () => {
const programPath = path.join(testFilesFolder, 'program')
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
await createFile(jsProgramPath, sampleJsProgram)
await request(app)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(400)
})
})
describe('with runtime js and sas', () => {
beforeAll(() => {
process.runTimes = [RunTimeType.JS, RunTimeType.SAS]
})
beforeEach(() => {
jest.resetModules() // it clears the cache
setupMocks()
})
afterEach(async () => {
jest.resetAllMocks()
await deleteFolder(path.join(filesFolder, testFilesFolder))
})
it('should execute js program when both js and sas program are present', async () => {
const programPath = path.join(testFilesFolder, 'program')
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
await createFile(sasProgramPath, sampleSasProgram)
await createFile(jsProgramPath, sampleJsProgram)
await request(app)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(200)
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
RunTimeType.JS,
expect.anything(),
undefined
)
})
it('should execute sas program when js program is not present but sas program exists', async () => {
const programPath = path.join(testFilesFolder, 'program')
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
await createFile(sasProgramPath, sampleSasProgram)
await request(app)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(200)
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
RunTimeType.SAS,
expect.anything(),
undefined
)
})
it('should throw error when both sas and js programs do not exist', async () => {
const programPath = path.join(testFilesFolder, 'program')
await request(app)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(400)
})
})
describe('with runtime sas and js', () => {
beforeAll(() => {
process.runTimes = [RunTimeType.SAS, RunTimeType.JS]
})
beforeEach(() => {
jest.resetModules() // it clears the cache
setupMocks()
})
afterEach(async () => {
jest.resetAllMocks()
await deleteFolder(path.join(filesFolder, testFilesFolder))
})
it('should execute sas program when both sas and js programs exist', async () => {
const programPath = path.join(testFilesFolder, 'program')
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
await createFile(sasProgramPath, sampleSasProgram)
await createFile(jsProgramPath, sampleJsProgram)
await request(app)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(200)
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
RunTimeType.SAS,
expect.anything(),
undefined
)
})
it('should execute js program when sas program is not present but js program exists', async () => {
const programPath = path.join(testFilesFolder, 'program')
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
await createFile(jsProgramPath, sampleJsProgram)
await request(app)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(200)
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
RunTimeType.JS,
expect.anything(),
undefined
)
})
it('should throw error when both sas and js programs do not exist', async () => {
const programPath = path.join(testFilesFolder, 'program')
await request(app)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(400)
})
})
})
})
})
const generateSaveTokenAndCreateUser = async (
someUser: any
): Promise<string> => {
const userController = new UserController()
const dbUser = await userController.createUser(someUser)
return generateAndSaveToken(dbUser.id)
}
const generateAndSaveToken = async (userId: number) => {
const accessToken = generateAccessToken({
clientId,
userId
})
await saveTokensInDB(userId, clientId, accessToken, 'refreshToken')
return accessToken
}
const setupMocks = async () => {
jest
.spyOn(SASSessionController.prototype, 'getSession')
.mockImplementation(mockedGetSession)
jest
.spyOn(JSSessionController.prototype, 'getSession')
.mockImplementation(mockedGetSession)
jest
.spyOn(ProcessProgramModule, 'processProgram')
.mockImplementation(() => Promise.resolve())
}
const mockedGetSession = async () => {
const sessionId = generateUniqueFileName(generateTimestamp())
const sessionFolder = path.join(getSessionsFolder(), sessionId)
const creationTimeStamp = sessionId.split('-').pop() as string
// death time of session is 15 mins from creation
const deathTimeStamp = (
parseInt(creationTimeStamp) +
15 * 60 * 1000 -
1000
).toString()
const session: Session = {
id: sessionId,
ready: true,
inUse: true,
consumed: false,
completed: false,
creationTimeStamp,
deathTimeStamp,
path: sessionFolder
}
return session
}

View File

@@ -35,16 +35,17 @@ stpRouter.post(
fileUploadController.preUploadMiddleware, fileUploadController.preUploadMiddleware,
fileUploadController.getMulterUploadObject().any(), fileUploadController.getMulterUploadObject().any(),
async (req, res: any) => { async (req, res: any) => {
const { error: errQ, value: query } = executeProgramRawValidation(req.query) // below validations are moved to preUploadMiddleware
const { error: errB, value: body } = executeProgramRawValidation(req.body) // const { error: errQ, value: query } = executeProgramRawValidation(req.query)
// const { error: errB, value: body } = executeProgramRawValidation(req.body)
if (errQ && errB) return res.status(400).send(errB.details[0].message) // if (errQ && errB) return res.status(400).send(errB.details[0].message)
try { try {
const response = await controller.executeReturnJson( const response = await controller.executeReturnJson(
req, req,
body, req.body,
query?._program req.query?._program as string
) )
// TODO: investigate if this code is required // TODO: investigate if this code is required

View File

@@ -2,6 +2,6 @@ declare namespace Express {
export interface Request { export interface Request {
accessToken?: string accessToken?: string
user?: import('../').RequestUser user?: import('../').RequestUser
sasSession?: import('../').Session sasjsSession?: import('../').Session
} }
} }

View File

@@ -1,9 +1,12 @@
declare namespace NodeJS { declare namespace NodeJS {
export interface Process { export interface Process {
sasLoc: string sasLoc: string
nodeLoc: string
driveLoc: string driveLoc: string
sessionController?: import('../../controllers/internal').SessionController sasSessionController?: import('../../controllers/internal').SASSessionController
jsSessionController?: import('../../controllers/internal').JSSessionController
appStreamConfig: import('../').AppStreamConfig appStreamConfig: import('../').AppStreamConfig
logger: import('@sasjs/utils/logger').Logger logger: import('@sasjs/utils/logger').Logger
runTimes: import('../../utils').RunTimeType[]
} }
} }

View File

@@ -5,12 +5,13 @@ import { createFolder, fileExists, folderExists } from '@sasjs/utils'
const isWindows = () => process.platform === 'win32' const isWindows = () => process.platform === 'win32'
export const getDesktopFields = async () => { export const getDesktopFields = async () => {
const { SAS_PATH } = process.env const { SAS_PATH, NODE_PATH } = process.env
const sasLoc = SAS_PATH ?? (await getSASLocation()) const sasLoc = SAS_PATH ?? (await getSASLocation())
const nodeLoc = NODE_PATH ?? (await getNodeLocation())
// const driveLoc = DRIVE_PATH ?? (await getDriveLocation()) // const driveLoc = DRIVE_PATH ?? (await getDriveLocation())
return { sasLoc } return { sasLoc, nodeLoc }
} }
const getDriveLocation = async (): Promise<string> => { const getDriveLocation = async (): Promise<string> => {
@@ -61,3 +62,27 @@ const getSASLocation = async (): Promise<string> => {
return targetName return targetName
} }
const getNodeLocation = async (): Promise<string> => {
const validator = async (filePath: string) => {
if (!filePath) return 'Path to NodeJS executable is required.'
if (!(await fileExists(filePath))) {
return 'No file found at provided path.'
}
return true
}
const defaultLocation = isWindows()
? 'C:\\Program Files\\nodejs\\'
: '/usr/local/nodejs/bin'
const targetName = await getString(
'Please enter path to nodejs executable (absolute path): ',
validator,
defaultLocation
)
return targetName
}

View File

@@ -0,0 +1,37 @@
import path from 'path'
import { fileExists } from '@sasjs/utils'
import { getFilesFolder } from './file'
import { RunTimeType } from '.'
export const getRunTimeAndFilePath = async (programPath: string) => {
const ext = path.extname(programPath)
// if program path is provided with extension we should split that into code path and ext as run time
if (ext) {
const runTime = ext.slice(1)
const runTimeTypes = Object.values(RunTimeType)
if (!runTimeTypes.includes(runTime as RunTimeType)) {
throw `The '${runTime}' runtime is not supported.`
}
const codePath = path
.join(getFilesFolder(), programPath)
.replace(new RegExp('/', 'g'), path.sep)
if (await fileExists(codePath)) {
return { codePath, runTime: runTime as RunTimeType }
}
} else {
for (const runTime of process.runTimes) {
const codePath =
path
.join(getFilesFolder(), programPath)
.replace(new RegExp('/', 'g'), path.sep) +
'.' +
runTime
if (await fileExists(codePath)) return { codePath, runTime }
}
}
throw `The Program at (${programPath}) does not exist.`
}

View File

@@ -11,6 +11,7 @@ export * from './generateRefreshToken'
export * from './getCertificates' export * from './getCertificates'
export * from './getDesktopFields' export * from './getDesktopFields'
export * from './getPreProgramVariables' export * from './getPreProgramVariables'
export * from './getRunTimeAndFilePath'
export * from './getServerUrl' export * from './getServerUrl'
export * from './instantiateLogger' export * from './instantiateLogger'
export * from './isDebugOn' export * from './isDebugOn'

View File

@@ -1,7 +1,7 @@
import path from 'path' import path from 'path'
import { createFolder, getAbsolutePath, getRealPath } from '@sasjs/utils' import { createFolder, getAbsolutePath, getRealPath } from '@sasjs/utils'
import { getDesktopFields, ModeType } from '.' import { getDesktopFields, ModeType, RunTimeType } from '.'
export const setProcessVariables = async () => { export const setProcessVariables = async () => {
if (process.env.NODE_ENV === 'test') { if (process.env.NODE_ENV === 'test') {
@@ -13,10 +13,12 @@ export const setProcessVariables = async () => {
if (MODE === ModeType.Server) { if (MODE === ModeType.Server) {
process.sasLoc = process.env.SAS_PATH as string process.sasLoc = process.env.SAS_PATH as string
process.nodeLoc = process.env.NODE_PATH as string
} else { } else {
const { sasLoc } = await getDesktopFields() const { sasLoc, nodeLoc } = await getDesktopFields()
process.sasLoc = sasLoc process.sasLoc = sasLoc
process.nodeLoc = nodeLoc
} }
const { SASJS_ROOT } = process.env const { SASJS_ROOT } = process.env
@@ -24,6 +26,10 @@ export const setProcessVariables = async () => {
await createFolder(absPath) await createFolder(absPath)
process.driveLoc = getRealPath(absPath) process.driveLoc = getRealPath(absPath)
const { RUN_TIMES } = process.env
process.runTimes = (RUN_TIMES as string).split(',') as RunTimeType[]
console.log('sasLoc: ', process.sasLoc) console.log('sasLoc: ', process.sasLoc)
console.log('sasDrive: ', process.driveLoc) console.log('sasDrive: ', process.driveLoc)
console.log('runTimes: ', process.runTimes)
} }

View File

@@ -1,5 +1,6 @@
import path from 'path'
import { MulterFile } from '../types/Upload' import { MulterFile } from '../types/Upload'
import { listFilesInFolder } from '@sasjs/utils' import { listFilesInFolder, readFileBinary } from '@sasjs/utils'
interface FilenameMapSingle { interface FilenameMapSingle {
fieldName: string fieldName: string
@@ -98,3 +99,34 @@ export const generateFileUploadSasCode = async (
return uploadSasCode return uploadSasCode
} }
/**
* Generates the js code that references uploaded files in the concurrent request
* @param filesNamesMap object that maps hashed file names and original file names
* @param sessionFolder name of the folder that is created for the purpose of files in concurrent request
* @returns generated js code
*/
export const generateFileUploadJSCode = async (
filesNamesMap: FilenamesMap,
sessionFolder: string
) => {
let uploadCode = ''
let fileCount = 0
const sessionFolderList: string[] = await listFilesInFolder(sessionFolder)
sessionFolderList.forEach(async (fileName) => {
if (fileName.includes('req_file')) {
fileCount++
const filePath = path.join(sessionFolder, fileName)
uploadCode += `\nconst _WEBIN_FILEREF${fileCount} = fs.readFileSync('${filePath}')`
uploadCode += `\nconst _WEBIN_FILENAME${fileCount} = '${filesNamesMap[fileName].originalName}'`
uploadCode += `\nconst _WEBIN_NAME${fileCount} = '${filesNamesMap[fileName].fieldName}'`
}
})
if (fileCount) {
uploadCode = `\nconst _WEBIN_FILE_COUNT = ${fileCount}` + uploadCode
}
return uploadCode
}

View File

@@ -1,7 +1,9 @@
import Joi from 'joi' import Joi from 'joi'
import { RunTimeType } from '.'
const usernameSchema = Joi.string().lowercase().alphanum().min(3).max(16) const usernameSchema = Joi.string().lowercase().alphanum().min(3).max(16)
const passwordSchema = Joi.string().min(6).max(1024) const passwordSchema = Joi.string().min(6).max(1024)
const groupnameSchema = Joi.string().lowercase().alphanum().min(3).max(16)
export const blockFileRegex = /\.(exe|sh|htaccess)$/i export const blockFileRegex = /\.(exe|sh|htaccess)$/i
@@ -29,11 +31,16 @@ export const tokenValidation = (data: any): Joi.ValidationResult =>
export const registerGroupValidation = (data: any): Joi.ValidationResult => export const registerGroupValidation = (data: any): Joi.ValidationResult =>
Joi.object({ Joi.object({
name: Joi.string().min(6).required(), name: groupnameSchema.required(),
description: Joi.string(), description: Joi.string(),
isActive: Joi.boolean() isActive: Joi.boolean()
}).validate(data) }).validate(data)
export const getGroupValidation = (data: any): Joi.ValidationResult =>
Joi.object({
name: groupnameSchema.required()
}).validate(data)
export const registerUserValidation = (data: any): Joi.ValidationResult => export const registerUserValidation = (data: any): Joi.ValidationResult =>
Joi.object({ Joi.object({
displayName: Joi.string().min(6).required(), displayName: Joi.string().min(6).required(),
@@ -114,9 +121,10 @@ export const folderParamValidation = (data: any): Joi.ValidationResult =>
_folderPath: Joi.string() _folderPath: Joi.string()
}).validate(data) }).validate(data)
export const runSASValidation = (data: any): Joi.ValidationResult => export const runCodeValidation = (data: any): Joi.ValidationResult =>
Joi.object({ Joi.object({
code: Joi.string().required() code: Joi.string().required(),
runTime: Joi.string().valid(...process.runTimes)
}).validate(data) }).validate(data)
export const executeProgramRawValidation = (data: any): Joi.ValidationResult => export const executeProgramRawValidation = (data: any): Joi.ValidationResult =>

View File

@@ -26,6 +26,11 @@ export enum LOG_FORMAT_MORGANType {
tiny = 'tiny' tiny = 'tiny'
} }
export enum RunTimeType {
SAS = 'sas',
JS = 'js'
}
export enum ReturnCode { export enum ReturnCode {
Success, Success,
InvalidEnv InvalidEnv
@@ -46,6 +51,10 @@ export const verifyEnvVariables = (): ReturnCode => {
errors.push(...verifyLOG_FORMAT_MORGAN()) errors.push(...verifyLOG_FORMAT_MORGAN())
errors.push(...verifyRUN_TIMES())
errors.push(...verifyExecutablePaths())
if (errors.length) { if (errors.length) {
process.logger?.error( process.logger?.error(
`Invalid environment variable(s) provided: \n${errors.join('\n')}` `Invalid environment variable(s) provided: \n${errors.join('\n')}`
@@ -202,10 +211,52 @@ const verifyLOG_FORMAT_MORGAN = (): string[] => {
return errors return errors
} }
const verifyRUN_TIMES = (): string[] => {
const errors: string[] = []
const { RUN_TIMES } = process.env
if (RUN_TIMES) {
const runTimes = RUN_TIMES.split(',')
const runTimeTypes = Object.values(RunTimeType)
runTimes.forEach((runTime) => {
if (!runTimeTypes.includes(runTime as RunTimeType)) {
errors.push(
`- Invalid '${runTime}' runtime\n - valid options ${runTimeTypes}`
)
}
})
} else {
process.env.RUN_TIMES = DEFAULTS.RUN_TIMES
}
return errors
}
const verifyExecutablePaths = () => {
const errors: string[] = []
const { RUN_TIMES, SAS_PATH, NODE_PATH, MODE } = process.env
if (MODE === ModeType.Server) {
const runTimes = RUN_TIMES?.split(',')
if (runTimes?.includes(RunTimeType.SAS) && !SAS_PATH) {
errors.push(`- SAS_PATH is required for ${RunTimeType.SAS} run time`)
}
if (runTimes?.includes(RunTimeType.JS) && !NODE_PATH) {
errors.push(`- NODE_PATH is required for ${RunTimeType.JS} run time`)
}
}
return errors
}
const DEFAULTS = { const DEFAULTS = {
MODE: ModeType.Desktop, MODE: ModeType.Desktop,
PROTOCOL: ProtocolType.HTTP, PROTOCOL: ProtocolType.HTTP,
PORT: '5000', PORT: '5000',
HELMET_COEP: HelmetCoepType.TRUE, HELMET_COEP: HelmetCoepType.TRUE,
LOG_FORMAT_MORGAN: LOG_FORMAT_MORGANType.Common LOG_FORMAT_MORGAN: LOG_FORMAT_MORGANType.Common,
RUN_TIMES: `${RunTimeType.SAS}`
} }

View File

@@ -1 +1 @@
v16.14.0 v16.15.1

View File

@@ -9,8 +9,8 @@ const Home = () => {
<CssBaseline /> <CssBaseline />
<h2>Welcome to SASjs Server!</h2> <h2>Welcome to SASjs Server!</h2>
<p> <p>
This portal provides an interface for executing Stored Programs (drive) SASjs Server provides a REST interface for executing Stored Programs and
and ad hoc code (studio) against a SAS executable. The source code is ad hoc code (studio) against SAS and JS executables. The source is
available on{' '} available on{' '}
<a <a
href="https://github.com/sasjs/server" href="https://github.com/sasjs/server"

View File

@@ -94,10 +94,7 @@ const Main = (props: Props) => {
setEditMode(false) setEditMode(false)
} else { } else {
window.open( window.open(
`${baseUrl}/SASjsApi/stp/execute?_program=${props.selectedFilePath.replace( `${baseUrl}/SASjsApi/stp/execute?_program=${props.selectedFilePath}`
/.sas$/,
''
)}`
) )
} }
} }

View File

@@ -1,13 +1,24 @@
import React, { useEffect, useRef, useState } from 'react' import React, { useEffect, useRef, useState, useContext } from 'react'
import axios from 'axios' import axios from 'axios'
import Box from '@mui/material/Box' import {
import { Button, Paper, Stack, Tab, Tooltip } from '@mui/material' Box,
MenuItem,
FormControl,
Select,
SelectChangeEvent,
Button,
Paper,
Tab,
Tooltip
} from '@mui/material'
import { makeStyles } from '@mui/styles' import { makeStyles } from '@mui/styles'
import Editor, { EditorDidMount } from 'react-monaco-editor' import Editor, { EditorDidMount } from 'react-monaco-editor'
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
import { TabContext, TabList, TabPanel } from '@mui/lab' import { TabContext, TabList, TabPanel } from '@mui/lab'
import { AppContext, RunTimeType } from '../../context/appContext'
const useStyles = makeStyles(() => ({ const useStyles = makeStyles(() => ({
root: { root: {
fontSize: '1rem', fontSize: '1rem',
@@ -30,12 +41,23 @@ const useStyles = makeStyles(() => ({
})) }))
const Studio = () => { const Studio = () => {
const appContext = useContext(AppContext)
const location = useLocation() const location = useLocation()
const [fileContent, setFileContent] = useState('') const [fileContent, setFileContent] = useState('')
const [log, setLog] = useState('') const [log, setLog] = useState('')
const [ctrlPressed, setCtrlPressed] = useState(false) const [ctrlPressed, setCtrlPressed] = useState(false)
const [webout, setWebout] = useState('') const [webout, setWebout] = useState('')
const [tab, setTab] = React.useState('1') const [tab, setTab] = useState('1')
const [runTimes, setRunTimes] = useState<string[]>([])
const [selectedRunTime, setSelectedRunTime] = useState('')
useEffect(() => {
setRunTimes(Object.values(appContext.runTimes))
}, [appContext.runTimes])
useEffect(() => {
if (runTimes.length) setSelectedRunTime(runTimes[0])
}, [runTimes])
const handleTabChange = (_e: any, newValue: string) => { const handleTabChange = (_e: any, newValue: string) => {
setTab(newValue) setTab(newValue)
@@ -57,7 +79,7 @@ const Studio = () => {
const runCode = (code: string) => { const runCode = (code: string) => {
axios axios
.post(`/SASjsApi/code/execute`, { code }) .post(`/SASjsApi/code/execute`, { code, runTime: selectedRunTime })
.then((res: any) => { .then((res: any) => {
const parsedLog = res?.data?.log const parsedLog = res?.data?.log
.map((logLine: any) => logLine.line) .map((logLine: any) => logLine.line)
@@ -89,6 +111,10 @@ const Studio = () => {
if (!event.ctrlKey && ctrlPressed) setCtrlPressed(false) if (!event.ctrlKey && ctrlPressed) setCtrlPressed(false)
} }
const handleChangeRunTime = (event: SelectChangeEvent) => {
setSelectedRunTime(event.target.value as RunTimeType)
}
useEffect(() => { useEffect(() => {
const content = localStorage.getItem('fileContent') ?? '' const content = localStorage.getItem('fileContent') ?? ''
setFileContent(content) setFileContent(content)
@@ -136,7 +162,7 @@ const Studio = () => {
</TabList> </TabList>
</Box> </Box>
<TabPanel style={{ paddingBottom: 0 }} value="1"> <TabPanel sx={{ paddingBottom: 0 }} value="1">
<div className={classes.subMenu}> <div className={classes.subMenu}>
<Tooltip title="CTRL+ENTER will also run SAS code"> <Tooltip title="CTRL+ENTER will also run SAS code">
<Button onClick={handleRunBtnClick} className={classes.runButton}> <Button onClick={handleRunBtnClick} className={classes.runButton}>
@@ -149,8 +175,23 @@ const Studio = () => {
<span style={{ fontSize: '12px' }}>RUN</span> <span style={{ fontSize: '12px' }}>RUN</span>
</Button> </Button>
</Tooltip> </Tooltip>
<Box sx={{ minWidth: '75px', marginLeft: '10px' }}>
<FormControl variant="standard">
<Select
labelId="run-time-select-label"
id="run-time-select"
value={selectedRunTime}
onChange={handleChangeRunTime}
>
{runTimes.map((runTime) => (
<MenuItem key={runTime} value={runTime}>
{runTime}
</MenuItem>
))}
</Select>
</FormControl>
</Box>
</div> </div>
{/* <Toolbar /> */}
<Paper <Paper
sx={{ sx={{
height: 'calc(100vh - 170px)', height: 'calc(100vh - 170px)',

View File

@@ -14,6 +14,11 @@ export enum ModeType {
Desktop = 'desktop' Desktop = 'desktop'
} }
export enum RunTimeType {
SAS = 'sas',
JS = 'js'
}
interface AppContextProps { interface AppContextProps {
checkingSession: boolean checkingSession: boolean
loggedIn: boolean loggedIn: boolean
@@ -25,6 +30,7 @@ interface AppContextProps {
displayName: string displayName: string
setDisplayName: Dispatch<SetStateAction<string>> | null setDisplayName: Dispatch<SetStateAction<string>> | null
mode: ModeType mode: ModeType
runTimes: RunTimeType[]
logout: (() => void) | null logout: (() => void) | null
} }
@@ -39,6 +45,7 @@ export const AppContext = createContext<AppContextProps>({
displayName: '', displayName: '',
setDisplayName: null, setDisplayName: null,
mode: ModeType.Server, mode: ModeType.Server,
runTimes: [],
logout: null logout: null
}) })
@@ -50,6 +57,7 @@ const AppContextProvider = (props: { children: ReactNode }) => {
const [username, setUsername] = useState('') const [username, setUsername] = useState('')
const [displayName, setDisplayName] = useState('') const [displayName, setDisplayName] = useState('')
const [mode, setMode] = useState(ModeType.Server) const [mode, setMode] = useState(ModeType.Server)
const [runTimes, setRunTimes] = useState<RunTimeType[]>([])
useEffect(() => { useEffect(() => {
setCheckingSession(true) setCheckingSession(true)
@@ -74,6 +82,7 @@ const AppContextProvider = (props: { children: ReactNode }) => {
.then((res) => res.data) .then((res) => res.data)
.then((data: any) => { .then((data: any) => {
setMode(data.mode) setMode(data.mode)
setRunTimes(data.runTimes)
}) })
.catch(() => {}) .catch(() => {})
}, []) }, [])
@@ -99,6 +108,7 @@ const AppContextProvider = (props: { children: ReactNode }) => {
displayName, displayName,
setDisplayName, setDisplayName,
mode, mode,
runTimes,
logout logout
}} }}
> >