mirror of
https://github.com/sasjs/server.git
synced 2025-12-10 11:24:35 +00:00
Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bee5deed2a | ||
|
|
e6e46838b3 | ||
|
|
404f1ec059 | ||
|
|
09d36bc754 | ||
|
|
3722bbaec3 | ||
|
|
480ee4da83 | ||
|
|
dd853fe13b | ||
|
|
e1142a33a0 | ||
|
|
d4e8d91cae | ||
|
|
9a74ec545d | ||
|
|
e3f5206758 | ||
|
|
fffd21b348 | ||
|
|
2d74ef5e12 | ||
|
|
224743a439 | ||
|
|
f39a76da17 | ||
|
|
6107d02c8e | ||
|
|
1966b17f27 | ||
|
|
87c8aa5146 | ||
|
|
e4c027ad51 | ||
|
|
083355fdba | ||
|
|
a3b57f6e28 | ||
|
|
b0ffa145bc | ||
|
|
a8df5f4afd | ||
|
|
62de960e86 | ||
|
|
31532c0efa | ||
|
|
732230524d | ||
|
|
6dc281313e | ||
|
|
92db3c7c82 | ||
|
|
d8b75a47d3 | ||
|
|
d70fc1032f | ||
|
|
794ee8f6e0 | ||
|
|
43769e711d | ||
|
|
30528a1528 | ||
|
|
b7e1753d25 | ||
|
|
9c5772a303 | ||
|
|
7a3d710153 | ||
|
|
0a6ebe6e62 | ||
|
|
6cbc657da3 | ||
|
|
cd838915fd | ||
|
|
4e486fda69 | ||
|
|
79cac53fdb | ||
|
|
450d99f06e | ||
|
|
51ee8c0825 | ||
|
|
a1151606f2 | ||
|
|
38193c83dd | ||
|
|
59ecc36f2b | ||
|
|
8bc459c9a7 | ||
|
|
f1f1e47f76 | ||
|
|
679e9de245 | ||
|
|
f0ac996b3c | ||
|
|
2d77222ae8 | ||
|
|
e6e5a5fd64 | ||
|
|
e1eb04494a | ||
|
|
b7fa8e5f80 | ||
|
|
ef4fae4496 | ||
|
|
3e5a4e0555 | ||
|
|
cf9a8091ea | ||
|
|
0edc45dd0a | ||
|
|
ceca370e27 | ||
|
|
f235b9c2f9 | ||
|
|
d86c841f1f | ||
|
|
076b866c02 | ||
|
|
19d4430b31 | ||
|
|
e5be0e6789 | ||
|
|
27129a8921 | ||
|
|
da11c03d55 | ||
|
|
4fbdda0365 |
@@ -1,4 +1,5 @@
|
|||||||
SAS_EXEC=<path to folder containing SAS executable 'sas'>
|
SAS_EXEC_PATH=<path to folder containing SAS executable>
|
||||||
|
SAS_EXEC_NAME=<name of SAS executable file>
|
||||||
PORT_API=<port for sasjs server (api)>
|
PORT_API=<port for sasjs server (api)>
|
||||||
PORT_WEB=<port for sasjs web component(react)>
|
PORT_WEB=<port for sasjs web component(react)>
|
||||||
ACCESS_TOKEN_SECRET=<secret>
|
ACCESS_TOKEN_SECRET=<secret>
|
||||||
|
|||||||
115
.github/CONTRIBUTING.md
vendored
Normal file
115
.github/CONTRIBUTING.md
vendored
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
# CONTRIBUTING
|
||||||
|
|
||||||
|
Contributions are very welcome! Feel free to raise an issue or start a discussion, for help in getting started.
|
||||||
|
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Configuration is made in the `configuration` section of `package.json`:
|
||||||
|
|
||||||
|
- Provide path to SAS9 executable.
|
||||||
|
|
||||||
|
|
||||||
|
### Using dockers:
|
||||||
|
|
||||||
|
There is `.env.example` file present at root of the project. [for Production]
|
||||||
|
|
||||||
|
There is `.env.example` file present at `./api` of the project. [for Development]
|
||||||
|
|
||||||
|
There is `.env.example` file present at `./web` of the project. [for Development]
|
||||||
|
|
||||||
|
Remember to provide enviornment variables.
|
||||||
|
|
||||||
|
#### Development
|
||||||
|
|
||||||
|
Command to run docker for development:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
It uses default docker compose file i.e. `docker-compose.yml` present at root.
|
||||||
|
It will build following images if running first time:
|
||||||
|
|
||||||
|
- `sasjs_server_api` - image for sasjs api server app based on _ExpressJS_
|
||||||
|
- `sasjs_server_web` - image for sasjs web component app based on _ReactJS_
|
||||||
|
- `mongodb` - image for mongo database
|
||||||
|
- `mongo-seed-users` - will be populating user data specified in _./mongo-seed/users/user.json_
|
||||||
|
- `mongo-seed-clients` - will be populating client data specified in _./mongo-seed/clients/client.json_
|
||||||
|
|
||||||
|
|
||||||
|
#### Production
|
||||||
|
|
||||||
|
Command to run docker for production:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose -f docker-compose.prod.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
It uses specified docker compose file i.e. `docker-compose.prod.yml` present at root.
|
||||||
|
It will build following images if running first time:
|
||||||
|
|
||||||
|
- `sasjs_server_prod` - image for sasjs server app containing api and web component's build served at route `/`
|
||||||
|
- `mongodb` - image for mongo database
|
||||||
|
- `mongo-seed-users` - will be populating user data specified in _./mongo-seed/users/user.json_
|
||||||
|
- `mongo-seed-clients` - will be populating client data specified in _./mongo-seed/clients/client.json_
|
||||||
|
|
||||||
|
### Using node:
|
||||||
|
|
||||||
|
#### Development (running api and web seperately):
|
||||||
|
|
||||||
|
##### API
|
||||||
|
|
||||||
|
Navigate to `./api`
|
||||||
|
There is `.env.example` file present at `./api` directory. Remember to provide enviornment variables else default values will be used mentioned in `.env.example` files
|
||||||
|
Command to install and run api server.
|
||||||
|
|
||||||
|
```
|
||||||
|
npm install
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Web
|
||||||
|
|
||||||
|
Navigate to `./web`
|
||||||
|
There is `.env.example` file present at `./web` directory. Remember to provide enviornment variables else default values will be used mentioned in `.env.example` files
|
||||||
|
Command to install and run api server.
|
||||||
|
|
||||||
|
```
|
||||||
|
npm install
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Development (running only api server and have web build served):
|
||||||
|
|
||||||
|
##### API server also serving Web build files
|
||||||
|
|
||||||
|
There is `.env.example` file present at `./api` directory. Remember to provide enviornment variables else default values will be used mentioned in `.env.example` files
|
||||||
|
Command to install and run api server.
|
||||||
|
|
||||||
|
```
|
||||||
|
cd ./web && npm i && npm build && cd ../
|
||||||
|
cd ./api && npm i && npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Production
|
||||||
|
|
||||||
|
##### API & WEB
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run server
|
||||||
|
```
|
||||||
|
|
||||||
|
This will install/build `web` and install `api`, then start prod server.
|
||||||
|
|
||||||
|
|
||||||
|
## Executables
|
||||||
|
|
||||||
|
Command to generate executables
|
||||||
|
|
||||||
|
```
|
||||||
|
cd ./web && npm i && npm build && cd ../
|
||||||
|
cd ./api && npm i && npm run exe
|
||||||
|
```
|
||||||
|
|
||||||
|
This will install/build web app and install/create executables of sasjs server at root `./executables`
|
||||||
13
.github/workflows/release.yml
vendored
13
.github/workflows/release.yml
vendored
@@ -32,10 +32,17 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
CI: true
|
CI: true
|
||||||
|
|
||||||
|
- name: Compress Executables
|
||||||
|
working-directory: ./executables
|
||||||
|
run: |
|
||||||
|
zip linux.zip api-linux
|
||||||
|
zip macos.zip api-macos
|
||||||
|
zip windows.zip api-win.exe
|
||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
./executables/api-linux
|
./executables/linux.zip
|
||||||
./executables/api-macos
|
./executables/macos.zip
|
||||||
./executables/api-win.exe
|
./executables/windows.zip
|
||||||
|
|||||||
104
CHANGELOG.md
104
CHANGELOG.md
@@ -2,6 +2,110 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||||
|
|
||||||
|
### [0.0.25](https://github.com/sasjs/server/compare/v0.0.24...v0.0.25) (2022-02-11)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* adding global macvar and bumping sasjs/core with additional server support ([404f1ec](https://github.com/sasjs/server/commit/404f1ec0593a027ed5e84b1d6a84cb9f2d09d99e))
|
||||||
|
|
||||||
|
### [0.0.24](https://github.com/sasjs/server/compare/v0.0.23...v0.0.24) (2022-02-11)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* removing sysmacdelete ([480ee4d](https://github.com/sasjs/server/commit/480ee4da831d2a89888c58ebec26bd89802ee2f5))
|
||||||
|
|
||||||
|
### [0.0.23](https://github.com/sasjs/server/compare/v0.0.22...v0.0.23) (2022-02-08)
|
||||||
|
|
||||||
|
### [0.0.22](https://github.com/sasjs/server/compare/v0.0.17...v0.0.22) (2022-02-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* adding missing global vars to autoexec ([1966b17](https://github.com/sasjs/server/commit/1966b17f27e66bf1c9673ef6e1c11f4868b4f816))
|
||||||
|
* avoid uninitialised note ([e4c027a](https://github.com/sasjs/server/commit/e4c027ad5121302b9ae093b2b76dc27f51a94365))
|
||||||
|
* bumping core version ([a8df5f4](https://github.com/sasjs/server/commit/a8df5f4afd6c4522270d0a60ab8153dfbdf79e16))
|
||||||
|
* bumping sasjs/core and updating descriptions ([31532c0](https://github.com/sasjs/server/commit/31532c0efa41e53f87377a2c7c41d21c7909e3a0))
|
||||||
|
* compressing release files for faster download times ([d8b75a4](https://github.com/sasjs/server/commit/d8b75a47d305e0772ccbf8837ba4d7347b94cc93))
|
||||||
|
* fixing versioning blooper ([a3b57f6](https://github.com/sasjs/server/commit/a3b57f6e28448fe98e634383041a5633541c8c02))
|
||||||
|
|
||||||
|
### [0.0.21](https://github.com/sasjs/server/compare/v0.0.20...v0.0.21) (2022-02-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* avoid uninitialised note ([e4c027a](https://github.com/sasjs/server/commit/e4c027ad5121302b9ae093b2b76dc27f51a94365))
|
||||||
|
|
||||||
|
### [0.0.20](https://github.com/sasjs/server/compare/v0.0.2...v0.0.20) (2022-01-20)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* fixing versioning blooper ([a3b57f6](https://github.com/sasjs/server/commit/a3b57f6e28448fe98e634383041a5633541c8c02))
|
||||||
|
|
||||||
|
### [0.0.19](https://github.com/sasjs/server/compare/v0.0.18...v0.0.19) (2022-01-20)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* bumping sasjs/core and updating descriptions ([31532c0](https://github.com/sasjs/server/commit/31532c0efa41e53f87377a2c7c41d21c7909e3a0))
|
||||||
|
|
||||||
|
### [0.0.18](https://github.com/sasjs/server/compare/v0.0.17...v0.0.18) (2022-01-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* compressing release files for faster download times ([d8b75a4](https://github.com/sasjs/server/commit/d8b75a47d305e0772ccbf8837ba4d7347b94cc93))
|
||||||
|
|
||||||
|
### [0.0.17](https://github.com/sasjs/server/compare/v0.0.16...v0.0.17) (2022-01-07)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* bug removed, log is clean now ([43769e7](https://github.com/sasjs/server/commit/43769e711d37a4f670786545630139a2d926dc76))
|
||||||
|
|
||||||
|
### [0.0.16](https://github.com/sasjs/server/compare/v0.0.15...v0.0.16) (2022-01-07)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* added sas9 server address ([cd83891](https://github.com/sasjs/server/commit/cd838915fdb216ee364ea677747409311b1214fb))
|
||||||
|
* recreate crashed session ([6cbc657](https://github.com/sasjs/server/commit/6cbc657da3eb7fa821a678443a3ae4079c2a1f09))
|
||||||
|
* session should be marked as consumed ([7a3d710](https://github.com/sasjs/server/commit/7a3d710153f37d12160ff45f8f97fb4fcc75d684))
|
||||||
|
|
||||||
|
### [0.0.15](https://github.com/sasjs/server/compare/v0.0.14...v0.0.15) (2022-01-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **studio:** web component updated ([2d77222](https://github.com/sasjs/server/commit/2d77222ae8a139acd9d96466d0e68291c4ebd70e))
|
||||||
|
* updated route for sas code ([e1eb044](https://github.com/sasjs/server/commit/e1eb04494a5650726c95990f74fc719eced4ccb5))
|
||||||
|
* **web:** autosave and autofocus ([51ee8c0](https://github.com/sasjs/server/commit/51ee8c0825f021d1d67b2d765d5b434cbf248a1f))
|
||||||
|
* **web:** parsing of webout ([a115160](https://github.com/sasjs/server/commit/a1151606f21e0007e2b1ca1245d592d96866f62a))
|
||||||
|
* **web:** sticky tabs on Studio + extra run code button removed ([450d99f](https://github.com/sasjs/server/commit/450d99f06e5929eb1679e6203284e4faa44e19b0))
|
||||||
|
|
||||||
|
### [0.0.14](https://github.com/sasjs/server/compare/v0.0.13...v0.0.14) (2021-12-19)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* actually a README change, the fix was in the previous commit (updating ms_webout) that should have been a PR, to trigger a release ([d86c841](https://github.com/sasjs/server/commit/d86c841f1fb94455ac3500f215a42b4acb8b0017))
|
||||||
|
* bumping sasjs/core with adjustment to ms_webout() ([076b866](https://github.com/sasjs/server/commit/076b866c020fb017512c2764801022a57fe4cca8))
|
||||||
|
* switch to main branch ([ceca370](https://github.com/sasjs/server/commit/ceca370e2757baf2e8ebb90dab6dfd27f7b990fc))
|
||||||
|
|
||||||
|
### [0.0.13](https://github.com/sasjs/server/compare/v0.0.12...v0.0.13) (2021-12-16)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **studio:** run selected code + open in studio ([27129a8](https://github.com/sasjs/server/commit/27129a8921084c72968383fdbc2ecbd2f417456c))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* output for Studio ([e5be0e6](https://github.com/sasjs/server/commit/e5be0e678965b05c64bcc8f55c48a366e0ff55a3))
|
||||||
|
|
||||||
### [0.0.12](https://github.com/sasjs/server/compare/v0.0.11...v0.0.12) (2021-12-15)
|
### [0.0.12](https://github.com/sasjs/server/compare/v0.0.11...v0.0.12) (2021-12-15)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
116
README.md
116
README.md
@@ -8,110 +8,68 @@ SASjs Server provides a NodeJS wrapper for calling the SAS binary executable. It
|
|||||||
|
|
||||||
One major benefit of using SASjs Server (alongside other components of the SASjs framework such as the [CLI](https://cli.sasjs.io), [Adapter](https://adapter.sasjs.io) and [Core](https://core.sasjs.io) library) is that the projects you create can be very easily ported to SAS 9 (Stored Process server) or Viya (Job Execution server).
|
One major benefit of using SASjs Server (alongside other components of the SASjs framework such as the [CLI](https://cli.sasjs.io), [Adapter](https://adapter.sasjs.io) and [Core](https://core.sasjs.io) library) is that the projects you create can be very easily ported to SAS 9 (Stored Process server) or Viya (Job Execution server).
|
||||||
|
|
||||||
## Configuration
|
SASjs Server is available in two modes - Desktop (without authentication) and Server (with authentiation, and a database)
|
||||||
|
## Desktop Version
|
||||||
|
|
||||||
Configuration is made in the `configuration` section of `package.json`:
|
### Manual Installation
|
||||||
|
Download the relevant package from the [releases](https://github.com/sasjs/server/releases) page
|
||||||
|
|
||||||
- Provide path to SAS9 executable.
|
Next, trigger by double clicking (windows) or executing from commandline.
|
||||||
|
|
||||||
### Using dockers:
|
You are presented with two prompts:
|
||||||
|
|
||||||
There is `.env.example` file present at root of the project. [for Production]
|
* Location of your `sas.exe` / `sas.sh` executable
|
||||||
|
* Path to a filesystem location for Stored Programs and temporary files
|
||||||
|
|
||||||
There is `.env.example` file present at `./api` of the project. [for Development]
|
|
||||||
|
|
||||||
There is `.env.example` file present at `./web` of the project. [for Development]
|
## Programmatic Installation
|
||||||
|
|
||||||
Remember to provide enviornment variables.
|
Fetch the relevant package from github using `curl`, eg as follows (for linux):
|
||||||
|
|
||||||
#### Development
|
```bash
|
||||||
|
curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
|
||||||
Command to run docker for development:
|
unzip linux.zip
|
||||||
|
|
||||||
```
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
```
|
||||||
|
|
||||||
It uses default docker compose file i.e. `docker-compose.yml` present at root.
|
The app can then be launched with `./api-linux` and prompts followed.
|
||||||
It will build following images if running first time:
|
|
||||||
|
|
||||||
- `sasjs_server_api` - image for sasjs api server app based on _ExpressJS_
|
When launching the app, it will make use of specific environment variables. These can be set in the following places:
|
||||||
- `sasjs_server_web` - image for sasjs web component app based on _ReactJS_
|
|
||||||
- `mongodb` - image for mongo database
|
|
||||||
- `mongo-seed-users` - will be populating user data specified in _./mongo-seed/users/user.json_
|
|
||||||
- `mongo-seed-clients` - will be populating client data specified in _./mongo-seed/clients/client.json_
|
|
||||||
|
|
||||||
#### Production
|
- Configured globally in /etc/environment file
|
||||||
|
- Export in terminal or shell script (`export VAR=VALUE`)
|
||||||
|
- Prepend in command
|
||||||
|
- Enter in the `.env` file alongside the executable
|
||||||
|
|
||||||
Command to run docker for production:
|
Example variables:
|
||||||
|
|
||||||
```
|
```
|
||||||
docker-compose -f docker-compose.prod.yml up -d
|
PORT=5004
|
||||||
|
SAS_PATH=/path/to/sas/executable.exe
|
||||||
|
DRIVE_PATH=./tmp
|
||||||
```
|
```
|
||||||
|
|
||||||
It uses specified docker compose file i.e. `docker-compose.prod.yml` present at root.
|
Setting these prompts variables will avoid the need for prompts.
|
||||||
It will build following images if running first time:
|
|
||||||
|
|
||||||
- `sasjs_server_prod` - image for sasjs server app containing api and web component's build served at route `/`
|
Normally the server process will stop when your terminal dies. To keep it going you can use the npm package [forever](https://www.npmjs.com/package/forever) (`npm i -g forever`) as follows:
|
||||||
- `mongodb` - image for mongo database
|
|
||||||
- `mongo-seed-users` - will be populating user data specified in _./mongo-seed/users/user.json_
|
|
||||||
- `mongo-seed-clients` - will be populating client data specified in _./mongo-seed/clients/client.json_
|
|
||||||
|
|
||||||
### Using node:
|
```bash
|
||||||
|
export SAS_PATH=/opt/sas9/SASHome/SASFoundation/9.4/sasexe/sas
|
||||||
|
export PORT=5001
|
||||||
|
export DRIVE_PATH=./tmp
|
||||||
|
|
||||||
#### Development (running api and web seperately):
|
forever start -c "./api-linux" ./
|
||||||
|
|
||||||
##### API
|
|
||||||
|
|
||||||
Navigate to `./api`
|
|
||||||
There is `.env.example` file present at `./api` directory. Remember to provide enviornment variables else default values will be used mentioned in `.env.example` files
|
|
||||||
Command to install and run api server.
|
|
||||||
|
|
||||||
```
|
|
||||||
npm install
|
|
||||||
npm start
|
|
||||||
```
|
```
|
||||||
|
|
||||||
##### Web
|
To get the log files:
|
||||||
|
```bash
|
||||||
Navigate to `./web`
|
forever list
|
||||||
There is `.env.example` file present at `./web` directory. Remember to provide enviornment variables else default values will be used mentioned in `.env.example` files
|
# grap log file link
|
||||||
Command to install and run api server.
|
tail -f LOGFILE
|
||||||
|
|
||||||
```
|
|
||||||
npm install
|
|
||||||
npm start
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Development (running only api server and have web build served):
|
To stop:
|
||||||
|
|
||||||
##### API server also serving Web build files
|
|
||||||
|
|
||||||
There is `.env.example` file present at `./api` directory. Remember to provide enviornment variables else default values will be used mentioned in `.env.example` files
|
|
||||||
Command to install and run api server.
|
|
||||||
|
|
||||||
```
|
```
|
||||||
cd ./web && npm i && npm build && cd ../
|
forever stop <pid>
|
||||||
cd ./api && npm i && npm start
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Production
|
|
||||||
|
|
||||||
##### API & WEB
|
|
||||||
|
|
||||||
```
|
|
||||||
npm run server
|
|
||||||
```
|
|
||||||
|
|
||||||
This will install/build `web` and install `api`, then start prod server.
|
|
||||||
|
|
||||||
## Executables
|
|
||||||
|
|
||||||
Command to generate executables
|
|
||||||
|
|
||||||
```
|
|
||||||
cd ./web && npm i && npm build && cd ../
|
|
||||||
cd ./api && npm i && npm run exe
|
|
||||||
```
|
|
||||||
|
|
||||||
This will install/build web app and install/create executables of sasjs server at root `./executables`
|
|
||||||
|
|||||||
89
SASjsServer.drawio
Normal file
89
SASjsServer.drawio
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
<mxfile host="65bd71144e">
|
||||||
|
<diagram id="HJy_QFGaI9JSrArARLup" name="Page-1">
|
||||||
|
<mxGraphModel dx="1908" dy="2140" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
|
||||||
|
<root>
|
||||||
|
<mxCell id="0"/>
|
||||||
|
<mxCell id="1" parent="0"/>
|
||||||
|
<mxCell id="4" value="End user" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;fontStyle=0" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="-360" y="-120" width="40" height="80" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="7" value="SASjs Server" style="whiteSpace=wrap;html=1;verticalAlign=top;fontStyle=0;fontSize=30;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="30" y="-150" width="360" height="850" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="8" value="" style="edgeStyle=none;html=1;entryX=0;entryY=0.25;entryDx=0;entryDy=0;" edge="1" parent="1" target="28">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<mxPoint x="-340" y="23" as="sourcePoint"/>
|
||||||
|
<mxPoint x="115" y="22.586363636363558" as="targetPoint"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="11" value="<div style="font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace ; font-size: 12px ; line-height: 18px"><span style="color: #a31515">/SASjsApi/auth/authorize<br>(username,password,clientId)</span></div>" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="8">
|
||||||
|
<mxGeometry x="-0.1257" y="2" relative="1" as="geometry">
|
||||||
|
<mxPoint as="offset"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="14" value="" style="edgeStyle=none;html=1;exitX=-0.002;exitY=0.874;exitDx=0;exitDy=0;exitPerimeter=0;" edge="1" parent="1" source="28">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<mxPoint x="110" y="80" as="sourcePoint"/>
|
||||||
|
<mxPoint x="-340" y="80" as="targetPoint"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="16" value="<font color="#a31515" face="menlo, monaco, courier new, monospace"><span style="font-size: 12px">`code`</span></font>" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="14">
|
||||||
|
<mxGeometry x="0.1931" y="-1" relative="1" as="geometry">
|
||||||
|
<mxPoint as="offset"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="21" value="End user" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;fontStyle=0" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="-360" y="545" width="40" height="80" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="22" value="" style="edgeStyle=none;html=1;entryX=0;entryY=0.25;entryDx=0;entryDy=0;" edge="1" parent="1" target="30">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<mxPoint x="-340" y="165" as="sourcePoint"/>
|
||||||
|
<mxPoint x="115" y="165" as="targetPoint"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="23" value="<div style="font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace ; font-size: 12px ; line-height: 18px"><div style="font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace ; line-height: 18px"><span style="color: #a31515">/SASjsApi/auth/token</span></div><span style="color: #a31515">(clientId,code)</span></div>" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="22">
|
||||||
|
<mxGeometry x="-0.1257" y="2" relative="1" as="geometry">
|
||||||
|
<mxPoint as="offset"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="24" value="" style="edgeStyle=none;html=1;exitX=0.009;exitY=0.905;exitDx=0;exitDy=0;exitPerimeter=0;" edge="1" parent="1" source="30">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<mxPoint x="210" y="222.5" as="sourcePoint"/>
|
||||||
|
<mxPoint x="-340" y="223" as="targetPoint"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="25" value="<font color="#a31515" face="menlo, monaco, courier new, monospace"><span style="font-size: 12px">`</span></font><span style="color: rgb(163 , 21 , 21) ; font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace ; font-size: 12px">accessToken</span><span style="font-size: 12px ; color: rgb(163 , 21 , 21) ; font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace">` &amp; `</span><span style="color: rgb(163 , 21 , 21) ; font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace ; font-size: 12px">refreshToken</span><span style="color: rgb(163 , 21 , 21) ; font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace ; font-size: 12px">`</span>" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="24">
|
||||||
|
<mxGeometry x="0.1931" y="-1" relative="1" as="geometry">
|
||||||
|
<mxPoint as="offset"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="26" value="" style="endArrow=none;dashed=1;html=1;dashPattern=1 3;strokeWidth=2;" edge="1" parent="1" source="21" target="4">
|
||||||
|
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||||
|
<mxPoint x="40" y="240" as="sourcePoint"/>
|
||||||
|
<mxPoint x="90" y="190" as="targetPoint"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="28" value="<span>Validates</span><br><span>username/password/clientId</span><br><span>and issue short</span><br><span>Authorization code</span>" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="115" width="190" height="90" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="30" value="Validates<br>clientId &amp; authorization code<br>and issue<br>Access Token &amp; Refresh Token" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="115" y="140" width="190" height="90" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="32" value="Protected APIs<br>Authenticate requests <br>with provided Bearer Token" style="whiteSpace=wrap;html=1;verticalAlign=top;fontStyle=0;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="50" y="280" width="320" height="400" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="33" value="" style="edgeStyle=none;html=1;entryX=0;entryY=0.373;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" target="32">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<mxPoint x="-340" y="432.5" as="sourcePoint"/>
|
||||||
|
<mxPoint x="-10" y="430" as="targetPoint"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="34" value="<div style="font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace ; font-size: 12px ; line-height: 18px"><div style="font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace ; line-height: 18px"><font color="#a31515">Request with Access Token</font></div></div>" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="33">
|
||||||
|
<mxGeometry x="-0.1257" y="2" relative="1" as="geometry">
|
||||||
|
<mxPoint as="offset"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
</root>
|
||||||
|
</mxGraphModel>
|
||||||
|
</diagram>
|
||||||
|
</mxfile>
|
||||||
@@ -8,4 +8,4 @@ AUTH_CODE_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
|
||||||
|
|
||||||
SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas
|
SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas
|
||||||
SAS_DRIVE=./tmp
|
DRIVE_PATH=./tmp
|
||||||
|
|||||||
70
api/package-lock.json
generated
70
api/package-lock.json
generated
@@ -1,14 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "api",
|
"name": "api",
|
||||||
"version": "0.0.1",
|
"version": "0.0.2",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "api",
|
"name": "api",
|
||||||
"version": "0.0.1",
|
"version": "0.0.2",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sasjs/core": "^2.48.6",
|
"@sasjs/core": "4.8.0",
|
||||||
"@sasjs/utils": "2.34.1",
|
"@sasjs/utils": "2.34.1",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
@@ -1551,9 +1551,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sasjs/core": {
|
"node_modules/@sasjs/core": {
|
||||||
"version": "2.48.6",
|
"version": "4.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-2.48.6.tgz",
|
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.8.0.tgz",
|
||||||
"integrity": "sha512-5kCf4TdCVOYve4wSHVTi+db34hknDwvY2C/JVEPHT6T3CkQ5cnwRVPSFz/1WzXzcVvdUi4ag5xd9SDOsU12oWA=="
|
"integrity": "sha512-go31UvPRzOqWJm2hkM+9/KQXIky9SA7bFMLa2d1dMfpdrX0XQyw+dcDM3jHD/agtcATWYJ9exJrET7Bkj148YQ=="
|
||||||
},
|
},
|
||||||
"node_modules/@sasjs/utils": {
|
"node_modules/@sasjs/utils": {
|
||||||
"version": "2.34.1",
|
"version": "2.34.1",
|
||||||
@@ -2607,9 +2607,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.4.1",
|
"version": "8.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.6.0.tgz",
|
||||||
"integrity": "sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA==",
|
"integrity": "sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
@@ -13408,9 +13408,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/source-map-support": {
|
"node_modules/source-map-support": {
|
||||||
"version": "0.5.19",
|
"version": "0.5.21",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
||||||
"integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==",
|
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"buffer-from": "^1.0.0",
|
"buffer-from": "^1.0.0",
|
||||||
@@ -13918,16 +13918,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/swagger-ui-dist": {
|
"node_modules/swagger-ui-dist": {
|
||||||
"version": "3.52.5",
|
"version": "4.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.52.5.tgz",
|
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.1.3.tgz",
|
||||||
"integrity": "sha512-8z18eX8G/jbTXYzyNIaobrnD7PSN7yU/YkSasMmajrXtw0FGS64XjrKn5v37d36qmU3o1xLeuYnktshRr7uIFw=="
|
"integrity": "sha512-WvfPSfAAMlE/sKS6YkW47nX/hA7StmhYnAHc6wWCXNL0oclwLj6UXv0hQCkLnDgvebi0MEV40SJJpVjKUgH1IQ=="
|
||||||
},
|
},
|
||||||
"node_modules/swagger-ui-express": {
|
"node_modules/swagger-ui-express": {
|
||||||
"version": "4.1.6",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.2.0.tgz",
|
||||||
"integrity": "sha512-Xs2BGGudvDBtL7RXcYtNvHsFtP1DBFPMJFRxHe5ez/VG/rzVOEjazJOOSc/kSCyxreCTKfJrII6MJlL9a6t8vw==",
|
"integrity": "sha512-znrHTwh9UpvsjqgWopA4noIet7mi7UGuIYZ465YfUDKQ5Dpas0jxnkfUKCo+0aB17YCBv26AhIjiQYDV4uvJFA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"swagger-ui-dist": "^3.18.1"
|
"swagger-ui-dist": ">3.52.5"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= v0.10.32"
|
"node": ">= v0.10.32"
|
||||||
@@ -16102,9 +16102,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@sasjs/core": {
|
"@sasjs/core": {
|
||||||
"version": "2.48.6",
|
"version": "4.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-2.48.6.tgz",
|
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.8.0.tgz",
|
||||||
"integrity": "sha512-5kCf4TdCVOYve4wSHVTi+db34hknDwvY2C/JVEPHT6T3CkQ5cnwRVPSFz/1WzXzcVvdUi4ag5xd9SDOsU12oWA=="
|
"integrity": "sha512-go31UvPRzOqWJm2hkM+9/KQXIky9SA7bFMLa2d1dMfpdrX0XQyw+dcDM3jHD/agtcATWYJ9exJrET7Bkj148YQ=="
|
||||||
},
|
},
|
||||||
"@sasjs/utils": {
|
"@sasjs/utils": {
|
||||||
"version": "2.34.1",
|
"version": "2.34.1",
|
||||||
@@ -17009,9 +17009,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"acorn": {
|
"acorn": {
|
||||||
"version": "8.4.1",
|
"version": "8.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.6.0.tgz",
|
||||||
"integrity": "sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA==",
|
"integrity": "sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"acorn-globals": {
|
"acorn-globals": {
|
||||||
@@ -25115,9 +25115,9 @@
|
|||||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
|
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
|
||||||
},
|
},
|
||||||
"source-map-support": {
|
"source-map-support": {
|
||||||
"version": "0.5.19",
|
"version": "0.5.21",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
||||||
"integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==",
|
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"buffer-from": "^1.0.0",
|
"buffer-from": "^1.0.0",
|
||||||
@@ -25531,16 +25531,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"swagger-ui-dist": {
|
"swagger-ui-dist": {
|
||||||
"version": "3.52.5",
|
"version": "4.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.52.5.tgz",
|
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.1.3.tgz",
|
||||||
"integrity": "sha512-8z18eX8G/jbTXYzyNIaobrnD7PSN7yU/YkSasMmajrXtw0FGS64XjrKn5v37d36qmU3o1xLeuYnktshRr7uIFw=="
|
"integrity": "sha512-WvfPSfAAMlE/sKS6YkW47nX/hA7StmhYnAHc6wWCXNL0oclwLj6UXv0hQCkLnDgvebi0MEV40SJJpVjKUgH1IQ=="
|
||||||
},
|
},
|
||||||
"swagger-ui-express": {
|
"swagger-ui-express": {
|
||||||
"version": "4.1.6",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.2.0.tgz",
|
||||||
"integrity": "sha512-Xs2BGGudvDBtL7RXcYtNvHsFtP1DBFPMJFRxHe5ez/VG/rzVOEjazJOOSc/kSCyxreCTKfJrII6MJlL9a6t8vw==",
|
"integrity": "sha512-znrHTwh9UpvsjqgWopA4noIet7mi7UGuIYZ465YfUDKQ5Dpas0jxnkfUKCo+0aB17YCBv26AhIjiQYDV4uvJFA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"swagger-ui-dist": "^3.18.1"
|
"swagger-ui-dist": ">3.52.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"symbol-tree": {
|
"symbol-tree": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "api",
|
"name": "api",
|
||||||
"version": "0.0.1",
|
"version": "0.0.2",
|
||||||
"description": "Api of SASjs server",
|
"description": "Api of SASjs server",
|
||||||
"main": "./src/server.ts",
|
"main": "./src/server.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -41,12 +41,12 @@
|
|||||||
},
|
},
|
||||||
"release": {
|
"release": {
|
||||||
"branches": [
|
"branches": [
|
||||||
"master"
|
"main"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"author": "Analytium Ltd",
|
"author": "4GL Ltd",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sasjs/core": "^2.48.6",
|
"@sasjs/core": "4.8.0",
|
||||||
"@sasjs/utils": "2.34.1",
|
"@sasjs/utils": "2.34.1",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
|||||||
@@ -92,6 +92,16 @@ components:
|
|||||||
- clientSecret
|
- clientSecret
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
|
ExecuteSASCodePayload:
|
||||||
|
properties:
|
||||||
|
code:
|
||||||
|
type: string
|
||||||
|
description: 'Code of SAS program'
|
||||||
|
example: '* SAS Code HERE;'
|
||||||
|
required:
|
||||||
|
- code
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
MemberType.folder:
|
MemberType.folder:
|
||||||
enum:
|
enum:
|
||||||
- folder
|
- folder
|
||||||
@@ -391,7 +401,7 @@ info:
|
|||||||
version: 0.0.1
|
version: 0.0.1
|
||||||
description: 'Api of SASjs server'
|
description: 'Api of SASjs server'
|
||||||
contact:
|
contact:
|
||||||
name: 'Analytium Ltd'
|
name: '4GL Ltd'
|
||||||
openapi: 3.0.0
|
openapi: 3.0.0
|
||||||
paths:
|
paths:
|
||||||
/SASjsApi/auth/authorize:
|
/SASjsApi/auth/authorize:
|
||||||
@@ -501,6 +511,30 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/ClientPayload'
|
$ref: '#/components/schemas/ClientPayload'
|
||||||
|
/SASjsApi/code/execute:
|
||||||
|
post:
|
||||||
|
operationId: ExecuteSASCode
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: 'Execute SAS code.'
|
||||||
|
summary: 'Run SAS Code and returns log'
|
||||||
|
tags:
|
||||||
|
- CODE
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
|
parameters: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ExecuteSASCodePayload'
|
||||||
/SASjsApi/drive/deploy:
|
/SASjsApi/drive/deploy:
|
||||||
post:
|
post:
|
||||||
operationId: Deploy
|
operationId: Deploy
|
||||||
@@ -972,6 +1006,26 @@ paths:
|
|||||||
format: double
|
format: double
|
||||||
type: number
|
type: number
|
||||||
example: '6789'
|
example: '6789'
|
||||||
|
/SASjsApi/session:
|
||||||
|
get:
|
||||||
|
operationId: Session
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/UserResponse'
|
||||||
|
examples:
|
||||||
|
'Example 1':
|
||||||
|
value: {id: 123, username: johnusername, displayName: John}
|
||||||
|
summary: 'Get session info (username).'
|
||||||
|
tags:
|
||||||
|
- Session
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
|
parameters: []
|
||||||
/SASjsApi/stp/execute:
|
/SASjsApi/stp/execute:
|
||||||
get:
|
get:
|
||||||
operationId: ExecuteReturnRaw
|
operationId: ExecuteReturnRaw
|
||||||
@@ -1027,26 +1081,6 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/ExecuteReturnJsonPayload'
|
$ref: '#/components/schemas/ExecuteReturnJsonPayload'
|
||||||
/SASjsApi/session:
|
|
||||||
get:
|
|
||||||
operationId: Session
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Ok
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/UserResponse'
|
|
||||||
examples:
|
|
||||||
'Example 1':
|
|
||||||
value: {id: 123, username: johnusername, displayName: John}
|
|
||||||
summary: 'Get session info (username).'
|
|
||||||
tags:
|
|
||||||
- Session
|
|
||||||
security:
|
|
||||||
-
|
|
||||||
bearerAuth: []
|
|
||||||
parameters: []
|
|
||||||
servers:
|
servers:
|
||||||
-
|
-
|
||||||
url: /
|
url: /
|
||||||
@@ -1072,3 +1106,6 @@ tags:
|
|||||||
-
|
-
|
||||||
name: STP
|
name: STP
|
||||||
description: 'Operations about STP'
|
description: 'Operations about STP'
|
||||||
|
-
|
||||||
|
name: CODE
|
||||||
|
description: 'Operations on SAS code'
|
||||||
|
|||||||
@@ -13,11 +13,14 @@ dotenv.config()
|
|||||||
const app = express()
|
const app = express()
|
||||||
|
|
||||||
const { MODE, CORS, PORT_WEB } = process.env
|
const { MODE, CORS, PORT_WEB } = process.env
|
||||||
|
const whiteList = [
|
||||||
|
`http://localhost:${PORT_WEB ?? 3000}`,
|
||||||
|
'https://sas.analytium.co.uk:8343'
|
||||||
|
]
|
||||||
|
|
||||||
if (MODE?.trim() !== 'server' || CORS?.trim() === 'enable') {
|
if (MODE?.trim() !== 'server' || CORS?.trim() === 'enable') {
|
||||||
console.log('All CORS Requests are enabled')
|
console.log('All CORS Requests are enabled')
|
||||||
app.use(
|
app.use(cors({ credentials: true, origin: whiteList }))
|
||||||
cors({ credentials: true, origin: `http://localhost:${PORT_WEB ?? 3000}` })
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
app.use(express.json({ limit: '50mb' }))
|
app.use(express.json({ limit: '50mb' }))
|
||||||
|
|||||||
63
api/src/controllers/code.ts
Normal file
63
api/src/controllers/code.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import express from 'express'
|
||||||
|
import { Request, Security, Route, Tags, Post, Body } from 'tsoa'
|
||||||
|
import { ExecutionController } from './internal'
|
||||||
|
import { PreProgramVars } from '../types'
|
||||||
|
|
||||||
|
interface ExecuteSASCodePayload {
|
||||||
|
/**
|
||||||
|
* Code of SAS program
|
||||||
|
* @example "* SAS Code HERE;"
|
||||||
|
*/
|
||||||
|
code: string
|
||||||
|
}
|
||||||
|
|
||||||
|
@Security('bearerAuth')
|
||||||
|
@Route('SASjsApi/code')
|
||||||
|
@Tags('CODE')
|
||||||
|
export class CodeController {
|
||||||
|
/**
|
||||||
|
* Execute SAS code.
|
||||||
|
* @summary Run SAS Code and returns log
|
||||||
|
*/
|
||||||
|
@Post('/execute')
|
||||||
|
public async executeSASCode(
|
||||||
|
@Request() request: express.Request,
|
||||||
|
@Body() body: ExecuteSASCodePayload
|
||||||
|
): Promise<string> {
|
||||||
|
return executeSASCode(request, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const executeSASCode = async (req: any, { code }: ExecuteSASCodePayload) => {
|
||||||
|
try {
|
||||||
|
const result = await new ExecutionController().executeProgram(
|
||||||
|
code,
|
||||||
|
getPreProgramVariables(req),
|
||||||
|
{ ...req.query, _debug: 131 },
|
||||||
|
undefined,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
|
||||||
|
return result as string
|
||||||
|
} catch (err: any) {
|
||||||
|
throw {
|
||||||
|
code: 400,
|
||||||
|
status: 'failure',
|
||||||
|
message: 'Job execution failed.',
|
||||||
|
error: typeof err === 'object' ? err.toString() : err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPreProgramVariables = (req: any): PreProgramVars => {
|
||||||
|
const host = req.get('host')
|
||||||
|
const protocol = req.protocol + '://'
|
||||||
|
const { user, accessToken } = req
|
||||||
|
return {
|
||||||
|
username: user.username,
|
||||||
|
userId: user.userId,
|
||||||
|
displayName: user.displayName,
|
||||||
|
serverUrl: protocol + host,
|
||||||
|
accessToken
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
export * from './auth'
|
export * from './auth'
|
||||||
export * from './client'
|
export * from './client'
|
||||||
|
export * from './code'
|
||||||
export * from './drive'
|
export * from './drive'
|
||||||
export * from './group'
|
export * from './group'
|
||||||
|
export * from './session'
|
||||||
export * from './stp'
|
export * from './stp'
|
||||||
export * from './user'
|
export * from './user'
|
||||||
export * from './session'
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { PreProgramVars, TreeNode } from '../../types'
|
|||||||
import { generateFileUploadSasCode, getTmpFilesFolderPath } from '../../utils'
|
import { generateFileUploadSasCode, getTmpFilesFolderPath } from '../../utils'
|
||||||
|
|
||||||
export class ExecutionController {
|
export class ExecutionController {
|
||||||
async execute(
|
async executeFile(
|
||||||
programPath: string,
|
programPath: string,
|
||||||
preProgramVariables: PreProgramVars,
|
preProgramVariables: PreProgramVars,
|
||||||
vars: { [key: string]: string | number | undefined },
|
vars: { [key: string]: string | number | undefined },
|
||||||
@@ -16,12 +16,28 @@ export class ExecutionController {
|
|||||||
if (!(await fileExists(programPath)))
|
if (!(await fileExists(programPath)))
|
||||||
throw 'ExecutionController: SAS file does not exist.'
|
throw 'ExecutionController: SAS file does not exist.'
|
||||||
|
|
||||||
let program = await readFile(programPath)
|
const program = await readFile(programPath)
|
||||||
|
|
||||||
|
return this.executeProgram(
|
||||||
|
program,
|
||||||
|
preProgramVariables,
|
||||||
|
vars,
|
||||||
|
otherArgs,
|
||||||
|
returnJson
|
||||||
|
)
|
||||||
|
}
|
||||||
|
async executeProgram(
|
||||||
|
program: string,
|
||||||
|
preProgramVariables: PreProgramVars,
|
||||||
|
vars: { [key: string]: string | number | undefined },
|
||||||
|
otherArgs?: any,
|
||||||
|
returnJson?: boolean
|
||||||
|
) {
|
||||||
const sessionController = getSessionController()
|
const sessionController = getSessionController()
|
||||||
|
|
||||||
const session = await sessionController.getSession()
|
const session = await sessionController.getSession()
|
||||||
session.inUse = true
|
session.inUse = true
|
||||||
|
session.consumed = true
|
||||||
|
|
||||||
const logPath = path.join(session.path, 'log.log')
|
const logPath = path.join(session.path, 'log.log')
|
||||||
|
|
||||||
@@ -48,7 +64,15 @@ export class ExecutionController {
|
|||||||
%let _sasjs_apipath=/SASjsApi/stp/execute;
|
%let _sasjs_apipath=/SASjsApi/stp/execute;
|
||||||
%let _metaperson=&_sasjs_displayname;
|
%let _metaperson=&_sasjs_displayname;
|
||||||
%let _metauser=&_sasjs_username;
|
%let _metauser=&_sasjs_username;
|
||||||
%let sasjsprocessmode=Stored Program;`
|
%let sasjsprocessmode=Stored Program;
|
||||||
|
|
||||||
|
%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 = `
|
program = `
|
||||||
/* runtime vars */
|
/* runtime vars */
|
||||||
@@ -85,14 +109,12 @@ ${program}`
|
|||||||
await createFile(codePath + '.bkp', program)
|
await createFile(codePath + '.bkp', program)
|
||||||
await moveFile(codePath + '.bkp', codePath)
|
await moveFile(codePath + '.bkp', codePath)
|
||||||
|
|
||||||
// we now need to poll the session array
|
// we now need to poll the session status
|
||||||
while (!session.completed) {
|
while (!session.completed) {
|
||||||
await delay(50)
|
await delay(50)
|
||||||
}
|
}
|
||||||
|
|
||||||
const log =
|
const log = (await fileExists(logPath)) ? await readFile(logPath) : ''
|
||||||
((await fileExists(logPath)) ? await readFile(logPath) : '') +
|
|
||||||
session.crashed
|
|
||||||
const webout = (await fileExists(weboutPath))
|
const webout = (await fileExists(weboutPath))
|
||||||
? await readFile(weboutPath)
|
? await readFile(weboutPath)
|
||||||
: ''
|
: ''
|
||||||
@@ -100,8 +122,8 @@ ${program}`
|
|||||||
const debugValue =
|
const debugValue =
|
||||||
typeof vars._debug === 'string' ? parseInt(vars._debug) : vars._debug
|
typeof vars._debug === 'string' ? parseInt(vars._debug) : vars._debug
|
||||||
|
|
||||||
|
// it should be deleted by scheduleSessionDestroy
|
||||||
session.inUse = false
|
session.inUse = false
|
||||||
sessionController.deleteSession(session)
|
|
||||||
|
|
||||||
if (returnJson) {
|
if (returnJson) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ import {
|
|||||||
createFile,
|
createFile,
|
||||||
fileExists,
|
fileExists,
|
||||||
generateTimestamp,
|
generateTimestamp,
|
||||||
readFile
|
readFile,
|
||||||
|
moveFile
|
||||||
} from '@sasjs/utils'
|
} from '@sasjs/utils'
|
||||||
|
|
||||||
const execFilePromise = promisify(execFile)
|
const execFilePromise = promisify(execFile)
|
||||||
@@ -20,8 +21,11 @@ const execFilePromise = promisify(execFile)
|
|||||||
export class SessionController {
|
export class SessionController {
|
||||||
private sessions: Session[] = []
|
private sessions: Session[] = []
|
||||||
|
|
||||||
|
private getReadySessions = (): Session[] =>
|
||||||
|
this.sessions.filter((sess: Session) => sess.ready && !sess.consumed)
|
||||||
|
|
||||||
public async getSession() {
|
public async getSession() {
|
||||||
const readySessions = this.sessions.filter((sess: Session) => sess.ready)
|
const readySessions = this.getReadySessions()
|
||||||
|
|
||||||
const session = readySessions.length
|
const session = readySessions.length
|
||||||
? readySessions[0]
|
? readySessions[0]
|
||||||
@@ -32,7 +36,7 @@ export class SessionController {
|
|||||||
return session
|
return session
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createSession() {
|
private async createSession(): Promise<Session> {
|
||||||
const sessionId = generateUniqueFileName(generateTimestamp())
|
const sessionId = generateUniqueFileName(generateTimestamp())
|
||||||
const sessionFolder = path.join(getTmpSessionsFolderPath(), sessionId)
|
const sessionFolder = path.join(getTmpSessionsFolderPath(), sessionId)
|
||||||
|
|
||||||
@@ -47,6 +51,7 @@ export class SessionController {
|
|||||||
id: sessionId,
|
id: sessionId,
|
||||||
ready: false,
|
ready: false,
|
||||||
inUse: false,
|
inUse: false,
|
||||||
|
consumed: false,
|
||||||
completed: false,
|
completed: false,
|
||||||
creationTimeStamp,
|
creationTimeStamp,
|
||||||
deathTimeStamp,
|
deathTimeStamp,
|
||||||
@@ -105,15 +110,16 @@ export class SessionController {
|
|||||||
return session
|
return session
|
||||||
}
|
}
|
||||||
|
|
||||||
public async waitForSession(session: Session) {
|
private async waitForSession(session: Session) {
|
||||||
const codeFilePath = path.join(session.path, 'code.sas')
|
const codeFilePath = path.join(session.path, 'code.sas')
|
||||||
|
|
||||||
// TODO: don't wait forever
|
// TODO: don't wait forever
|
||||||
while ((await fileExists(codeFilePath)) && !session.crashed) {}
|
while ((await fileExists(codeFilePath)) && !session.crashed) {}
|
||||||
console.log('session crashed?', !!session.crashed, session.crashed)
|
|
||||||
|
if (session.crashed)
|
||||||
|
console.log('session crashed! while waiting to be ready', session.crashed)
|
||||||
|
|
||||||
session.ready = true
|
session.ready = true
|
||||||
return Promise.resolve(session)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deleteSession(session: Session) {
|
public async deleteSession(session: Session) {
|
||||||
@@ -121,11 +127,9 @@ export class SessionController {
|
|||||||
await deleteFolder(session.path)
|
await deleteFolder(session.path)
|
||||||
|
|
||||||
// remove the session from the session array
|
// remove the session from the session array
|
||||||
if (session.ready) {
|
this.sessions = this.sessions.filter(
|
||||||
this.sessions = this.sessions.filter(
|
(sess: Session) => sess.id !== session.id
|
||||||
(sess: Session) => sess.id !== session.id
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private scheduleSessionDestroy(session: Session) {
|
private scheduleSessionDestroy(session: Session) {
|
||||||
@@ -153,6 +157,7 @@ const autoExecContent = `
|
|||||||
data _null_;
|
data _null_;
|
||||||
/* remove the dummy SYSIN */
|
/* remove the dummy SYSIN */
|
||||||
length fname $8;
|
length fname $8;
|
||||||
|
call missing(fname);
|
||||||
rc=filename(fname,getoption('SYSIN') );
|
rc=filename(fname,getoption('SYSIN') );
|
||||||
if rc = 0 and fexist(fname) then rc=fdelete(fname);
|
if rc = 0 and fexist(fname) then rc=fdelete(fname);
|
||||||
rc=filename(fname);
|
rc=filename(fname);
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export class STPController {
|
|||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
return executeReturnRaw(request, _program)
|
return executeReturnRaw(request, _program)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Trigger a SAS program using it's location in the _program parameter.
|
* Trigger a SAS program using it's location in the _program parameter.
|
||||||
* Enable debugging using the _debug parameter.
|
* Enable debugging using the _debug parameter.
|
||||||
@@ -72,7 +73,7 @@ const executeReturnRaw = async (
|
|||||||
.replace(new RegExp('/', 'g'), path.sep) + '.sas'
|
.replace(new RegExp('/', 'g'), path.sep) + '.sas'
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await new ExecutionController().execute(
|
const result = await new ExecutionController().executeFile(
|
||||||
sasCodePath,
|
sasCodePath,
|
||||||
getPreProgramVariables(req),
|
getPreProgramVariables(req),
|
||||||
query
|
query
|
||||||
@@ -101,7 +102,7 @@ const executeReturnJson = async (
|
|||||||
const filesNamesMap = req.files?.length ? makeFilesNamesMap(req.files) : null
|
const filesNamesMap = req.files?.length ? makeFilesNamesMap(req.files) : null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { webout, log } = (await new ExecutionController().execute(
|
const { webout, log } = (await new ExecutionController().executeFile(
|
||||||
sasCodePath,
|
sasCodePath,
|
||||||
getPreProgramVariables(req),
|
getPreProgramVariables(req),
|
||||||
{ ...req.query, ...req.body },
|
{ ...req.query, ...req.body },
|
||||||
|
|||||||
25
api/src/routes/api/code.ts
Normal file
25
api/src/routes/api/code.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import express from 'express'
|
||||||
|
import { runSASValidation } from '../../utils'
|
||||||
|
import { CodeController } from '../../controllers/'
|
||||||
|
|
||||||
|
const runRouter = express.Router()
|
||||||
|
|
||||||
|
const controller = new CodeController()
|
||||||
|
|
||||||
|
runRouter.post('/execute', async (req, res) => {
|
||||||
|
const { error, value: body } = runSASValidation(req.body)
|
||||||
|
if (error) return res.status(400).send(error.details[0].message)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await controller.executeSASCode(req, body)
|
||||||
|
res.send(response)
|
||||||
|
} catch (err: any) {
|
||||||
|
const statusCode = err.code
|
||||||
|
|
||||||
|
delete err.code
|
||||||
|
|
||||||
|
res.status(statusCode).send(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default runRouter
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
|
|
||||||
import driveRouter from './drive'
|
import driveRouter from './drive'
|
||||||
import stpRouter from './stp'
|
import stpRouter from './stp'
|
||||||
|
import codeRouter from './code'
|
||||||
import userRouter from './user'
|
import userRouter from './user'
|
||||||
import groupRouter from './group'
|
import groupRouter from './group'
|
||||||
import clientRouter from './client'
|
import clientRouter from './client'
|
||||||
@@ -31,6 +32,7 @@ router.use(
|
|||||||
router.use('/drive', authenticateAccessToken, driveRouter)
|
router.use('/drive', authenticateAccessToken, driveRouter)
|
||||||
router.use('/group', desktopRestrict, groupRouter)
|
router.use('/group', desktopRestrict, groupRouter)
|
||||||
router.use('/stp', authenticateAccessToken, stpRouter)
|
router.use('/stp', authenticateAccessToken, stpRouter)
|
||||||
|
router.use('/code', authenticateAccessToken, codeRouter)
|
||||||
router.use('/user', desktopRestrict, userRouter)
|
router.use('/user', desktopRestrict, userRouter)
|
||||||
router.use(
|
router.use(
|
||||||
'/',
|
'/',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { executeProgramRawValidation } from '../../utils'
|
import { executeProgramRawValidation, runSASValidation } from '../../utils'
|
||||||
import { STPController } from '../../controllers/'
|
import { STPController } from '../../controllers/'
|
||||||
import { FileUploadController } from '../../controllers/internal'
|
import { FileUploadController } from '../../controllers/internal'
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export interface Session {
|
|||||||
deathTimeStamp: string
|
deathTimeStamp: string
|
||||||
path: string
|
path: string
|
||||||
inUse: boolean
|
inUse: boolean
|
||||||
|
consumed: boolean
|
||||||
completed: boolean
|
completed: boolean
|
||||||
crashed?: string
|
crashed?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,11 @@ import { getRealPath } from '@sasjs/utils'
|
|||||||
|
|
||||||
export const connectDB = async () => {
|
export const connectDB = async () => {
|
||||||
// NOTE: when exporting app.js as agent for supertest
|
// NOTE: when exporting app.js as agent for supertest
|
||||||
// we should exlcude connecting to the real database
|
// we should exclude connecting to the real database
|
||||||
if (process.env.NODE_ENV !== 'test') {
|
if (process.env.NODE_ENV === 'test') {
|
||||||
|
process.driveLoc = path.join(process.cwd(), 'tmp')
|
||||||
|
return
|
||||||
|
} else {
|
||||||
const { MODE } = process.env
|
const { MODE } = process.env
|
||||||
|
|
||||||
if (MODE?.trim() !== 'server') {
|
if (MODE?.trim() !== 'server') {
|
||||||
|
|||||||
@@ -77,6 +77,11 @@ export const updateFileDriveValidation = (data: any): Joi.ValidationResult =>
|
|||||||
fileContent: Joi.string().required()
|
fileContent: Joi.string().required()
|
||||||
}).validate(data)
|
}).validate(data)
|
||||||
|
|
||||||
|
export const runSASValidation = (data: any): Joi.ValidationResult =>
|
||||||
|
Joi.object({
|
||||||
|
code: Joi.string().required()
|
||||||
|
}).validate(data)
|
||||||
|
|
||||||
export const executeProgramRawValidation = (data: any): Joi.ValidationResult =>
|
export const executeProgramRawValidation = (data: any): Joi.ValidationResult =>
|
||||||
Joi.object({
|
Joi.object({
|
||||||
_program: Joi.string().required()
|
_program: Joi.string().required()
|
||||||
|
|||||||
@@ -38,6 +38,10 @@
|
|||||||
{
|
{
|
||||||
"name": "STP",
|
"name": "STP",
|
||||||
"description": "Operations about STP"
|
"description": "Operations about STP"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "CODE",
|
||||||
|
"description": "Operations on SAS code"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"yaml": true,
|
"yaml": true,
|
||||||
|
|||||||
@@ -14,14 +14,14 @@ services:
|
|||||||
REFRESH_TOKEN_SECRET: ${REFRESH_TOKEN_SECRET}
|
REFRESH_TOKEN_SECRET: ${REFRESH_TOKEN_SECRET}
|
||||||
AUTH_CODE_SECRET: ${AUTH_CODE_SECRET}
|
AUTH_CODE_SECRET: ${AUTH_CODE_SECRET}
|
||||||
DB_CONNECT: mongodb://mongodb:27017/sasjs
|
DB_CONNECT: mongodb://mongodb:27017/sasjs
|
||||||
SAS_PATH: /usr/server/sasexe
|
SAS_PATH: /usr/server/sasexe/${SAS_EXEC_NAME}
|
||||||
expose:
|
expose:
|
||||||
- ${PORT_API}
|
- ${PORT_API}
|
||||||
ports:
|
ports:
|
||||||
- ${PORT_API}:${PORT_API}
|
- ${PORT_API}:${PORT_API}
|
||||||
volumes:
|
volumes:
|
||||||
- type: bind
|
- type: bind
|
||||||
source: ${SAS_EXEC}
|
source: ${SAS_EXEC_PATH}
|
||||||
target: /usr/server/sasexe
|
target: /usr/server/sasexe
|
||||||
read_only: true
|
read_only: true
|
||||||
links:
|
links:
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ services:
|
|||||||
- ./web:/usr/server/web
|
- ./web:/usr/server/web
|
||||||
|
|
||||||
mongodb:
|
mongodb:
|
||||||
image: mongo:latest
|
image: mongo:5.0.4
|
||||||
ports:
|
ports:
|
||||||
- 27017:27017
|
- 27017:27017
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
16
package-lock.json
generated
16
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "server",
|
"name": "server",
|
||||||
"version": "0.0.12",
|
"version": "0.0.25",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "server",
|
"name": "server",
|
||||||
"version": "0.0.12",
|
"version": "0.0.25",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"prettier": "^2.3.1",
|
"prettier": "^2.3.1",
|
||||||
"standard-version": "^9.3.2"
|
"standard-version": "^9.3.2"
|
||||||
@@ -865,9 +865,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/graceful-fs": {
|
"node_modules/graceful-fs": {
|
||||||
"version": "4.2.8",
|
"version": "4.2.9",
|
||||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz",
|
||||||
"integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==",
|
"integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/handlebars": {
|
"node_modules/handlebars": {
|
||||||
@@ -2787,9 +2787,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"graceful-fs": {
|
"graceful-fs": {
|
||||||
"version": "4.2.8",
|
"version": "4.2.9",
|
||||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz",
|
||||||
"integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==",
|
"integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"handlebars": {
|
"handlebars": {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "server",
|
"name": "server",
|
||||||
"version": "0.0.12",
|
"version": "0.0.25",
|
||||||
"description": "NodeJS wrapper for calling the SAS binary executable",
|
"description": "NodeJS wrapper for calling the SAS binary executable",
|
||||||
|
"repository": "https://github.com/sasjs/server",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"server": "npm run server:prepare && npm run server:start",
|
"server": "npm run server:prepare && npm run server:start",
|
||||||
"server:prepare": "cd web && npm ci && npm run build && cd ../api && npm ci && cd ..",
|
"server:prepare": "cd web && npm ci && npm run build && cd ../api && npm ci && cd ..",
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const Home = () => {
|
|||||||
and contributions are welcomed.
|
and contributions are welcomed.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
SASjs Server is maintained by the SASjs Apps team -{' '}
|
SASjs Server is maintained by the SAS Apps team -{' '}
|
||||||
<a
|
<a
|
||||||
href="https://sasapps.io/contact-us"
|
href="https://sasapps.io/contact-us"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
import Editor from '@monaco-editor/react'
|
import Editor from '@monaco-editor/react'
|
||||||
@@ -89,10 +90,10 @@ const Main = (props: any) => {
|
|||||||
style={{ position: 'absolute', left: '50%', top: '50%' }}
|
style={{ position: 'absolute', left: '50%', top: '50%' }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!isLoading && props?.selectedFilePath !== '' && !editMode && (
|
{!isLoading && props?.selectedFilePath && !editMode && (
|
||||||
<code style={{ whiteSpace: 'break-spaces' }}>{fileContent}</code>
|
<code style={{ whiteSpace: 'break-spaces' }}>{fileContent}</code>
|
||||||
)}
|
)}
|
||||||
{!isLoading && props?.selectedFilePath !== '' && editMode && (
|
{!isLoading && props?.selectedFilePath && editMode && (
|
||||||
<Editor
|
<Editor
|
||||||
height="95%"
|
height="95%"
|
||||||
value={fileContent}
|
value={fileContent}
|
||||||
@@ -110,17 +111,26 @@ const Main = (props: any) => {
|
|||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={handleEditSaveBtnClick}
|
onClick={handleEditSaveBtnClick}
|
||||||
disabled={isLoading || props?.selectedFilePath === ''}
|
disabled={isLoading || !props?.selectedFilePath}
|
||||||
>
|
>
|
||||||
{!editMode ? 'Edit' : 'Save'}
|
{!editMode ? 'Edit' : 'Save'}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={handleCancelExecuteBtnClick}
|
onClick={handleCancelExecuteBtnClick}
|
||||||
disabled={isLoading || props?.selectedFilePath === ''}
|
disabled={isLoading || !props?.selectedFilePath}
|
||||||
>
|
>
|
||||||
{editMode ? 'Cancel' : 'Execute'}
|
{editMode ? 'Cancel' : 'Execute'}
|
||||||
</Button>
|
</Button>
|
||||||
|
{props?.selectedFilePath && (
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
component={Link}
|
||||||
|
to={`/SASjsStudio?_program=${props.selectedFilePath}`}
|
||||||
|
>
|
||||||
|
Open in Studio
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,14 +1,165 @@
|
|||||||
import React from 'react'
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
import CssBaseline from '@mui/material/CssBaseline'
|
|
||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
|
import { Button, Paper, Stack, Tab } from '@mui/material'
|
||||||
|
import { makeStyles } from '@mui/styles'
|
||||||
|
import Editor, { OnMount } from '@monaco-editor/react'
|
||||||
|
import { useLocation } from 'react-router-dom'
|
||||||
|
import { TabContext, TabList, TabPanel } from '@mui/lab'
|
||||||
|
|
||||||
|
const useStyles = makeStyles(() => ({
|
||||||
|
root: {
|
||||||
|
fontSize: '1rem',
|
||||||
|
color: 'gray',
|
||||||
|
'&.Mui-selected': {
|
||||||
|
color: 'black'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
const Studio = () => {
|
const Studio = () => {
|
||||||
|
const location = useLocation()
|
||||||
|
const [fileContent, setFileContent] = useState('')
|
||||||
|
const [log, setLog] = useState('')
|
||||||
|
const [webout, setWebout] = useState('')
|
||||||
|
const [tab, setTab] = React.useState('1')
|
||||||
|
const handleTabChange = (_e: any, newValue: string) => {
|
||||||
|
setTab(newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
const editorRef = useRef(null as any)
|
||||||
|
const handleEditorDidMount: OnMount = (editor) => {
|
||||||
|
editor.focus()
|
||||||
|
editorRef.current = editor
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSelection = () => {
|
||||||
|
const editor = editorRef.current as any
|
||||||
|
const selection = editor?.getModel().getValueInRange(editor?.getSelection())
|
||||||
|
return selection ?? ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRunBtnClick = () => runCode(getSelection() || fileContent)
|
||||||
|
|
||||||
|
const runCode = (code: string) => {
|
||||||
|
axios
|
||||||
|
.post(`/SASjsApi/code/execute`, { code })
|
||||||
|
.then((res: any) => {
|
||||||
|
setLog(`<div><h2>SAS Log</h2><pre>${res?.data?.log}</pre></div>`)
|
||||||
|
|
||||||
|
let weboutString: string
|
||||||
|
try {
|
||||||
|
weboutString = res.data.webout
|
||||||
|
.split('>>weboutBEGIN<<')[1]
|
||||||
|
.split('>>weboutEND<<')[0]
|
||||||
|
} catch (_) {
|
||||||
|
weboutString = res?.data?.webout ?? ''
|
||||||
|
}
|
||||||
|
|
||||||
|
let webout: string
|
||||||
|
try {
|
||||||
|
webout = JSON.stringify(JSON.parse(weboutString), null, 4)
|
||||||
|
} catch (_) {
|
||||||
|
webout = weboutString
|
||||||
|
}
|
||||||
|
|
||||||
|
setWebout(`<pre><code>${webout}</code></pre>`)
|
||||||
|
setTab('2')
|
||||||
|
})
|
||||||
|
.catch((err) => console.log(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const content = localStorage.getItem('fileContent') ?? ''
|
||||||
|
setFileContent(content)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (fileContent.length) {
|
||||||
|
localStorage.setItem('fileContent', fileContent)
|
||||||
|
}
|
||||||
|
}, [fileContent])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const params = new URLSearchParams(location.search)
|
||||||
|
const programPath = params.get('_program')
|
||||||
|
|
||||||
|
if (programPath?.length)
|
||||||
|
axios
|
||||||
|
.get(`/SASjsApi/drive/file?filePath=${programPath}`)
|
||||||
|
.then((res: any) => setFileContent(res.data.fileContent))
|
||||||
|
.catch((err) => console.log(err))
|
||||||
|
}, [location.search])
|
||||||
|
|
||||||
|
const classes = useStyles()
|
||||||
return (
|
return (
|
||||||
<Box className="main">
|
<>
|
||||||
<CssBaseline />
|
<br />
|
||||||
<h2>This is container for SASjs studio</h2>
|
<br />
|
||||||
</Box>
|
<br />
|
||||||
|
<Box sx={{ width: '100%', typography: 'body1' }}>
|
||||||
|
<TabContext value={tab}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
borderBottom: 1,
|
||||||
|
borderColor: 'divider'
|
||||||
|
}}
|
||||||
|
style={{ position: 'fixed', background: 'white', width: '100%' }}
|
||||||
|
>
|
||||||
|
<TabList onChange={handleTabChange} centered>
|
||||||
|
<Tab className={classes.root} label="Code" value="1" />
|
||||||
|
<Tab className={classes.root} label="Log" value="2" />
|
||||||
|
<Tab className={classes.root} label="Webout" value="3" />
|
||||||
|
</TabList>
|
||||||
|
</Box>
|
||||||
|
<TabPanel value="1">
|
||||||
|
{/* <Toolbar /> */}
|
||||||
|
<Paper
|
||||||
|
sx={{
|
||||||
|
height: '70vh',
|
||||||
|
marginTop: '50px',
|
||||||
|
padding: '10px',
|
||||||
|
overflow: 'auto',
|
||||||
|
position: 'relative'
|
||||||
|
}}
|
||||||
|
elevation={3}
|
||||||
|
>
|
||||||
|
<Editor
|
||||||
|
height="95%"
|
||||||
|
value={fileContent}
|
||||||
|
onMount={handleEditorDidMount}
|
||||||
|
onChange={(val) => {
|
||||||
|
if (val) setFileContent(val)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Paper>
|
||||||
|
<Stack
|
||||||
|
spacing={3}
|
||||||
|
direction="row"
|
||||||
|
sx={{ justifyContent: 'center', marginTop: '20px' }}
|
||||||
|
>
|
||||||
|
<Button variant="contained" onClick={handleRunBtnClick}>
|
||||||
|
Run SAS Code
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel value="2">
|
||||||
|
<div
|
||||||
|
id="sas_log"
|
||||||
|
style={{ marginTop: '50px' }}
|
||||||
|
dangerouslySetInnerHTML={{ __html: log }}
|
||||||
|
/>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel value="3">
|
||||||
|
<div
|
||||||
|
style={{ marginTop: '50px' }}
|
||||||
|
dangerouslySetInnerHTML={{ __html: webout }}
|
||||||
|
/>
|
||||||
|
</TabPanel>
|
||||||
|
</TabContext>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user