mirror of
https://github.com/sasjs/server.git
synced 2025-12-10 19:34:34 +00:00
Compare commits
105 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67d200d817 | ||
|
|
a0c27ea8d3 | ||
|
|
3d583ff21d | ||
|
|
7072e282b1 | ||
|
|
145ac45036 | ||
|
|
698180ab7e | ||
|
|
0f4e38d51d | ||
|
|
e76283daa4 | ||
|
|
6ab42ca486 | ||
|
|
fa4da7624b | ||
|
|
9f5509d2d4 | ||
|
|
efaf38d303 | ||
|
|
95843fa4c7 | ||
|
|
5ba7661a83 | ||
|
|
ed5c58e10e | ||
|
|
5fce7d8f71 | ||
|
|
feeec4eb14 | ||
|
|
8c1941a87b | ||
|
|
765969db11 | ||
|
|
e60f17268d | ||
|
|
ce0a5e1229 | ||
|
|
c5738792b0 | ||
|
|
94e036dd10 | ||
|
|
da375b8086 | ||
|
|
7312763339 | ||
|
|
5005f203b8 | ||
|
|
232a73fd17 | ||
|
|
ef41691e40 | ||
|
|
3e6234e601 | ||
|
|
0a4b202428 | ||
|
|
a11893ece1 | ||
|
|
c5ad72c931 | ||
|
|
034f3173bd | ||
|
|
e2a6810e95 | ||
|
|
373d66f8af | ||
|
|
0b5f958f45 | ||
|
|
da899b90e2 | ||
|
|
2c4aa420b3 | ||
|
|
cd32912379 | ||
|
|
93dcb1753b | ||
|
|
35cf301905 | ||
|
|
5931fc1e71 | ||
|
|
18d845799c | ||
|
|
8c872bde92 | ||
|
|
f953472efd | ||
|
|
f10138b0f2 | ||
|
|
6f19d3d0ea | ||
|
|
a7facb005a | ||
|
|
88acf9df5d | ||
|
|
b0880b142a | ||
|
|
d3674c7f94 | ||
|
|
adccca6c7f | ||
|
|
8b83ccc4c2 | ||
|
|
556944b1d5 | ||
|
|
b14e07ee6e | ||
|
|
048bd9f78c | ||
|
|
d7e1aca7e3 | ||
|
|
de47d78a00 | ||
|
|
58b6f439b3 | ||
|
|
ce9bde5717 | ||
|
|
0cfe724ffa | ||
|
|
fde4bc051d | ||
|
|
367b0f1f89 | ||
|
|
d17a3dd590 | ||
|
|
bee5deed2a | ||
|
|
e6e46838b3 | ||
|
|
404f1ec059 | ||
|
|
09d36bc754 | ||
|
|
3722bbaec3 | ||
|
|
480ee4da83 | ||
|
|
dd853fe13b | ||
|
|
e1142a33a0 | ||
|
|
d4e8d91cae | ||
|
|
9a74ec545d | ||
|
|
f2000a1227 | ||
|
|
bf5767eadf | ||
|
|
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 |
@@ -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
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,6 +7,7 @@ sas/
|
|||||||
tmp/
|
tmp/
|
||||||
build/
|
build/
|
||||||
sasjsbuild/
|
sasjsbuild/
|
||||||
|
sasjscore/
|
||||||
certificates/
|
certificates/
|
||||||
executables/
|
executables/
|
||||||
.env
|
.env
|
||||||
|
|||||||
10
.gitpod.yml
Normal file
10
.gitpod.yml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# This configuration file was automatically generated by Gitpod.
|
||||||
|
# Please adjust to your needs (see https://www.gitpod.io/docs/config-gitpod-file)
|
||||||
|
# and commit this file to your remote git repository to share the goodness with others.
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- init: npm install
|
||||||
|
vscode:
|
||||||
|
extensions:
|
||||||
|
- dbaeumer.vscode-eslint
|
||||||
|
- sasjs.sasjs-for-vscode
|
||||||
150
CHANGELOG.md
150
CHANGELOG.md
@@ -2,6 +2,156 @@
|
|||||||
|
|
||||||
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.31](https://github.com/sasjs/server/compare/v0.0.30...v0.0.31) (2022-03-14)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **drive:** new route delete file api ([3d583ff](https://github.com/sasjs/server/commit/3d583ff21d344a71aa861c7e5b1426ebc2d54c22))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* added cookie for accessToken ([698180a](https://github.com/sasjs/server/commit/698180ab7e44d67d46c84352ececca5b6c83b230))
|
||||||
|
* **drive:** update file API is same as create file ([7072e28](https://github.com/sasjs/server/commit/7072e282b1cd1a296d81512c57130237610c1c1e))
|
||||||
|
* show content of get file api ([6ab42ca](https://github.com/sasjs/server/commit/6ab42ca4868366874f5f21bd711b7b8b72e36774))
|
||||||
|
* **stp:** return plain/text header for GET & debug ([145ac45](https://github.com/sasjs/server/commit/145ac450365ed39279248ec9321bbe4918bee9fa))
|
||||||
|
|
||||||
|
### [0.0.30](https://github.com/sasjs/server/compare/v0.0.29...v0.0.30) (2022-03-05)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* parse log to array ([c5ad72c](https://github.com/sasjs/server/commit/c5ad72c931ec8fbd7d5a6475838adcbd380c8aee))
|
||||||
|
* set response headers provded by SAS Code execution ([2c4aa42](https://github.com/sasjs/server/commit/2c4aa420b3119890cafde4265ed5dddbc9d6a636))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* added http headers to /code api as well ([da899b9](https://github.com/sasjs/server/commit/da899b90e26d5ee393eefc302be985eb7c9055a5))
|
||||||
|
* code api is updated return type ([e2a6810](https://github.com/sasjs/server/commit/e2a6810e9531a8102d3c51fd8df2e1f78f0d965f))
|
||||||
|
* **file:** fixes response headers ([ef41691](https://github.com/sasjs/server/commit/ef41691e408ef1c1c7a921cc1050bdd533651331))
|
||||||
|
* get file instead of it's content ([efaf38d](https://github.com/sasjs/server/commit/efaf38d3039391392ce0e14a3accddd8f34ea7d6))
|
||||||
|
* hot fix for web component ([0a4b202](https://github.com/sasjs/server/commit/0a4b202428e14effc8014a6813cecf7761ce3715))
|
||||||
|
* improvement in flow of uploading ([8c1941a](https://github.com/sasjs/server/commit/8c1941a87bc184be4e0e09eeff73fc6cb69e3041))
|
||||||
|
* macros are available Sessions with SASAUTOS ([95843fa](https://github.com/sasjs/server/commit/95843fa4c711aa695ee63ad265b8def4ba56360d))
|
||||||
|
* minor changes ([0b5f958](https://github.com/sasjs/server/commit/0b5f958f456d291ec7a8697236657c7819d5c654))
|
||||||
|
* multi-part file upload + validations + specs ([e60f172](https://github.com/sasjs/server/commit/e60f17268d1fa9ab623313026d46bd3f63756f69))
|
||||||
|
* organized code for usage of multer ([ce0a5e1](https://github.com/sasjs/server/commit/ce0a5e1229bed69c450061fac2bc19711448da56))
|
||||||
|
* return buffer in case of file response ([3e6234e](https://github.com/sasjs/server/commit/3e6234e6019c5f3ae4280fac079ecc9cb0effc07))
|
||||||
|
* **stp:** return json for webout ([5005f20](https://github.com/sasjs/server/commit/5005f203b8d6b1d577cdf094b83886bd1fc817a2))
|
||||||
|
* updating docs ([7312763](https://github.com/sasjs/server/commit/7312763339d6769826328561e2c8d11bbfc0c9f4))
|
||||||
|
* **upload:** added query param as well for filepath ([feeec4e](https://github.com/sasjs/server/commit/feeec4eb149e9a47e5a52320d1fc95243bf5eb15))
|
||||||
|
|
||||||
|
### [0.0.29](https://github.com/sasjs/server/compare/v0.0.28...v0.0.29) (2022-02-16)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* adding .. in folder path ([5931fc1](https://github.com/sasjs/server/commit/5931fc1e712c545ef80454dea5b36e684017c367))
|
||||||
|
* adding sasjs stpsrv_header() path to autoexec. Relates to [#58](https://github.com/sasjs/server/issues/58) ([ce9bde5](https://github.com/sasjs/server/commit/ce9bde5717369de2d76dc183319be8830b2362b2))
|
||||||
|
|
||||||
|
### [0.0.28](https://github.com/sasjs/server/compare/v0.0.27...v0.0.28) (2022-02-16)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* default macros and bumping core ([6f19d3d](https://github.com/sasjs/server/commit/6f19d3d0ea3815815f246a3e455495c72c8604c7))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* moving core ([f10138b](https://github.com/sasjs/server/commit/f10138b0f2005a958f63cb3a8351e1afa52f086a))
|
||||||
|
|
||||||
|
### [0.0.27](https://github.com/sasjs/server/compare/v0.0.26...v0.0.27) (2022-02-16)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* removing stpsrv_header and updating README with auth details ([d3674c7](https://github.com/sasjs/server/commit/d3674c7f9449d77977e482cd63ccdf7e974fa838))
|
||||||
|
* **stp-execution:** add returnLog option to execution query ([bf5767e](https://github.com/sasjs/server/commit/bf5767eadfb87f7ed902659347a18361a6a6c74b))
|
||||||
|
|
||||||
|
### [0.0.26](https://github.com/sasjs/server/compare/v0.0.25...v0.0.26) (2022-02-14)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* refactored + removed unused package ([d7e1aca](https://github.com/sasjs/server/commit/d7e1aca7e33c3264c784d406fa766e29a6b15ae9))
|
||||||
|
* release should also has https protocol ([0cfe724](https://github.com/sasjs/server/commit/0cfe724ffa089b84a9f8bca49c9033b56f51c9cb))
|
||||||
|
* updated token expiry times ([d17a3dd](https://github.com/sasjs/server/commit/d17a3dd5900d5eb88120af8575e3fc7c2cb71ed6))
|
||||||
|
|
||||||
|
### [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)
|
### [0.0.15](https://github.com/sasjs/server/compare/v0.0.14...v0.0.15) (2022-01-06)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
141
README.md
141
README.md
@@ -8,119 +8,94 @@ 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).
|
||||||
|
|
||||||
## Installation
|
SASjs Server is available in two modes - Desktop (without authentication) and Server (with authentiation, and a database)
|
||||||
|
|
||||||
Just download the relevant package from the [releases](https://github.com/sasjs/server/releases) page and trigger, either by double clicking (windows) or executing from commandline.
|
|
||||||
|
|
||||||
You are presented with two prompts:
|
|
||||||
|
|
||||||
* Location of your `sas.exe` / `sas.sh` executable
|
|
||||||
* Path to a filesystem location for Stored Programs and temporary files
|
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Configuration is made in the `configuration` section of `package.json`:
|
When launching the app, it will make use of specific environment variables. These can be set in the following places:
|
||||||
|
|
||||||
- Provide path to SAS9 executable.
|
- 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
|
||||||
|
|
||||||
### Using dockers:
|
Example variables:
|
||||||
|
|
||||||
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
|
MODE=[desktop|server] default considered as desktop
|
||||||
|
CORS=[disable|enable] default considered as disable
|
||||||
|
PROTOCOL=[http|https] default considered as http
|
||||||
|
PORT=[5000] default value is 5000
|
||||||
|
PORT_WEB=[port for sasjs web component(react)] default value is 3000
|
||||||
|
SAS_PATH=/path/to/sas/executable.exe
|
||||||
|
DRIVE_PATH=./tmp
|
||||||
|
PROTOCOL=[http|https] default considered as http. Use pems below if htttps.
|
||||||
|
PRIVATE_KEY=privkey.pem
|
||||||
|
FULL_CHAIN=fullchain.pem
|
||||||
```
|
```
|
||||||
|
|
||||||
It uses default docker compose file i.e. `docker-compose.yml` present at root.
|
## Desktop Version
|
||||||
It will build following images if running first time:
|
|
||||||
|
|
||||||
- `sasjs_server_api` - image for sasjs api server app based on _ExpressJS_
|
### Manual Installation
|
||||||
- `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
|
Download the relevant package from the [releases](https://github.com/sasjs/server/releases) page
|
||||||
|
|
||||||
Command to run docker for production:
|
Next, trigger by double clicking (windows) or executing from commandline.
|
||||||
|
|
||||||
```
|
You are presented with two prompts (if not set as ENV vars):
|
||||||
docker-compose -f docker-compose.prod.yml up -d
|
|
||||||
|
- Location of your `sas.exe` / `sas.sh` executable
|
||||||
|
- Path to a filesystem location for Stored Programs and temporary files
|
||||||
|
|
||||||
|
## Programmatic Installation
|
||||||
|
|
||||||
|
Fetch the relevant package from github using `curl`, eg as follows (for linux):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
|
||||||
|
unzip linux.zip
|
||||||
```
|
```
|
||||||
|
|
||||||
It uses specified docker compose file i.e. `docker-compose.prod.yml` present at root.
|
The app can then be launched with `./api-linux` and prompts followed (if ENV vars not set).
|
||||||
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 [pm2](https://www.npmjs.com/package/pm2) (`npm install pm2@latest -g`) 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):
|
pm2 start 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 logs (and some usefull commands):
|
||||||
|
|
||||||
Navigate to `./web`
|
```bash
|
||||||
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
|
pm2 [list|ls|status]
|
||||||
Command to install and run api server.
|
pm2 logs
|
||||||
|
pm2 logs --lines 200
|
||||||
```
|
|
||||||
npm install
|
|
||||||
npm start
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Development (running only api server and have web build served):
|
Managing processes:
|
||||||
|
|
||||||
##### 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 ../
|
pm2 restart app_name
|
||||||
cd ./api && npm i && npm start
|
pm2 reload app_name
|
||||||
|
pm2 stop app_name
|
||||||
|
pm2 delete app_name
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Production
|
Instead of `app_name` you can pass:
|
||||||
|
|
||||||
##### API & WEB
|
- `all` to act on all processes
|
||||||
|
- `id` to act on a specific process id
|
||||||
|
|
||||||
```
|
|
||||||
npm run server
|
|
||||||
```
|
|
||||||
|
|
||||||
This will install/build `web` and install `api`, then start prod server.
|
## Server Version
|
||||||
|
|
||||||
## Executables
|
The following credentials can be used for the initial connection to SASjs/server. It is recommended to change these on first use.
|
||||||
|
|
||||||
Command to generate executables
|
* CLIENTID: `clientID1`
|
||||||
|
* USERNAME: `secretuser`
|
||||||
```
|
* PASSWORD: `secretpassword`
|
||||||
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>
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
MODE=[desktop|server] default considered as desktop
|
MODE=[desktop|server] default considered as desktop
|
||||||
CORS=[disable|enable] default considered as disable
|
CORS=[disable|enable] default considered as disable
|
||||||
|
PROTOCOL=[http|https] default considered as http
|
||||||
|
PRIVATE_KEY=privkey.pem
|
||||||
|
FULL_CHAIN=fullchain.pem
|
||||||
PORT=[5000] default value is 5000
|
PORT=[5000] default value is 5000
|
||||||
PORT_WEB=[port for sasjs web component(react)] default value is 3000
|
PORT_WEB=[port for sasjs web component(react)] default value is 3000
|
||||||
ACCESS_TOKEN_SECRET=<secret>
|
ACCESS_TOKEN_SECRET=<secret>
|
||||||
|
|||||||
13
api/.vscode/launch.json
vendored
Normal file
13
api/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Launch via NPM",
|
||||||
|
"request": "launch",
|
||||||
|
"runtimeArgs": ["run-script", "start"],
|
||||||
|
"runtimeExecutable": "npm",
|
||||||
|
"skipFiles": ["<node_internals>/**"],
|
||||||
|
"type": "pwa-node"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
10213
api/package-lock.json
generated
10213
api/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,29 +1,28 @@
|
|||||||
{
|
{
|
||||||
"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": {
|
||||||
"initial": "npm run swagger && npm run compileSysInit",
|
"initial": "npm run swagger && npm run compileSysInit && npm run copySASjsCore",
|
||||||
"prestart": "npm run initial",
|
"prestart": "npm run initial",
|
||||||
"prestart:prod": "npm run initial",
|
|
||||||
"prebuild": "npm run initial",
|
"prebuild": "npm run initial",
|
||||||
"start": "nodemon ./src/server.ts",
|
"start": "nodemon ./src/server.ts",
|
||||||
"start:prod": "nodemon ./src/prod-server.ts",
|
|
||||||
"build": "rimraf build && tsc",
|
"build": "rimraf build && tsc",
|
||||||
"swagger": "tsoa spec",
|
"swagger": "tsoa spec",
|
||||||
"semantic-release": "semantic-release -d",
|
|
||||||
"prepare": "[ -d .git ] && git config core.hooksPath ./.git-hooks || true",
|
"prepare": "[ -d .git ] && git config core.hooksPath ./.git-hooks || true",
|
||||||
"test": "mkdir -p tmp && mkdir -p ../web/build && jest --silent --coverage",
|
"test": "mkdir -p tmp && mkdir -p ../web/build && jest --silent --coverage",
|
||||||
"lint:fix": "npx prettier --write \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
"lint:fix": "npx prettier --write \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
||||||
"lint": "npx prettier --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
"lint": "npx prettier --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
||||||
"package:lib": "npm run build && cp ./package.json build && cp README.md build && cd build && npm version \"5.0.0\" && npm pack",
|
"package:lib": "npm run build && cp ./package.json build && cp README.md build && cd build && npm version \"5.0.0\" && npm pack",
|
||||||
"exe": "npm run build && npm run exe:copy && pkg .",
|
"exe": "npm run build && npm run exe:copy && pkg .",
|
||||||
"exe:copy": "npm run public:copy && npm run sasjsbuild:copy && npm run web:copy",
|
"exe:copy": "npm run public:copy && npm run sasjsbuild:copy && npm run sasjscore:copy && npm run web:copy",
|
||||||
"public:copy": "cp -r ./public/ ./build/public/",
|
"public:copy": "cp -r ./public/ ./build/public/",
|
||||||
"sasjsbuild:copy": "cp -r ./sasjsbuild/ ./build/sasjsbuild/",
|
"sasjsbuild:copy": "cp -r ./sasjsbuild/ ./build/sasjsbuild/",
|
||||||
|
"sasjscore:copy": "cp -r ./sasjscore/ ./build/sasjscore/",
|
||||||
"web:copy": "rimraf web && mkdir web && cp -r ../web/build/ ./web/build/",
|
"web:copy": "rimraf web && mkdir web && cp -r ../web/build/ ./web/build/",
|
||||||
"compileSysInit": "ts-node ./scripts/compileSysInit.ts"
|
"compileSysInit": "ts-node ./scripts/compileSysInit.ts",
|
||||||
|
"copySASjsCore": "ts-node ./scripts/copySASjsCore.ts"
|
||||||
},
|
},
|
||||||
"bin": "./build/src/server.js",
|
"bin": "./build/src/server.js",
|
||||||
"pkg": {
|
"pkg": {
|
||||||
@@ -44,11 +43,12 @@
|
|||||||
"main"
|
"main"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"author": "Analytium Ltd",
|
"author": "4GL Ltd",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sasjs/core": "^3.0.2",
|
"@sasjs/core": "4.9.0",
|
||||||
"@sasjs/utils": "2.34.1",
|
"@sasjs/utils": "2.34.1",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
|
"cookie-parser": "^1.4.6",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"joi": "^17.4.2",
|
"joi": "^17.4.2",
|
||||||
@@ -58,10 +58,11 @@
|
|||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"multer": "^1.4.3",
|
"multer": "^1.4.3",
|
||||||
"swagger-ui-express": "^4.1.6",
|
"swagger-ui-express": "^4.1.6",
|
||||||
"tsoa": "^3.14.0"
|
"tsoa": "3.14.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bcryptjs": "^2.4.2",
|
"@types/bcryptjs": "^2.4.2",
|
||||||
|
"@types/cookie-parser": "^1.4.2",
|
||||||
"@types/cors": "^2.8.12",
|
"@types/cors": "^2.8.12",
|
||||||
"@types/express": "^4.17.12",
|
"@types/express": "^4.17.12",
|
||||||
"@types/jest": "^26.0.24",
|
"@types/jest": "^26.0.24",
|
||||||
@@ -73,13 +74,13 @@
|
|||||||
"@types/supertest": "^2.0.11",
|
"@types/supertest": "^2.0.11",
|
||||||
"@types/swagger-ui-express": "^4.1.3",
|
"@types/swagger-ui-express": "^4.1.3",
|
||||||
"dotenv": "^10.0.0",
|
"dotenv": "^10.0.0",
|
||||||
|
"http-headers-validation": "^0.0.1",
|
||||||
"jest": "^27.0.6",
|
"jest": "^27.0.6",
|
||||||
"mongodb-memory-server": "^8.0.0",
|
"mongodb-memory-server": "^8.0.0",
|
||||||
"nodemon": "^2.0.7",
|
"nodemon": "^2.0.7",
|
||||||
"pkg": "^5.4.1",
|
"pkg": "5.5.2",
|
||||||
"prettier": "^2.3.1",
|
"prettier": "^2.3.1",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"semantic-release": "^17.4.3",
|
|
||||||
"supertest": "^6.1.3",
|
"supertest": "^6.1.3",
|
||||||
"ts-jest": "^27.0.3",
|
"ts-jest": "^27.0.3",
|
||||||
"ts-node": "^10.0.0",
|
"ts-node": "^10.0.0",
|
||||||
@@ -88,4 +89,4 @@
|
|||||||
"configuration": {
|
"configuration": {
|
||||||
"sasPath": "/opt/sas/sas9/SASHome/SASFoundation/9.4/sas"
|
"sasPath": "/opt/sas/sas9/SASHome/SASFoundation/9.4/sas"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,6 +92,48 @@ components:
|
|||||||
- clientSecret
|
- clientSecret
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
|
IRecordOfAny:
|
||||||
|
properties: {}
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
|
LogLine:
|
||||||
|
properties:
|
||||||
|
line:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- line
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
HTTPHeaders:
|
||||||
|
properties: {}
|
||||||
|
type: object
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
ExecuteReturnJsonResponse:
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
_webout:
|
||||||
|
anyOf:
|
||||||
|
-
|
||||||
|
type: string
|
||||||
|
-
|
||||||
|
$ref: '#/components/schemas/IRecordOfAny'
|
||||||
|
log:
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/LogLine'
|
||||||
|
type: array
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
httpHeaders:
|
||||||
|
$ref: '#/components/schemas/HTTPHeaders'
|
||||||
|
required:
|
||||||
|
- status
|
||||||
|
- _webout
|
||||||
|
- log
|
||||||
|
- httpHeaders
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
ExecuteSASCodePayload:
|
ExecuteSASCodePayload:
|
||||||
properties:
|
properties:
|
||||||
code:
|
code:
|
||||||
@@ -181,18 +223,6 @@ components:
|
|||||||
- fileTree
|
- fileTree
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
GetFileResponse:
|
|
||||||
properties:
|
|
||||||
status:
|
|
||||||
type: string
|
|
||||||
fileContent:
|
|
||||||
type: string
|
|
||||||
message:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- status
|
|
||||||
type: object
|
|
||||||
additionalProperties: false
|
|
||||||
UpdateFileResponse:
|
UpdateFileResponse:
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
@@ -203,21 +233,6 @@ components:
|
|||||||
- status
|
- status
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
FilePayload:
|
|
||||||
properties:
|
|
||||||
filePath:
|
|
||||||
type: string
|
|
||||||
description: 'Path of the file'
|
|
||||||
example: /Public/somefolder/some.file
|
|
||||||
fileContent:
|
|
||||||
type: string
|
|
||||||
description: 'Contents of the file'
|
|
||||||
example: 'Contents of the File'
|
|
||||||
required:
|
|
||||||
- filePath
|
|
||||||
- fileContent
|
|
||||||
type: object
|
|
||||||
additionalProperties: false
|
|
||||||
TreeNode:
|
TreeNode:
|
||||||
properties:
|
properties:
|
||||||
name:
|
name:
|
||||||
@@ -368,21 +383,6 @@ components:
|
|||||||
- description
|
- description
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
ExecuteReturnJsonResponse:
|
|
||||||
properties:
|
|
||||||
status:
|
|
||||||
type: string
|
|
||||||
_webout:
|
|
||||||
type: string
|
|
||||||
log:
|
|
||||||
type: string
|
|
||||||
message:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- status
|
|
||||||
- _webout
|
|
||||||
type: object
|
|
||||||
additionalProperties: false
|
|
||||||
ExecuteReturnJsonPayload:
|
ExecuteReturnJsonPayload:
|
||||||
properties:
|
properties:
|
||||||
_program:
|
_program:
|
||||||
@@ -398,10 +398,10 @@ components:
|
|||||||
bearerFormat: JWT
|
bearerFormat: JWT
|
||||||
info:
|
info:
|
||||||
title: api
|
title: api
|
||||||
version: 0.0.1
|
version: 0.0.2
|
||||||
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:
|
||||||
@@ -520,7 +520,7 @@ paths:
|
|||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: string
|
$ref: '#/components/schemas/ExecuteReturnJsonResponse'
|
||||||
description: 'Execute SAS code.'
|
description: 'Execute SAS code.'
|
||||||
summary: 'Run SAS Code and returns log'
|
summary: 'Run SAS Code and returns log'
|
||||||
tags:
|
tags:
|
||||||
@@ -583,24 +583,9 @@ paths:
|
|||||||
get:
|
get:
|
||||||
operationId: GetFile
|
operationId: GetFile
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'204':
|
||||||
description: Ok
|
description: 'No content'
|
||||||
content:
|
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."
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/GetFileResponse'
|
|
||||||
examples:
|
|
||||||
'Example 1':
|
|
||||||
value: {status: success, fileContent: 'Contents of the File'}
|
|
||||||
'400':
|
|
||||||
description: 'Unable to get File'
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/GetFileResponse'
|
|
||||||
examples:
|
|
||||||
'Example 1':
|
|
||||||
value: {status: failure, message: 'File request failed.'}
|
|
||||||
summary: 'Get file from SASjs Drive'
|
summary: 'Get file from SASjs Drive'
|
||||||
tags:
|
tags:
|
||||||
- Drive
|
- Drive
|
||||||
@@ -610,11 +595,57 @@ paths:
|
|||||||
parameters:
|
parameters:
|
||||||
-
|
-
|
||||||
in: query
|
in: query
|
||||||
name: filePath
|
name: _filePath
|
||||||
required: true
|
required: false
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
example: /Public/somefolder/some.file
|
example: /Public/somefolder/some.file
|
||||||
|
requestBody:
|
||||||
|
required: false
|
||||||
|
content:
|
||||||
|
multipart/form-data:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
filePath:
|
||||||
|
type: string
|
||||||
|
delete:
|
||||||
|
operationId: DeleteFile
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
properties:
|
||||||
|
status: {type: string}
|
||||||
|
required:
|
||||||
|
- status
|
||||||
|
type: object
|
||||||
|
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."
|
||||||
|
summary: 'Delete file from SASjs Drive'
|
||||||
|
tags:
|
||||||
|
- Drive
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
|
parameters:
|
||||||
|
-
|
||||||
|
in: query
|
||||||
|
name: _filePath
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: /Public/somefolder/some.file
|
||||||
|
requestBody:
|
||||||
|
required: false
|
||||||
|
content:
|
||||||
|
multipart/form-data:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
filePath:
|
||||||
|
type: string
|
||||||
post:
|
post:
|
||||||
operationId: SaveFile
|
operationId: SaveFile
|
||||||
responses:
|
responses:
|
||||||
@@ -627,7 +658,7 @@ paths:
|
|||||||
examples:
|
examples:
|
||||||
'Example 1':
|
'Example 1':
|
||||||
value: {status: success}
|
value: {status: success}
|
||||||
'400':
|
'403':
|
||||||
description: 'File already exists'
|
description: 'File already exists'
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
@@ -636,19 +667,36 @@ 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."
|
||||||
summary: 'Create a file in SASjs Drive'
|
summary: 'Create a file in SASjs Drive'
|
||||||
tags:
|
tags:
|
||||||
- Drive
|
- Drive
|
||||||
security:
|
security:
|
||||||
-
|
-
|
||||||
bearerAuth: []
|
bearerAuth: []
|
||||||
parameters: []
|
parameters:
|
||||||
|
-
|
||||||
|
description: 'Location of SAS program'
|
||||||
|
in: query
|
||||||
|
name: _filePath
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: /Public/somefolder/some.file.sas
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
content:
|
content:
|
||||||
application/json:
|
multipart/form-data:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/FilePayload'
|
type: object
|
||||||
|
properties:
|
||||||
|
file:
|
||||||
|
type: string
|
||||||
|
format: binary
|
||||||
|
filePath:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- file
|
||||||
patch:
|
patch:
|
||||||
operationId: UpdateFile
|
operationId: UpdateFile
|
||||||
responses:
|
responses:
|
||||||
@@ -661,8 +709,8 @@ paths:
|
|||||||
examples:
|
examples:
|
||||||
'Example 1':
|
'Example 1':
|
||||||
value: {status: success}
|
value: {status: success}
|
||||||
'400':
|
'403':
|
||||||
description: 'Unable to get File'
|
description: ""
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
@@ -670,19 +718,36 @@ 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."
|
||||||
summary: 'Modify a file in SASjs Drive'
|
summary: 'Modify a file in SASjs Drive'
|
||||||
tags:
|
tags:
|
||||||
- Drive
|
- Drive
|
||||||
security:
|
security:
|
||||||
-
|
-
|
||||||
bearerAuth: []
|
bearerAuth: []
|
||||||
parameters: []
|
parameters:
|
||||||
|
-
|
||||||
|
description: 'Location of SAS program'
|
||||||
|
in: query
|
||||||
|
name: _filePath
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: /Public/somefolder/some.file.sas
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
content:
|
content:
|
||||||
application/json:
|
multipart/form-data:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/FilePayload'
|
type: object
|
||||||
|
properties:
|
||||||
|
file:
|
||||||
|
type: string
|
||||||
|
format: binary
|
||||||
|
filePath:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- file
|
||||||
/SASjsApi/drive/filetree:
|
/SASjsApi/drive/filetree:
|
||||||
get:
|
get:
|
||||||
operationId: GetFileTree
|
operationId: GetFileTree
|
||||||
@@ -1035,9 +1100,11 @@ paths:
|
|||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: string
|
anyOf:
|
||||||
description: "Trigger a SAS program using it's location in the _program parameter.\nEnable debugging using the _debug parameter.\nAdditional URL parameters are turned into SAS macro variables.\nAny files provided are placed into the session and\ncorresponding _WEBIN_XXX variables are created."
|
- {type: string}
|
||||||
summary: 'Execute Stored Program, return raw content'
|
- {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."
|
||||||
|
summary: 'Execute Stored Program, return raw _webout content.'
|
||||||
tags:
|
tags:
|
||||||
- STP
|
- STP
|
||||||
security:
|
security:
|
||||||
@@ -1045,6 +1112,7 @@ paths:
|
|||||||
bearerAuth: []
|
bearerAuth: []
|
||||||
parameters:
|
parameters:
|
||||||
-
|
-
|
||||||
|
description: 'Location of SAS program'
|
||||||
in: query
|
in: query
|
||||||
name: _program
|
name: _program
|
||||||
required: true
|
required: true
|
||||||
@@ -1060,7 +1128,10 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/ExecuteReturnJsonResponse'
|
$ref: '#/components/schemas/ExecuteReturnJsonResponse'
|
||||||
description: "Trigger a SAS program using it's location in the _program parameter.\nEnable debugging using the _debug parameter.\nAdditional URL parameters are turned into SAS macro variables.\nAny files provided are placed into the session and\ncorresponding _WEBIN_XXX variables are created."
|
examples:
|
||||||
|
'Example 1':
|
||||||
|
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."
|
||||||
summary: 'Execute Stored Program, return JSON'
|
summary: 'Execute Stored Program, return JSON'
|
||||||
tags:
|
tags:
|
||||||
- STP
|
- STP
|
||||||
@@ -1069,6 +1140,7 @@ paths:
|
|||||||
bearerAuth: []
|
bearerAuth: []
|
||||||
parameters:
|
parameters:
|
||||||
-
|
-
|
||||||
|
description: 'Location of SAS program'
|
||||||
in: query
|
in: query
|
||||||
name: _program
|
name: _program
|
||||||
required: false
|
required: false
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ const compiledSystemInit = async (systemInit: string) =>
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
const createSysInitFile = async () => {
|
const createSysInitFile = async () => {
|
||||||
console.log('macroCorePath', macroCorePath)
|
|
||||||
const systemInitContent = await readFile(
|
const systemInitContent = await readFile(
|
||||||
path.join(__dirname, 'systemInit.sas')
|
path.join(__dirname, 'systemInit.sas')
|
||||||
)
|
)
|
||||||
|
|||||||
21
api/scripts/copySASjsCore.ts
Normal file
21
api/scripts/copySASjsCore.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import path from 'path'
|
||||||
|
import { asyncForEach, copy, createFolder, deleteFolder } from '@sasjs/utils'
|
||||||
|
|
||||||
|
import { apiRoot, sasJSCoreMacros } from '../src/utils'
|
||||||
|
|
||||||
|
const macroCorePath = path.join(apiRoot, 'node_modules', '@sasjs', 'core')
|
||||||
|
|
||||||
|
export const copySASjsCore = async () => {
|
||||||
|
await deleteFolder(sasJSCoreMacros)
|
||||||
|
await createFolder(sasJSCoreMacros)
|
||||||
|
|
||||||
|
const foldersToCopy = ['base', 'ddl', 'fcmp', 'lua', 'server']
|
||||||
|
|
||||||
|
await asyncForEach(foldersToCopy, async (coreSubFolder) => {
|
||||||
|
const coreSubFolderPath = path.join(macroCorePath, coreSubFolder)
|
||||||
|
|
||||||
|
await copy(coreSubFolderPath, sasJSCoreMacros)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
copySASjsCore()
|
||||||
@@ -4,10 +4,24 @@
|
|||||||
@details This program is inserted into every sasjs/server program invocation,
|
@details This program is inserted into every sasjs/server program invocation,
|
||||||
_before_ any user-provided content.
|
_before_ any user-provided content.
|
||||||
|
|
||||||
|
A number of useful CORE macros are also compiled below, so that they can be
|
||||||
|
available "out of the box".
|
||||||
|
|
||||||
<h4> SAS Macros </h4>
|
<h4> SAS Macros </h4>
|
||||||
@li mcf_stpsrv_header.sas
|
@li mcf_stpsrv_header.sas
|
||||||
|
@li mf_getuser.sas
|
||||||
|
@li mf_getvarlist.sas
|
||||||
|
@li mf_mkdir.sas
|
||||||
|
@li mf_nobs.sas
|
||||||
|
@li mf_uid.sas
|
||||||
|
@li mfs_httpheader.sas
|
||||||
|
@li mp_dirlist.sas
|
||||||
|
@li mp_ds2ddl.sas
|
||||||
|
@li mp_ds2md.sas
|
||||||
|
@li mp_getdbml.sas
|
||||||
|
@li mp_init.sas
|
||||||
|
@li mp_makedata.sas
|
||||||
|
@li mp_zip.sas
|
||||||
|
|
||||||
**/
|
**/
|
||||||
|
|
||||||
|
|
||||||
%mcf_stpsrv_header(wrap=YES, insert_cmplib=YES)
|
|
||||||
|
|||||||
@@ -1,33 +1,53 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import express from 'express'
|
import express, { ErrorRequestHandler } from 'express'
|
||||||
import morgan from 'morgan'
|
import morgan from 'morgan'
|
||||||
|
import cookieParser from 'cookie-parser'
|
||||||
import dotenv from 'dotenv'
|
import dotenv from 'dotenv'
|
||||||
import cors from 'cors'
|
import cors from 'cors'
|
||||||
|
|
||||||
import webRouter from './routes/web'
|
import {
|
||||||
import apiRouter from './routes/api'
|
connectDB,
|
||||||
import { connectDB, getWebBuildFolderPath } from './utils'
|
getWebBuildFolderPath,
|
||||||
|
sasJSCoreMacros,
|
||||||
|
setProcessVariables
|
||||||
|
} from './utils'
|
||||||
|
|
||||||
dotenv.config()
|
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(cookieParser())
|
||||||
app.use(morgan('tiny'))
|
app.use(morgan('tiny'))
|
||||||
app.use(express.static(path.join(__dirname, '../public')))
|
|
||||||
|
|
||||||
app.use('/', webRouter)
|
|
||||||
app.use('/SASjsApi', apiRouter)
|
|
||||||
app.use(express.json({ limit: '50mb' }))
|
app.use(express.json({ limit: '50mb' }))
|
||||||
|
app.use(express.static(path.join(__dirname, '../public')))
|
||||||
app.use(express.static(getWebBuildFolderPath()))
|
app.use(express.static(getWebBuildFolderPath()))
|
||||||
|
|
||||||
export default connectDB().then(() => app)
|
const onError: ErrorRequestHandler = (err, req, res, next) => {
|
||||||
|
console.error(err.stack)
|
||||||
|
res.status(500).send('Something broke!')
|
||||||
|
}
|
||||||
|
|
||||||
|
export default setProcessVariables().then(async () => {
|
||||||
|
// loading these modules after setting up variables due to
|
||||||
|
// multer's usage of process var process.driveLoc
|
||||||
|
const { setupRoutes } = await import('./routes/setupRoutes')
|
||||||
|
setupRoutes(app)
|
||||||
|
|
||||||
|
console.log('sasJSCoreMacros', sasJSCoreMacros)
|
||||||
|
|
||||||
|
app.use(onError)
|
||||||
|
|
||||||
|
await connectDB()
|
||||||
|
return app
|
||||||
|
})
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { Request, Security, Route, Tags, Post, Body } from 'tsoa'
|
import { Request, Security, Route, Tags, Post, Body } from 'tsoa'
|
||||||
import { ExecutionController } from './internal'
|
import { ExecuteReturnJson, ExecutionController } from './internal'
|
||||||
import { PreProgramVars } from '../types'
|
import { PreProgramVars } from '../types'
|
||||||
|
import { ExecuteReturnJsonResponse } from '.'
|
||||||
|
import { parseLogToArray } from '../utils'
|
||||||
|
|
||||||
interface ExecuteSASCodePayload {
|
interface ExecuteSASCodePayload {
|
||||||
/**
|
/**
|
||||||
@@ -23,22 +25,28 @@ export class CodeController {
|
|||||||
public async executeSASCode(
|
public async executeSASCode(
|
||||||
@Request() request: express.Request,
|
@Request() request: express.Request,
|
||||||
@Body() body: ExecuteSASCodePayload
|
@Body() body: ExecuteSASCodePayload
|
||||||
): Promise<string> {
|
): Promise<ExecuteReturnJsonResponse> {
|
||||||
return executeSASCode(request, body)
|
return executeSASCode(request, body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const executeSASCode = async (req: any, { code }: ExecuteSASCodePayload) => {
|
const executeSASCode = async (req: any, { code }: ExecuteSASCodePayload) => {
|
||||||
try {
|
try {
|
||||||
const result = await new ExecutionController().executeProgram(
|
const { webout, log, httpHeaders } =
|
||||||
code,
|
(await new ExecutionController().executeProgram(
|
||||||
getPreProgramVariables(req),
|
code,
|
||||||
{ ...req.query, _debug: 131 },
|
getPreProgramVariables(req),
|
||||||
undefined,
|
{ ...req.query, _debug: 131 },
|
||||||
true
|
undefined,
|
||||||
)
|
true
|
||||||
|
)) as ExecuteReturnJson
|
||||||
|
|
||||||
return result as string
|
return {
|
||||||
|
status: 'success',
|
||||||
|
_webout: webout as string,
|
||||||
|
log: parseLogToArray(log),
|
||||||
|
httpHeaders
|
||||||
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
throw {
|
throw {
|
||||||
code: 400,
|
code: 400,
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
import path from 'path'
|
||||||
|
import express, { Express } from 'express'
|
||||||
import {
|
import {
|
||||||
Security,
|
Security,
|
||||||
|
Request,
|
||||||
Route,
|
Route,
|
||||||
Tags,
|
Tags,
|
||||||
Example,
|
Example,
|
||||||
@@ -8,31 +11,26 @@ import {
|
|||||||
Response,
|
Response,
|
||||||
Query,
|
Query,
|
||||||
Get,
|
Get,
|
||||||
Patch
|
Patch,
|
||||||
|
UploadedFile,
|
||||||
|
FormField,
|
||||||
|
Delete
|
||||||
} from 'tsoa'
|
} from 'tsoa'
|
||||||
import { fileExists, readFile, createFile } from '@sasjs/utils'
|
import {
|
||||||
|
fileExists,
|
||||||
|
moveFile,
|
||||||
|
createFolder,
|
||||||
|
deleteFile as deleteFileOnSystem
|
||||||
|
} from '@sasjs/utils'
|
||||||
import { createFileTree, ExecutionController, getTreeExample } from './internal'
|
import { createFileTree, ExecutionController, getTreeExample } from './internal'
|
||||||
|
|
||||||
import { FileTree, isFileTree, TreeNode } from '../types'
|
import { FileTree, isFileTree, TreeNode } from '../types'
|
||||||
import path from 'path'
|
|
||||||
import { getTmpFilesFolderPath } from '../utils'
|
import { getTmpFilesFolderPath } from '../utils'
|
||||||
|
|
||||||
interface DeployPayload {
|
interface DeployPayload {
|
||||||
appLoc?: string
|
appLoc?: string
|
||||||
fileTree: FileTree
|
fileTree: FileTree
|
||||||
}
|
}
|
||||||
interface FilePayload {
|
|
||||||
/**
|
|
||||||
* Path of the file
|
|
||||||
* @example "/Public/somefolder/some.file"
|
|
||||||
*/
|
|
||||||
filePath: string
|
|
||||||
/**
|
|
||||||
* Contents of the file
|
|
||||||
* @example "Contents of the File"
|
|
||||||
*/
|
|
||||||
fileContent: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DeployResponse {
|
interface DeployResponse {
|
||||||
status: string
|
status: string
|
||||||
@@ -89,57 +87,91 @@ export class DriveController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* It's optional to either provide `_filePath` in url as query parameter
|
||||||
|
* Or provide `filePath` in body as form field.
|
||||||
|
* But it's required to provide else API will respond with Bad Request.
|
||||||
|
*
|
||||||
* @summary Get file from SASjs Drive
|
* @summary Get file from SASjs Drive
|
||||||
* @query filePath Location of SAS program
|
* @query _filePath Location of SAS program
|
||||||
* @example filePath "/Public/somefolder/some.file"
|
* @example _filePath "/Public/somefolder/some.file"
|
||||||
*/
|
*/
|
||||||
@Example<GetFileResponse>({
|
|
||||||
status: 'success',
|
|
||||||
fileContent: 'Contents of the File'
|
|
||||||
})
|
|
||||||
@Response<GetFileResponse>(400, 'Unable to get File', {
|
|
||||||
status: 'failure',
|
|
||||||
message: 'File request failed.'
|
|
||||||
})
|
|
||||||
@Get('/file')
|
@Get('/file')
|
||||||
public async getFile(@Query() filePath: string): Promise<GetFileResponse> {
|
public async getFile(
|
||||||
return getFile(filePath)
|
@Request() request: express.Request,
|
||||||
|
|
||||||
|
@Query() _filePath?: string,
|
||||||
|
@FormField() filePath?: string
|
||||||
|
) {
|
||||||
|
return getFile(request, (_filePath ?? filePath)!)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* It's optional to either provide `_filePath` in url as query parameter
|
||||||
|
* Or provide `filePath` in body as form field.
|
||||||
|
* But it's required to provide else API will respond with Bad Request.
|
||||||
|
*
|
||||||
|
* @summary Delete file from SASjs Drive
|
||||||
|
* @query _filePath Location of SAS program
|
||||||
|
* @example _filePath "/Public/somefolder/some.file"
|
||||||
|
*/
|
||||||
|
@Delete('/file')
|
||||||
|
public async deleteFile(
|
||||||
|
@Query() _filePath?: string,
|
||||||
|
@FormField() filePath?: string
|
||||||
|
) {
|
||||||
|
return deleteFile((_filePath ?? filePath)!)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* It's optional to either provide `_filePath` in url as query parameter
|
||||||
|
* Or provide `filePath` in body as form field.
|
||||||
|
* But 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
|
||||||
|
* @param _filePath Location of SAS program
|
||||||
|
* @example _filePath "/Public/somefolder/some.file.sas"
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@Example<UpdateFileResponse>({
|
@Example<UpdateFileResponse>({
|
||||||
status: 'success'
|
status: 'success'
|
||||||
})
|
})
|
||||||
@Response<UpdateFileResponse>(400, 'File already exists', {
|
@Response<UpdateFileResponse>(403, 'File already exists', {
|
||||||
status: 'failure',
|
status: 'failure',
|
||||||
message: 'File request failed.'
|
message: 'File request failed.'
|
||||||
})
|
})
|
||||||
@Post('/file')
|
@Post('/file')
|
||||||
public async saveFile(
|
public async saveFile(
|
||||||
@Body() body: FilePayload
|
@UploadedFile() file: Express.Multer.File,
|
||||||
|
@Query() _filePath?: string,
|
||||||
|
@FormField() filePath?: string
|
||||||
): Promise<UpdateFileResponse> {
|
): Promise<UpdateFileResponse> {
|
||||||
return saveFile(body)
|
return saveFile((_filePath ?? filePath)!, file)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* It's optional to either provide `_filePath` in url as query parameter
|
||||||
|
* Or provide `filePath` in body as form field.
|
||||||
|
* But 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
|
||||||
|
* @param _filePath Location of SAS program
|
||||||
|
* @example _filePath "/Public/somefolder/some.file.sas"
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@Example<UpdateFileResponse>({
|
@Example<UpdateFileResponse>({
|
||||||
status: 'success'
|
status: 'success'
|
||||||
})
|
})
|
||||||
@Response<UpdateFileResponse>(400, 'Unable to get File', {
|
@Response<UpdateFileResponse>(403, `File doesn't exist`, {
|
||||||
status: 'failure',
|
status: 'failure',
|
||||||
message: 'File request failed.'
|
message: 'File request failed.'
|
||||||
})
|
})
|
||||||
@Patch('/file')
|
@Patch('/file')
|
||||||
public async updateFile(
|
public async updateFile(
|
||||||
@Body() body: FilePayload
|
@UploadedFile() file: Express.Multer.File,
|
||||||
|
@Query() _filePath?: string,
|
||||||
|
@FormField() filePath?: string
|
||||||
): Promise<UpdateFileResponse> {
|
): Promise<UpdateFileResponse> {
|
||||||
return updateFile(body)
|
return updateFile((_filePath ?? filePath)!, file)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -153,7 +185,7 @@ export class DriveController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getFileTree = () => {
|
const getFileTree = () => {
|
||||||
const tree = new ExecutionController().buildDirectorytree()
|
const tree = new ExecutionController().buildDirectoryTree()
|
||||||
return { status: 'success', tree }
|
return { status: 'success', tree }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,68 +204,95 @@ const deploy = async (data: DeployPayload) => {
|
|||||||
return successDeployResponse
|
return successDeployResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFile = async (filePath: string): Promise<GetFileResponse> => {
|
const getFile = async (req: express.Request, filePath: string) => {
|
||||||
try {
|
const driveFilesPath = getTmpFilesFolderPath()
|
||||||
const filePathFull = path
|
|
||||||
.join(getTmpFilesFolderPath(), filePath)
|
|
||||||
.replace(new RegExp('/', 'g'), path.sep)
|
|
||||||
|
|
||||||
await validateFilePath(filePathFull)
|
const filePathFull = path
|
||||||
const fileContent = await readFile(filePathFull)
|
.join(getTmpFilesFolderPath(), filePath)
|
||||||
|
.replace(new RegExp('/', 'g'), path.sep)
|
||||||
|
|
||||||
return { status: 'success', fileContent: fileContent }
|
if (!filePathFull.includes(driveFilesPath)) {
|
||||||
} catch (err: any) {
|
throw new Error('Cannot get file outside drive.')
|
||||||
throw {
|
|
||||||
code: 400,
|
|
||||||
status: 'failure',
|
|
||||||
message: 'File request failed.',
|
|
||||||
error: typeof err === 'object' ? err.toString() : err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!(await fileExists(filePathFull))) {
|
||||||
|
throw new Error('File does not exist.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const extension = path.extname(filePathFull).toLowerCase()
|
||||||
|
if (extension === '.sas') {
|
||||||
|
req.res?.setHeader('Content-type', 'text/plain')
|
||||||
|
}
|
||||||
|
|
||||||
|
req.res?.sendFile(path.resolve(filePathFull))
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveFile = async (body: FilePayload): Promise<GetFileResponse> => {
|
const deleteFile = async (filePath: string) => {
|
||||||
const { filePath, fileContent } = body
|
const driveFilesPath = getTmpFilesFolderPath()
|
||||||
try {
|
|
||||||
const filePathFull = path
|
|
||||||
.join(getTmpFilesFolderPath(), filePath)
|
|
||||||
.replace(new RegExp('/', 'g'), path.sep)
|
|
||||||
|
|
||||||
if (await fileExists(filePathFull)) {
|
const filePathFull = path
|
||||||
throw 'DriveController: File already exists.'
|
.join(getTmpFilesFolderPath(), filePath)
|
||||||
}
|
.replace(new RegExp('/', 'g'), path.sep)
|
||||||
await createFile(filePathFull, fileContent)
|
|
||||||
|
|
||||||
return { status: 'success' }
|
if (!filePathFull.includes(driveFilesPath)) {
|
||||||
} catch (err: any) {
|
throw new Error('Cannot delete file outside drive.')
|
||||||
throw {
|
|
||||||
code: 400,
|
|
||||||
status: 'failure',
|
|
||||||
message: 'File request failed.',
|
|
||||||
error: typeof err === 'object' ? err.toString() : err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!(await fileExists(filePathFull))) {
|
||||||
|
throw new Error('File does not exist.')
|
||||||
|
}
|
||||||
|
|
||||||
|
await deleteFileOnSystem(filePathFull)
|
||||||
|
|
||||||
|
return { status: 'success' }
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateFile = async (body: FilePayload): Promise<GetFileResponse> => {
|
const saveFile = async (
|
||||||
const { filePath, fileContent } = body
|
filePath: string,
|
||||||
try {
|
multerFile: Express.Multer.File
|
||||||
const filePathFull = path
|
): Promise<GetFileResponse> => {
|
||||||
.join(getTmpFilesFolderPath(), filePath)
|
const driveFilesPath = getTmpFilesFolderPath()
|
||||||
.replace(new RegExp('/', 'g'), path.sep)
|
|
||||||
|
|
||||||
await validateFilePath(filePathFull)
|
const filePathFull = path
|
||||||
await createFile(filePathFull, fileContent)
|
.join(driveFilesPath, filePath)
|
||||||
|
.replace(new RegExp('/', 'g'), path.sep)
|
||||||
|
|
||||||
return { status: 'success' }
|
if (!filePathFull.includes(driveFilesPath)) {
|
||||||
} catch (err: any) {
|
throw new Error('Cannot put file outside drive.')
|
||||||
throw {
|
|
||||||
code: 400,
|
|
||||||
status: 'failure',
|
|
||||||
message: 'File request failed.',
|
|
||||||
error: typeof err === 'object' ? err.toString() : err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (await fileExists(filePathFull)) {
|
||||||
|
throw new Error('File already exists.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const folderPath = path.dirname(filePathFull)
|
||||||
|
await createFolder(folderPath)
|
||||||
|
await moveFile(multerFile.path, filePathFull)
|
||||||
|
|
||||||
|
return { status: 'success' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateFile = async (
|
||||||
|
filePath: string,
|
||||||
|
multerFile: Express.Multer.File
|
||||||
|
): Promise<GetFileResponse> => {
|
||||||
|
const driveFilesPath = getTmpFilesFolderPath()
|
||||||
|
|
||||||
|
const filePathFull = path
|
||||||
|
.join(driveFilesPath, filePath)
|
||||||
|
.replace(new RegExp('/', 'g'), path.sep)
|
||||||
|
|
||||||
|
if (!filePathFull.includes(driveFilesPath)) {
|
||||||
|
throw new Error('Cannot modify file outside drive.')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await fileExists(filePathFull))) {
|
||||||
|
throw new Error(`File doesn't exist.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
await moveFile(multerFile.path, filePathFull)
|
||||||
|
|
||||||
|
return { status: 'success' }
|
||||||
}
|
}
|
||||||
|
|
||||||
const validateFilePath = async (filePath: string) => {
|
const validateFilePath = async (filePath: string) => {
|
||||||
|
|||||||
@@ -1,15 +1,43 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import { getSessionController } from './'
|
import { getSessionController } from './'
|
||||||
import { readFile, fileExists, createFile, moveFile } from '@sasjs/utils'
|
import {
|
||||||
|
readFile,
|
||||||
|
fileExists,
|
||||||
|
createFile,
|
||||||
|
moveFile,
|
||||||
|
readFileBinary
|
||||||
|
} from '@sasjs/utils'
|
||||||
import { PreProgramVars, TreeNode } from '../../types'
|
import { PreProgramVars, TreeNode } from '../../types'
|
||||||
import { generateFileUploadSasCode, getTmpFilesFolderPath } from '../../utils'
|
import {
|
||||||
|
extractHeaders,
|
||||||
|
generateFileUploadSasCode,
|
||||||
|
getTmpFilesFolderPath,
|
||||||
|
HTTPHeaders,
|
||||||
|
isDebugOn,
|
||||||
|
sasJSCoreMacros
|
||||||
|
} from '../../utils'
|
||||||
|
|
||||||
|
export interface ExecutionVars {
|
||||||
|
[key: string]: string | number | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExecuteReturnRaw {
|
||||||
|
httpHeaders: HTTPHeaders
|
||||||
|
result: string | Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExecuteReturnJson {
|
||||||
|
httpHeaders: HTTPHeaders
|
||||||
|
webout: string | Buffer
|
||||||
|
log?: string
|
||||||
|
}
|
||||||
|
|
||||||
export class ExecutionController {
|
export class ExecutionController {
|
||||||
async executeFile(
|
async executeFile(
|
||||||
programPath: string,
|
programPath: string,
|
||||||
preProgramVariables: PreProgramVars,
|
preProgramVariables: PreProgramVars,
|
||||||
vars: { [key: string]: string | number | undefined },
|
vars: ExecutionVars,
|
||||||
otherArgs?: any,
|
otherArgs?: any,
|
||||||
returnJson?: boolean
|
returnJson?: boolean
|
||||||
) {
|
) {
|
||||||
@@ -26,24 +54,26 @@ export class ExecutionController {
|
|||||||
returnJson
|
returnJson
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async executeProgram(
|
async executeProgram(
|
||||||
program: string,
|
program: string,
|
||||||
preProgramVariables: PreProgramVars,
|
preProgramVariables: PreProgramVars,
|
||||||
vars: { [key: string]: string | number | undefined },
|
vars: ExecutionVars,
|
||||||
otherArgs?: any,
|
otherArgs?: any,
|
||||||
returnJson?: boolean
|
returnJson?: boolean
|
||||||
) {
|
): Promise<ExecuteReturnRaw | ExecuteReturnJson> {
|
||||||
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')
|
||||||
|
const headersPath = path.join(session.path, 'stpsrv_header.txt')
|
||||||
const weboutPath = path.join(session.path, 'webout.txt')
|
const weboutPath = path.join(session.path, 'webout.txt')
|
||||||
await createFile(weboutPath, '')
|
|
||||||
|
|
||||||
const tokenFile = path.join(session.path, 'accessToken.txt')
|
const tokenFile = path.join(session.path, 'accessToken.txt')
|
||||||
|
|
||||||
|
await createFile(weboutPath, '')
|
||||||
await createFile(
|
await createFile(
|
||||||
tokenFile,
|
tokenFile,
|
||||||
preProgramVariables?.accessToken ?? 'accessToken'
|
preProgramVariables?.accessToken ?? 'accessToken'
|
||||||
@@ -54,6 +84,7 @@ export class ExecutionController {
|
|||||||
`${computed}%let ${key}=${vars[key]};\n`,
|
`${computed}%let ${key}=${vars[key]};\n`,
|
||||||
''
|
''
|
||||||
)
|
)
|
||||||
|
|
||||||
const preProgramVarStatments = `
|
const preProgramVarStatments = `
|
||||||
%let _sasjs_tokenfile=${tokenFile};
|
%let _sasjs_tokenfile=${tokenFile};
|
||||||
%let _sasjs_username=${preProgramVariables?.username};
|
%let _sasjs_username=${preProgramVariables?.username};
|
||||||
@@ -63,9 +94,20 @@ 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;
|
||||||
|
%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 = `
|
program = `
|
||||||
|
options insert=(SASAUTOS="${sasJSCoreMacros}");
|
||||||
|
|
||||||
/* runtime vars */
|
/* runtime vars */
|
||||||
${varStatments}
|
${varStatments}
|
||||||
filename _webout "${weboutPath}" mod;
|
filename _webout "${weboutPath}" mod;
|
||||||
@@ -100,38 +142,47 @@ ${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) : '') +
|
const headersContent = (await fileExists(headersPath))
|
||||||
session.crashed
|
? await readFile(headersPath)
|
||||||
|
: ''
|
||||||
|
const httpHeaders: HTTPHeaders = extractHeaders(headersContent)
|
||||||
|
const fileResponse: boolean =
|
||||||
|
httpHeaders.hasOwnProperty('content-type') && !returnJson
|
||||||
|
|
||||||
const webout = (await fileExists(weboutPath))
|
const webout = (await fileExists(weboutPath))
|
||||||
? await readFile(weboutPath)
|
? fileResponse
|
||||||
|
? await readFileBinary(weboutPath)
|
||||||
|
: await readFile(weboutPath)
|
||||||
: ''
|
: ''
|
||||||
|
|
||||||
const debugValue =
|
// it should be deleted by scheduleSessionDestroy
|
||||||
typeof vars._debug === 'string' ? parseInt(vars._debug) : vars._debug
|
|
||||||
|
|
||||||
session.inUse = false
|
session.inUse = false
|
||||||
sessionController.deleteSession(session)
|
|
||||||
|
|
||||||
if (returnJson) {
|
if (returnJson) {
|
||||||
return {
|
return {
|
||||||
|
httpHeaders,
|
||||||
webout,
|
webout,
|
||||||
log:
|
log: isDebugOn(vars) || session.crashed ? log : undefined
|
||||||
(debugValue && debugValue >= 131) || session.crashed ? log : undefined
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (debugValue && debugValue >= 131) || session.crashed
|
return {
|
||||||
? `<html><body>${webout}<div style="text-align:left"><hr /><h2>SAS Log</h2><pre>${log}</pre></div></body></html>`
|
httpHeaders,
|
||||||
: webout
|
result: fileResponse
|
||||||
|
? webout
|
||||||
|
: isDebugOn(vars) || session.crashed
|
||||||
|
? `<html><body>${webout}<div style="text-align:left"><hr /><h2>SAS Log</h2><pre>${log}</pre></div></body></html>`
|
||||||
|
: webout
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildDirectorytree() {
|
buildDirectoryTree() {
|
||||||
const root: TreeNode = {
|
const root: TreeNode = {
|
||||||
name: 'files',
|
name: 'files',
|
||||||
relativePath: '',
|
relativePath: '',
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ 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 = async (req: any, res: any, next: any) => {
|
public preUploadMiddleware = async (req: any, res: any, next: any) => {
|
||||||
let session
|
let session
|
||||||
|
|
||||||
const sessionController = getSessionController()
|
const sessionController = getSessionController()
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -62,7 +67,10 @@ export class SessionController {
|
|||||||
|
|
||||||
// the autoexec file is executed on SAS startup
|
// the autoexec file is executed on SAS startup
|
||||||
const autoExecPath = path.join(sessionFolder, 'autoexec.sas')
|
const autoExecPath = path.join(sessionFolder, 'autoexec.sas')
|
||||||
const contentForAutoExec = `/* compiled systemInit */\n${compiledSystemInitContent}\n/* autoexec */\n${autoExecContent}`
|
const contentForAutoExec = `/* compiled systemInit */
|
||||||
|
${compiledSystemInitContent}
|
||||||
|
/* autoexec */
|
||||||
|
${autoExecContent}`
|
||||||
await createFile(autoExecPath, contentForAutoExec)
|
await createFile(autoExecPath, contentForAutoExec)
|
||||||
|
|
||||||
// create empty code.sas as SAS will not start without a SYSIN
|
// create empty code.sas as SAS will not start without a SYSIN
|
||||||
@@ -105,15 +113,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 +130,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 +160,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);
|
||||||
|
|||||||
@@ -1,9 +1,31 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { Request, Security, Route, Tags, Post, Body, Get, Query } from 'tsoa'
|
import {
|
||||||
import { ExecutionController } from './internal'
|
Request,
|
||||||
|
Security,
|
||||||
|
Route,
|
||||||
|
Tags,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
Get,
|
||||||
|
Query,
|
||||||
|
Example
|
||||||
|
} from 'tsoa'
|
||||||
|
import {
|
||||||
|
ExecuteReturnJson,
|
||||||
|
ExecuteReturnRaw,
|
||||||
|
ExecutionController,
|
||||||
|
ExecutionVars
|
||||||
|
} from './internal'
|
||||||
import { PreProgramVars } from '../types'
|
import { PreProgramVars } from '../types'
|
||||||
import { getTmpFilesFolderPath, makeFilesNamesMap } from '../utils'
|
import {
|
||||||
|
getTmpFilesFolderPath,
|
||||||
|
HTTPHeaders,
|
||||||
|
isDebugOn,
|
||||||
|
LogLine,
|
||||||
|
makeFilesNamesMap,
|
||||||
|
parseLogToArray
|
||||||
|
} from '../utils'
|
||||||
|
|
||||||
interface ExecuteReturnJsonPayload {
|
interface ExecuteReturnJsonPayload {
|
||||||
/**
|
/**
|
||||||
@@ -12,11 +34,16 @@ interface ExecuteReturnJsonPayload {
|
|||||||
*/
|
*/
|
||||||
_program?: string
|
_program?: string
|
||||||
}
|
}
|
||||||
interface ExecuteReturnJsonResponse {
|
|
||||||
|
interface IRecordOfAny {
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
export interface ExecuteReturnJsonResponse {
|
||||||
status: string
|
status: string
|
||||||
_webout: string
|
_webout: string | IRecordOfAny
|
||||||
log?: string
|
log: LogLine[]
|
||||||
message?: string
|
message?: string
|
||||||
|
httpHeaders: HTTPHeaders
|
||||||
}
|
}
|
||||||
|
|
||||||
@Security('bearerAuth')
|
@Security('bearerAuth')
|
||||||
@@ -24,33 +51,69 @@ interface ExecuteReturnJsonResponse {
|
|||||||
@Tags('STP')
|
@Tags('STP')
|
||||||
export class STPController {
|
export class STPController {
|
||||||
/**
|
/**
|
||||||
* Trigger a SAS program using it's location in the _program parameter.
|
* Trigger a SAS program using it's location in the _program URL parameter.
|
||||||
* Enable debugging using the _debug 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.
|
* Additional URL parameters are turned into SAS macro variables.
|
||||||
* Any files provided are placed into the session and
|
*
|
||||||
* corresponding _WEBIN_XXX variables are created.
|
* Any files provided in the request body are placed into the SAS session with
|
||||||
* @summary Execute Stored Program, return raw content
|
* corresponding _WEBIN_XXX variables created.
|
||||||
* @query _program Location of SAS program
|
*
|
||||||
|
* The response headers can be adjusted using the mfs_httpheader() macro. Any
|
||||||
|
* file type can be returned, including binary files such as zip or xls.
|
||||||
|
*
|
||||||
|
* 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"
|
* @example _program "/Public/somefolder/some.file"
|
||||||
*/
|
*/
|
||||||
@Get('/execute')
|
@Get('/execute')
|
||||||
public async executeReturnRaw(
|
public async executeReturnRaw(
|
||||||
@Request() request: express.Request,
|
@Request() request: express.Request,
|
||||||
@Query() _program: string
|
@Query() _program: string
|
||||||
): Promise<string> {
|
): Promise<string | Buffer> {
|
||||||
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 URL parameter.
|
||||||
* Enable debugging using the _debug 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.
|
* Additional URL parameters are turned into SAS macro variables.
|
||||||
* Any files provided are placed into the session and
|
*
|
||||||
* corresponding _WEBIN_XXX variables are created.
|
* Any files provided in the request body are placed into the SAS session with
|
||||||
|
* corresponding _WEBIN_XXX variables created.
|
||||||
|
*
|
||||||
|
* The response will be a JSON object with the following root attributes: log,
|
||||||
|
* webout, headers.
|
||||||
|
*
|
||||||
|
* The webout will be a nested JSON object ONLY if the response-header
|
||||||
|
* contains a content-type of application/json AND it is valid JSON.
|
||||||
|
* Otherwise it will be a stringified version of the webout content.
|
||||||
|
*
|
||||||
|
* Response headers from the mfs_httpheader macro are simply listed in the
|
||||||
|
* headers object, for POST requests they have no effect on the actual
|
||||||
|
* response header.
|
||||||
|
*
|
||||||
* @summary Execute Stored Program, return JSON
|
* @summary Execute Stored Program, return JSON
|
||||||
* @query _program Location of SAS program
|
* @param _program Location of SAS program
|
||||||
* @example _program "/Public/somefolder/some.file"
|
* @example _program "/Public/somefolder/some.file"
|
||||||
*/
|
*/
|
||||||
|
@Example<ExecuteReturnJsonResponse>({
|
||||||
|
status: 'success',
|
||||||
|
_webout: 'webout content',
|
||||||
|
log: [],
|
||||||
|
httpHeaders: {
|
||||||
|
'Content-type': 'application/zip',
|
||||||
|
'Cache-Control': 'public, max-age=1000'
|
||||||
|
}
|
||||||
|
})
|
||||||
@Post('/execute')
|
@Post('/execute')
|
||||||
public async executeReturnJson(
|
public async executeReturnJson(
|
||||||
@Request() request: express.Request,
|
@Request() request: express.Request,
|
||||||
@@ -65,21 +128,35 @@ export class STPController {
|
|||||||
const executeReturnRaw = async (
|
const executeReturnRaw = async (
|
||||||
req: express.Request,
|
req: express.Request,
|
||||||
_program: string
|
_program: string
|
||||||
): Promise<string> => {
|
): Promise<string | Buffer> => {
|
||||||
const query = req.query as { [key: string]: string | number | undefined }
|
const query = req.query as ExecutionVars
|
||||||
const sasCodePath =
|
const sasCodePath =
|
||||||
path
|
path
|
||||||
.join(getTmpFilesFolderPath(), _program)
|
.join(getTmpFilesFolderPath(), _program)
|
||||||
.replace(new RegExp('/', 'g'), path.sep) + '.sas'
|
.replace(new RegExp('/', 'g'), path.sep) + '.sas'
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await new ExecutionController().executeFile(
|
const { result, httpHeaders } =
|
||||||
sasCodePath,
|
(await new ExecutionController().executeFile(
|
||||||
getPreProgramVariables(req),
|
sasCodePath,
|
||||||
query
|
getPreProgramVariables(req),
|
||||||
)
|
query
|
||||||
|
)) as ExecuteReturnRaw
|
||||||
|
|
||||||
return result as string
|
// Should over-ride response header for
|
||||||
|
// debug on GET request to see entire log
|
||||||
|
// rendering on browser.
|
||||||
|
if (isDebugOn(query)) {
|
||||||
|
httpHeaders['content-type'] = 'text/plain'
|
||||||
|
}
|
||||||
|
|
||||||
|
req.res?.set(httpHeaders)
|
||||||
|
|
||||||
|
if (result instanceof Buffer) {
|
||||||
|
;(req as any).sasHeaders = httpHeaders
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
throw {
|
throw {
|
||||||
code: 400,
|
code: 400,
|
||||||
@@ -102,17 +179,27 @@ 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().executeFile(
|
const { webout, log, httpHeaders } =
|
||||||
sasCodePath,
|
(await new ExecutionController().executeFile(
|
||||||
getPreProgramVariables(req),
|
sasCodePath,
|
||||||
{ ...req.query, ...req.body },
|
getPreProgramVariables(req),
|
||||||
{ filesNamesMap: filesNamesMap },
|
{ ...req.query, ...req.body },
|
||||||
true
|
{ filesNamesMap: filesNamesMap },
|
||||||
)) as { webout: string; log: string }
|
true
|
||||||
|
)) as ExecuteReturnJson
|
||||||
|
|
||||||
|
let weboutRes: string | IRecordOfAny = webout
|
||||||
|
if (httpHeaders['content-type']?.toLowerCase() === 'application/json') {
|
||||||
|
try {
|
||||||
|
weboutRes = JSON.parse(webout as string)
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: 'success',
|
status: 'success',
|
||||||
_webout: webout,
|
_webout: weboutRes,
|
||||||
log
|
log: parseLogToArray(log),
|
||||||
|
httpHeaders
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
throw {
|
throw {
|
||||||
|
|||||||
@@ -43,7 +43,9 @@ const authenticateToken = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const authHeader = req.headers['authorization']
|
const authHeader = req.headers['authorization']
|
||||||
const token = authHeader?.split(' ')[1]
|
const token =
|
||||||
|
authHeader?.split(' ')[1] ??
|
||||||
|
(tokenType === 'accessToken' ? req.cookies.accessToken : '')
|
||||||
if (!token) return res.sendStatus(401)
|
if (!token) return res.sendStatus(401)
|
||||||
|
|
||||||
jwt.verify(token, key, async (err: any, data: any) => {
|
jwt.verify(token, key, async (err: any, data: any) => {
|
||||||
|
|||||||
77
api/src/middlewares/multer.ts
Normal file
77
api/src/middlewares/multer.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import path from 'path'
|
||||||
|
import { Request } from 'express'
|
||||||
|
import multer, { FileFilterCallback, Options } from 'multer'
|
||||||
|
import { getTmpUploadsPath } from '../utils'
|
||||||
|
|
||||||
|
const acceptableExtensions = ['.sas']
|
||||||
|
const fieldNameSize = 300
|
||||||
|
const fileSize = 10485760 // 10 MB
|
||||||
|
|
||||||
|
const storage = multer.diskStorage({
|
||||||
|
destination: getTmpUploadsPath(),
|
||||||
|
filename: function (
|
||||||
|
_req: Request,
|
||||||
|
file: Express.Multer.File,
|
||||||
|
callback: (error: Error | null, filename: string) => void
|
||||||
|
) {
|
||||||
|
callback(
|
||||||
|
null,
|
||||||
|
file.fieldname + path.extname(file.originalname) + '-' + Date.now()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const limits: Options['limits'] = {
|
||||||
|
fieldNameSize,
|
||||||
|
fileSize
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileFilter: Options['fileFilter'] = (
|
||||||
|
req: Request,
|
||||||
|
file: Express.Multer.File,
|
||||||
|
callback: FileFilterCallback
|
||||||
|
) => {
|
||||||
|
const fileExtension = path.extname(file.originalname).toLocaleLowerCase()
|
||||||
|
|
||||||
|
if (!acceptableExtensions.includes(fileExtension)) {
|
||||||
|
return callback(
|
||||||
|
new Error(
|
||||||
|
`File extension '${fileExtension}' not acceptable. Valid extension(s): ${acceptableExtensions.join(
|
||||||
|
', '
|
||||||
|
)}`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadFileSize = parseInt(req.headers['content-length'] ?? '')
|
||||||
|
if (uploadFileSize > fileSize) {
|
||||||
|
return callback(
|
||||||
|
new Error(
|
||||||
|
`File size is over limit. File limit is: ${fileSize / 1024 / 1024} MB`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(null, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: Options = { storage, limits, fileFilter }
|
||||||
|
|
||||||
|
const multerInstance = multer(options)
|
||||||
|
|
||||||
|
export const multerSingle = (fileName: string, arg: any) => {
|
||||||
|
const [req, res, next] = arg
|
||||||
|
const upload = multerInstance.single(fileName)
|
||||||
|
|
||||||
|
upload(req, res, function (err) {
|
||||||
|
if (err instanceof multer.MulterError) {
|
||||||
|
return res.status(500).send(err.message)
|
||||||
|
} else if (err) {
|
||||||
|
return res.status(400).send(err.message)
|
||||||
|
}
|
||||||
|
// Everything went fine.
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default multerInstance
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import path from 'path'
|
|
||||||
import { readFileSync } from 'fs'
|
|
||||||
import * as https from 'https'
|
|
||||||
import appPromise from './app'
|
|
||||||
|
|
||||||
const keyPath = path.join('..', 'certificates', 'privkey.pem')
|
|
||||||
const certPath = path.join('..', 'certificates', 'fullchain.pem')
|
|
||||||
|
|
||||||
const key = readFileSync(keyPath)
|
|
||||||
const cert = readFileSync(certPath)
|
|
||||||
|
|
||||||
appPromise.then((app) => {
|
|
||||||
const httpsServer = https.createServer({ key, cert }, app)
|
|
||||||
|
|
||||||
const sasJsPort = process.env.PORT ?? 5000
|
|
||||||
httpsServer.listen(sasJsPort, () => {
|
|
||||||
console.log(
|
|
||||||
`⚡️[server]: Server is running at https://localhost:${sasJsPort}`
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -55,8 +55,9 @@ authRouter.post('/token', async (req, res) => {
|
|||||||
const controller = new AuthController()
|
const controller = new AuthController()
|
||||||
try {
|
try {
|
||||||
const response = await controller.token(body)
|
const response = await controller.token(body)
|
||||||
|
const { accessToken } = response
|
||||||
|
|
||||||
res.send(response)
|
res.cookie('accessToken', accessToken).send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(403).send(err.toString())
|
res.status(403).send(err.toString())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ runRouter.post('/execute', async (req, res) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await controller.executeSASCode(req, body)
|
const response = await controller.executeSASCode(req, body)
|
||||||
|
|
||||||
|
if (response instanceof Buffer) {
|
||||||
|
res.writeHead(200, (req as any).sasHeaders)
|
||||||
|
return res.end(response)
|
||||||
|
}
|
||||||
|
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const statusCode = err.code
|
const statusCode = err.code
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
|
import { deleteFile } from '@sasjs/utils'
|
||||||
|
|
||||||
|
import { multerSingle } from '../../middlewares/multer'
|
||||||
import { DriveController } from '../../controllers/'
|
import { DriveController } from '../../controllers/'
|
||||||
import { getFileDriveValidation, updateFileDriveValidation } from '../../utils'
|
import { fileBodyValidation, fileParamValidation } from '../../utils'
|
||||||
|
|
||||||
|
const controller = new DriveController()
|
||||||
|
|
||||||
const driveRouter = express.Router()
|
const driveRouter = express.Router()
|
||||||
|
|
||||||
driveRouter.post('/deploy', async (req, res) => {
|
driveRouter.post('/deploy', async (req, res) => {
|
||||||
const controller = new DriveController()
|
|
||||||
try {
|
try {
|
||||||
const response = await controller.deploy(req.body)
|
const response = await controller.deploy(req.body)
|
||||||
res.send(response)
|
res.send(response)
|
||||||
@@ -19,58 +23,89 @@ driveRouter.post('/deploy', async (req, res) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
driveRouter.get('/file', async (req, res) => {
|
driveRouter.get('/file', async (req, res) => {
|
||||||
const { error, value: query } = getFileDriveValidation(req.query)
|
const { error: errQ, value: query } = fileParamValidation(req.query)
|
||||||
if (error) return res.status(400).send(error.details[0].message)
|
const { error: errB, value: body } = fileBodyValidation(req.body)
|
||||||
|
|
||||||
|
if (errQ && errB) return res.status(400).send(errB.details[0].message)
|
||||||
|
|
||||||
const controller = new DriveController()
|
|
||||||
try {
|
try {
|
||||||
const response = await controller.getFile(query.filePath)
|
await controller.getFile(req, query._filePath, body.filePath)
|
||||||
res.send(response)
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const statusCode = err.code
|
res.status(403).send(err.toString())
|
||||||
|
|
||||||
delete err.code
|
|
||||||
|
|
||||||
res.status(statusCode).send(err)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
driveRouter.post('/file', async (req, res) => {
|
driveRouter.delete('/file', async (req, res) => {
|
||||||
const { error, value: body } = updateFileDriveValidation(req.body)
|
const { error: errQ, value: query } = fileParamValidation(req.query)
|
||||||
if (error) return res.status(400).send(error.details[0].message)
|
const { error: errB, value: body } = fileBodyValidation(req.body)
|
||||||
|
|
||||||
|
if (errQ && errB) return res.status(400).send(errB.details[0].message)
|
||||||
|
|
||||||
const controller = new DriveController()
|
|
||||||
try {
|
try {
|
||||||
const response = await controller.saveFile(body)
|
const response = await controller.deleteFile(query._filePath, body.filePath)
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const statusCode = err.code
|
res.status(403).send(err.toString())
|
||||||
|
|
||||||
delete err.code
|
|
||||||
|
|
||||||
res.status(statusCode).send(err)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
driveRouter.patch('/file', async (req, res) => {
|
driveRouter.post(
|
||||||
const { error, value: body } = updateFileDriveValidation(req.body)
|
'/file',
|
||||||
if (error) return res.status(400).send(error.details[0].message)
|
(...arg) => multerSingle('file', arg),
|
||||||
|
async (req, res) => {
|
||||||
|
const { error: errQ, value: query } = fileParamValidation(req.query)
|
||||||
|
const { error: errB, value: body } = fileBodyValidation(req.body)
|
||||||
|
|
||||||
const controller = new DriveController()
|
if (errQ && errB) {
|
||||||
try {
|
if (req.file) await deleteFile(req.file.path)
|
||||||
const response = await controller.updateFile(body)
|
return res.status(400).send(errB.details[0].message)
|
||||||
res.send(response)
|
}
|
||||||
} catch (err: any) {
|
|
||||||
const statusCode = err.code
|
|
||||||
|
|
||||||
delete err.code
|
if (!req.file) return res.status(400).send('"file" is not present.')
|
||||||
|
|
||||||
res.status(statusCode).send(err)
|
try {
|
||||||
|
const response = await controller.saveFile(
|
||||||
|
req.file,
|
||||||
|
query._filePath,
|
||||||
|
body.filePath
|
||||||
|
)
|
||||||
|
res.send(response)
|
||||||
|
} catch (err: any) {
|
||||||
|
await deleteFile(req.file.path)
|
||||||
|
res.status(403).send(err.toString())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
|
driveRouter.patch(
|
||||||
|
'/file',
|
||||||
|
(...arg) => multerSingle('file', arg),
|
||||||
|
async (req, res) => {
|
||||||
|
const { error: errQ, value: query } = fileParamValidation(req.query)
|
||||||
|
const { error: errB, value: body } = fileBodyValidation(req.body)
|
||||||
|
|
||||||
|
if (errQ && errB) {
|
||||||
|
if (req.file) await deleteFile(req.file.path)
|
||||||
|
return res.status(400).send(errB.details[0].message)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.file) return res.status(400).send('"file" is not present.')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await controller.updateFile(
|
||||||
|
req.file,
|
||||||
|
query._filePath,
|
||||||
|
body.filePath
|
||||||
|
)
|
||||||
|
res.send(response)
|
||||||
|
} catch (err: any) {
|
||||||
|
await deleteFile(req.file.path)
|
||||||
|
res.status(403).send(err.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
driveRouter.get('/fileTree', async (req, res) => {
|
driveRouter.get('/fileTree', async (req, res) => {
|
||||||
const controller = new DriveController()
|
|
||||||
try {
|
try {
|
||||||
const response = await controller.getFileTree()
|
const response = await controller.getFileTree()
|
||||||
res.send(response)
|
res.send(response)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { SessionController } from '../../controllers'
|
import { SessionController } from '../../controllers'
|
||||||
import { authenticateAccessToken } from '../../middlewares'
|
|
||||||
|
|
||||||
const sessionRouter = express.Router()
|
const sessionRouter = express.Router()
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,34 @@
|
|||||||
|
import path from 'path'
|
||||||
import { Express } from 'express'
|
import { Express } from 'express'
|
||||||
import mongoose, { Mongoose } from 'mongoose'
|
import mongoose, { Mongoose } from 'mongoose'
|
||||||
import { MongoMemoryServer } from 'mongodb-memory-server'
|
import { MongoMemoryServer } from 'mongodb-memory-server'
|
||||||
import request from 'supertest'
|
import request from 'supertest'
|
||||||
|
|
||||||
|
import {
|
||||||
|
folderExists,
|
||||||
|
fileExists,
|
||||||
|
readFile,
|
||||||
|
deleteFolder,
|
||||||
|
generateTimestamp,
|
||||||
|
copy
|
||||||
|
} from '@sasjs/utils'
|
||||||
|
import * as fileUtilModules from '../../../utils/file'
|
||||||
|
|
||||||
|
const timestamp = generateTimestamp()
|
||||||
|
const tmpFolder = path.join(process.cwd(), `tmp-${timestamp}`)
|
||||||
|
jest
|
||||||
|
.spyOn(fileUtilModules, 'getTmpFolderPath')
|
||||||
|
.mockImplementation(() => tmpFolder)
|
||||||
|
jest
|
||||||
|
.spyOn(fileUtilModules, 'getTmpUploadsPath')
|
||||||
|
.mockImplementation(() => path.join(tmpFolder, 'uploads'))
|
||||||
|
|
||||||
import appPromise from '../../../app'
|
import appPromise from '../../../app'
|
||||||
import { UserController } from '../../../controllers/'
|
import { UserController } from '../../../controllers/'
|
||||||
import { getTreeExample } from '../../../controllers/internal'
|
import { getTreeExample } from '../../../controllers/internal'
|
||||||
import { getTmpFilesFolderPath } from '../../../utils/file'
|
|
||||||
import { folderExists, fileExists, readFile, deleteFolder } from '@sasjs/utils'
|
|
||||||
import path from 'path'
|
|
||||||
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
|
||||||
import { FolderMember, ServiceMember } from '../../../types'
|
import { FolderMember, ServiceMember } from '../../../types'
|
||||||
|
import { generateAccessToken, saveTokensInDB } from '../../../utils/'
|
||||||
|
const { getTmpFilesFolderPath } = fileUtilModules
|
||||||
|
|
||||||
let app: Express
|
let app: Express
|
||||||
appPromise.then((_app) => {
|
appPromise.then((_app) => {
|
||||||
@@ -30,28 +49,27 @@ describe('files', () => {
|
|||||||
let mongoServer: MongoMemoryServer
|
let mongoServer: MongoMemoryServer
|
||||||
const controller = new UserController()
|
const controller = new UserController()
|
||||||
|
|
||||||
|
let accessToken: string
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
mongoServer = await MongoMemoryServer.create()
|
mongoServer = await MongoMemoryServer.create()
|
||||||
con = await mongoose.connect(mongoServer.getUri())
|
con = await mongoose.connect(mongoServer.getUri())
|
||||||
|
|
||||||
|
const dbUser = await controller.createUser(user)
|
||||||
|
accessToken = generateAccessToken({
|
||||||
|
clientId,
|
||||||
|
userId: dbUser.id
|
||||||
|
})
|
||||||
|
await saveTokensInDB(dbUser.id, clientId, accessToken, 'refreshToken')
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await con.connection.dropDatabase()
|
await con.connection.dropDatabase()
|
||||||
await con.connection.close()
|
await con.connection.close()
|
||||||
await mongoServer.stop()
|
await mongoServer.stop()
|
||||||
|
await deleteFolder(tmpFolder)
|
||||||
})
|
})
|
||||||
describe('deploy', () => {
|
describe('deploy', () => {
|
||||||
let accessToken: string
|
|
||||||
let dbUser: any
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
dbUser = await controller.createUser(user)
|
|
||||||
accessToken = generateAccessToken({
|
|
||||||
clientId,
|
|
||||||
userId: dbUser.id
|
|
||||||
})
|
|
||||||
await saveTokensInDB(dbUser.id, clientId, accessToken, 'refreshToken')
|
|
||||||
})
|
|
||||||
const shouldFailAssertion = async (payload: any) => {
|
const shouldFailAssertion = async (payload: any) => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/SASjsApi/drive/deploy')
|
.post('/SASjsApi/drive/deploy')
|
||||||
@@ -144,8 +162,6 @@ describe('files', () => {
|
|||||||
const exampleService = getExampleService()
|
const exampleService = getExampleService()
|
||||||
const testJobFile = path.join(testJobFolder, exampleService.name) + '.sas'
|
const testJobFile = path.join(testJobFolder, exampleService.name) + '.sas'
|
||||||
|
|
||||||
console.log(`[testJobFile]`, testJobFile)
|
|
||||||
|
|
||||||
await expect(fileExists(testJobFile)).resolves.toEqual(true)
|
await expect(fileExists(testJobFile)).resolves.toEqual(true)
|
||||||
|
|
||||||
await expect(readFile(testJobFile)).resolves.toEqual(exampleService.code)
|
await expect(readFile(testJobFile)).resolves.toEqual(exampleService.code)
|
||||||
@@ -153,6 +169,319 @@ describe('files', () => {
|
|||||||
await deleteFolder(getTmpFilesFolderPath())
|
await deleteFolder(getTmpFilesFolderPath())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('file', () => {
|
||||||
|
describe('create', () => {
|
||||||
|
it('should create a SAS file on drive having filePath as form field', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASjsApi/drive/file')
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.field('filePath', '/my/path/code.sas')
|
||||||
|
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
|
||||||
|
|
||||||
|
expect(res.statusCode).toEqual(200)
|
||||||
|
expect(res.body).toEqual({
|
||||||
|
status: 'success'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create a SAS file on drive having _filePath as query param', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASjsApi/drive/file')
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.query({ _filePath: '/my/path/code1.sas' })
|
||||||
|
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
|
||||||
|
|
||||||
|
expect(res.statusCode).toEqual(200)
|
||||||
|
expect(res.body).toEqual({
|
||||||
|
status: 'success'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Unauthorized if access token is not present', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASjsApi/drive/file')
|
||||||
|
.field('filePath', '/my/path/code.sas')
|
||||||
|
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
|
||||||
|
.expect(401)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('Unauthorized')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Forbidden if file is already present', async () => {
|
||||||
|
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||||
|
const pathToUpload = '/my/path/code.sas'
|
||||||
|
|
||||||
|
const pathToCopy = path.join(
|
||||||
|
fileUtilModules.getTmpFilesFolderPath(),
|
||||||
|
pathToUpload
|
||||||
|
)
|
||||||
|
await copy(fileToAttachPath, pathToCopy)
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASjsApi/drive/file')
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.field('filePath', pathToUpload)
|
||||||
|
.attach('file', fileToAttachPath)
|
||||||
|
.expect(403)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('Error: File already exists.')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Forbidden if filePath outside Drive', async () => {
|
||||||
|
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||||
|
const pathToUpload = '/../path/code.sas'
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASjsApi/drive/file')
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.field('filePath', pathToUpload)
|
||||||
|
.attach('file', fileToAttachPath)
|
||||||
|
.expect(403)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('Error: Cannot put file outside drive.')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Bad Request if filePath is missing', async () => {
|
||||||
|
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASjsApi/drive/file')
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.attach('file', fileToAttachPath)
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual(`"filePath" is required`)
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should respond with Bad Request if filePath doesn't has correct extension", async () => {
|
||||||
|
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||||
|
const pathToUpload = '/my/path/code.oth'
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASjsApi/drive/file')
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.field('filePath', pathToUpload)
|
||||||
|
.attach('file', fileToAttachPath)
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('Valid extensions for filePath: .sas')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Bad Request if file is missing', async () => {
|
||||||
|
const pathToUpload = '/my/path/code.sas'
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASjsApi/drive/file')
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.field('filePath', pathToUpload)
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('"file" is not present.')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should respond with Bad Request if attached file doesn't has correct extension", async () => {
|
||||||
|
const fileToAttachPath = path.join(__dirname, 'files', 'sample.oth')
|
||||||
|
const pathToUpload = '/my/path/code.sas'
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASjsApi/drive/file')
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.field('filePath', pathToUpload)
|
||||||
|
.attach('file', fileToAttachPath)
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual(
|
||||||
|
`File extension '.oth' not acceptable. Valid extension(s): .sas`
|
||||||
|
)
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Bad Request if attached file exceeds file limit', async () => {
|
||||||
|
const pathToUpload = '/my/path/code.sas'
|
||||||
|
|
||||||
|
const attachedFile = Buffer.from('.'.repeat(20 * 1024 * 1024))
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASjsApi/drive/file')
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.field('filePath', pathToUpload)
|
||||||
|
.attach('file', attachedFile, 'another.sas')
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual(
|
||||||
|
'File size is over limit. File limit is: 10 MB'
|
||||||
|
)
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('update', () => {
|
||||||
|
it('should update a SAS file on drive having filePath as form field', async () => {
|
||||||
|
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||||
|
const pathToUpload = '/my/path/code.sas'
|
||||||
|
|
||||||
|
const pathToCopy = path.join(
|
||||||
|
fileUtilModules.getTmpFilesFolderPath(),
|
||||||
|
pathToUpload
|
||||||
|
)
|
||||||
|
await copy(fileToAttachPath, pathToCopy)
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.patch('/SASjsApi/drive/file')
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.field('filePath', pathToUpload)
|
||||||
|
.attach('file', fileToAttachPath)
|
||||||
|
|
||||||
|
expect(res.statusCode).toEqual(200)
|
||||||
|
expect(res.body).toEqual({
|
||||||
|
status: 'success'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should update a SAS file on drive having _filePath as query param', async () => {
|
||||||
|
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||||
|
const pathToUpload = '/my/path/code.sas'
|
||||||
|
|
||||||
|
const pathToCopy = path.join(
|
||||||
|
fileUtilModules.getTmpFilesFolderPath(),
|
||||||
|
pathToUpload
|
||||||
|
)
|
||||||
|
await copy(fileToAttachPath, pathToCopy)
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.patch('/SASjsApi/drive/file')
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.field('filePath', pathToUpload)
|
||||||
|
.attach('file', fileToAttachPath)
|
||||||
|
|
||||||
|
expect(res.statusCode).toEqual(200)
|
||||||
|
expect(res.body).toEqual({
|
||||||
|
status: 'success'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Unauthorized if access token is not present', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.patch('/SASjsApi/drive/file')
|
||||||
|
.field('filePath', '/my/path/code.sas')
|
||||||
|
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
|
||||||
|
.expect(401)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('Unauthorized')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Forbidden if file is not present', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.patch('/SASjsApi/drive/file')
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.field('filePath', `/my/path/code-${generateTimestamp()}.sas`)
|
||||||
|
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
|
||||||
|
.expect(403)
|
||||||
|
|
||||||
|
expect(res.text).toEqual(`Error: File doesn't exist.`)
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Forbidden if filePath outside Drive', async () => {
|
||||||
|
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||||
|
const pathToUpload = '/../path/code.sas'
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.patch('/SASjsApi/drive/file')
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.field('filePath', pathToUpload)
|
||||||
|
.attach('file', fileToAttachPath)
|
||||||
|
.expect(403)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('Error: Cannot modify file outside drive.')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Bad Request if filePath is missing', async () => {
|
||||||
|
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.patch('/SASjsApi/drive/file')
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.attach('file', fileToAttachPath)
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual(`"filePath" is required`)
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should respond with Bad Request if filePath doesn't has correct extension", async () => {
|
||||||
|
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||||
|
const pathToUpload = '/my/path/code.oth'
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.patch('/SASjsApi/drive/file')
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.field('filePath', pathToUpload)
|
||||||
|
.attach('file', fileToAttachPath)
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('Valid extensions for filePath: .sas')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Bad Request if file is missing', async () => {
|
||||||
|
const pathToUpload = '/my/path/code.sas'
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.patch('/SASjsApi/drive/file')
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.field('filePath', pathToUpload)
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('"file" is not present.')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should respond with Bad Request if attached file doesn't has correct extension", async () => {
|
||||||
|
const fileToAttachPath = path.join(__dirname, 'files', 'sample.oth')
|
||||||
|
const pathToUpload = '/my/path/code.sas'
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.patch('/SASjsApi/drive/file')
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.field('filePath', pathToUpload)
|
||||||
|
.attach('file', fileToAttachPath)
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual(
|
||||||
|
`File extension '.oth' not acceptable. Valid extension(s): .sas`
|
||||||
|
)
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Bad Request if attached file exceeds file limit', async () => {
|
||||||
|
const pathToUpload = '/my/path/code.sas'
|
||||||
|
|
||||||
|
const attachedFile = Buffer.from('.'.repeat(20 * 1024 * 1024))
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.patch('/SASjsApi/drive/file')
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.field('filePath', pathToUpload)
|
||||||
|
.attach('file', attachedFile, 'another.sas')
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual(
|
||||||
|
'File size is over limit. File limit is: 10 MB'
|
||||||
|
)
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const getExampleService = (): ServiceMember =>
|
const getExampleService = (): ServiceMember =>
|
||||||
|
|||||||
1
api/src/routes/api/spec/files/sample.oth
Normal file
1
api/src/routes/api/spec/files/sample.oth
Normal file
@@ -0,0 +1 @@
|
|||||||
|
some code of sas
|
||||||
1
api/src/routes/api/spec/files/sample.sas
Normal file
1
api/src/routes/api/spec/files/sample.sas
Normal file
@@ -0,0 +1 @@
|
|||||||
|
some code of sas
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { executeProgramRawValidation, runSASValidation } from '../../utils'
|
import { executeProgramRawValidation } from '../../utils'
|
||||||
import { STPController } from '../../controllers/'
|
import { STPController } from '../../controllers/'
|
||||||
import { FileUploadController } from '../../controllers/internal'
|
import { FileUploadController } from '../../controllers/internal'
|
||||||
|
|
||||||
@@ -14,6 +14,12 @@ stpRouter.get('/execute', async (req, res) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await controller.executeReturnRaw(req, query._program)
|
const response = await controller.executeReturnRaw(req, query._program)
|
||||||
|
|
||||||
|
if (response instanceof Buffer) {
|
||||||
|
res.writeHead(200, (req as any).sasHeaders)
|
||||||
|
return res.end(response)
|
||||||
|
}
|
||||||
|
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const statusCode = err.code
|
const statusCode = err.code
|
||||||
@@ -26,7 +32,7 @@ stpRouter.get('/execute', async (req, res) => {
|
|||||||
|
|
||||||
stpRouter.post(
|
stpRouter.post(
|
||||||
'/execute',
|
'/execute',
|
||||||
fileUploadController.preuploadMiddleware,
|
fileUploadController.preUploadMiddleware,
|
||||||
fileUploadController.getMulterUploadObject().any(),
|
fileUploadController.getMulterUploadObject().any(),
|
||||||
async (req: any, res: any) => {
|
async (req: any, res: any) => {
|
||||||
const { error: errQ, value: query } = executeProgramRawValidation(req.query)
|
const { error: errQ, value: query } = executeProgramRawValidation(req.query)
|
||||||
@@ -40,6 +46,12 @@ stpRouter.post(
|
|||||||
body,
|
body,
|
||||||
query?._program
|
query?._program
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (response instanceof Buffer) {
|
||||||
|
res.writeHead(200, (req as any).sasHeaders)
|
||||||
|
return res.end(response)
|
||||||
|
}
|
||||||
|
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const statusCode = err.code
|
const statusCode = err.code
|
||||||
|
|||||||
9
api/src/routes/setupRoutes.ts
Normal file
9
api/src/routes/setupRoutes.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Express } from 'express'
|
||||||
|
|
||||||
|
import webRouter from './web'
|
||||||
|
import apiRouter from './api'
|
||||||
|
|
||||||
|
export const setupRoutes = (app: Express) => {
|
||||||
|
app.use('/', webRouter)
|
||||||
|
app.use('/SASjsApi', apiRouter)
|
||||||
|
}
|
||||||
@@ -1,10 +1,28 @@
|
|||||||
import appPromise from './app'
|
import { createServer } from 'https'
|
||||||
|
|
||||||
appPromise.then((app) => {
|
import appPromise from './app'
|
||||||
|
import { getCertificates } from './utils'
|
||||||
|
|
||||||
|
appPromise.then(async (app) => {
|
||||||
|
const protocol = process.env.PROTOCOL ?? 'http'
|
||||||
const sasJsPort = process.env.PORT ?? 5000
|
const sasJsPort = process.env.PORT ?? 5000
|
||||||
app.listen(sasJsPort, () => {
|
|
||||||
console.log(
|
console.log('PROTOCOL: ', protocol)
|
||||||
`⚡️[server]: Server is running at http://localhost:${sasJsPort}`
|
|
||||||
)
|
if (protocol !== 'https') {
|
||||||
})
|
app.listen(sasJsPort, () => {
|
||||||
|
console.log(
|
||||||
|
`⚡️[server]: Server is running at http://localhost:${sasJsPort}`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const { key, cert } = await getCertificates()
|
||||||
|
|
||||||
|
const httpsServer = createServer({ key, cert }, app)
|
||||||
|
httpsServer.listen(sasJsPort, () => {
|
||||||
|
console.log(
|
||||||
|
`⚡️[server]: Server is running at https://localhost:${sasJsPort}`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +1,19 @@
|
|||||||
import path from 'path'
|
|
||||||
import mongoose from 'mongoose'
|
import mongoose from 'mongoose'
|
||||||
import { configuration } from '../../package.json'
|
|
||||||
import { getDesktopFields } from '.'
|
|
||||||
import { populateClients } from '../routes/api/auth'
|
import { populateClients } from '../routes/api/auth'
|
||||||
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
|
return
|
||||||
} else {
|
} else {
|
||||||
const { MODE } = process.env
|
const { MODE } = process.env
|
||||||
|
|
||||||
if (MODE?.trim() !== 'server') {
|
if (MODE?.trim() !== 'server') {
|
||||||
console.log('Running in Destop Mode, no DB to connect.')
|
console.log('Running in Destop Mode, no DB to connect.')
|
||||||
|
return
|
||||||
const { sasLoc, driveLoc } = await getDesktopFields()
|
|
||||||
|
|
||||||
process.sasLoc = sasLoc
|
|
||||||
process.driveLoc = driveLoc
|
|
||||||
} else {
|
|
||||||
const { SAS_PATH, DRIVE_PATH } = process.env
|
|
||||||
|
|
||||||
process.sasLoc = SAS_PATH ?? configuration.sasPath
|
|
||||||
process.driveLoc = getRealPath(
|
|
||||||
path.join(process.cwd(), DRIVE_PATH ?? 'tmp')
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('sasLoc: ', process.sasLoc)
|
|
||||||
console.log('sasDrive: ', process.driveLoc)
|
|
||||||
|
|
||||||
if (MODE?.trim() !== 'server') return
|
|
||||||
|
|
||||||
mongoose.connect(process.env.DB_CONNECT as string, async (err) => {
|
mongoose.connect(process.env.DB_CONNECT as string, async (err) => {
|
||||||
if (err) throw err
|
if (err) throw err
|
||||||
|
|
||||||
|
|||||||
25
api/src/utils/extractHeaders.ts
Normal file
25
api/src/utils/extractHeaders.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
const headerUtils = require('http-headers-validation')
|
||||||
|
|
||||||
|
export interface HTTPHeaders {
|
||||||
|
[key: string]: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const extractHeaders = (content?: string): HTTPHeaders => {
|
||||||
|
const headersObj: HTTPHeaders = {}
|
||||||
|
const headersArr = content
|
||||||
|
?.split('\n')
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter((line) => !!line)
|
||||||
|
|
||||||
|
headersArr?.forEach((headerStr) => {
|
||||||
|
const [key, value] = headerStr.split(':').map((data) => data.trim())
|
||||||
|
|
||||||
|
if (value && headerUtils.validateHeader(key, value)) {
|
||||||
|
headersObj[key.toLowerCase()] = value
|
||||||
|
} else {
|
||||||
|
delete headersObj[key.toLowerCase()]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return headersObj
|
||||||
|
}
|
||||||
@@ -8,11 +8,15 @@ export const sysInitCompiledPath = path.join(
|
|||||||
'systemInitCompiled.sas'
|
'systemInitCompiled.sas'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const sasJSCoreMacros = path.join(apiRoot, 'sasjscore')
|
||||||
|
|
||||||
export const getWebBuildFolderPath = () =>
|
export const getWebBuildFolderPath = () =>
|
||||||
path.join(codebaseRoot, 'web', 'build')
|
path.join(codebaseRoot, 'web', 'build')
|
||||||
|
|
||||||
export const getTmpFolderPath = () => process.driveLoc
|
export const getTmpFolderPath = () => process.driveLoc
|
||||||
|
|
||||||
|
export const getTmpUploadsPath = () => path.join(getTmpFolderPath(), 'uploads')
|
||||||
|
|
||||||
export const getTmpFilesFolderPath = () =>
|
export const getTmpFilesFolderPath = () =>
|
||||||
path.join(getTmpFolderPath(), 'files')
|
path.join(getTmpFolderPath(), 'files')
|
||||||
|
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ import { InfoJWT } from '../types'
|
|||||||
|
|
||||||
export const generateAccessToken = (data: InfoJWT) =>
|
export const generateAccessToken = (data: InfoJWT) =>
|
||||||
jwt.sign(data, process.env.ACCESS_TOKEN_SECRET as string, {
|
jwt.sign(data, process.env.ACCESS_TOKEN_SECRET as string, {
|
||||||
expiresIn: '1h'
|
expiresIn: '1day'
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ import { InfoJWT } from '../types'
|
|||||||
|
|
||||||
export const generateRefreshToken = (data: InfoJWT) =>
|
export const generateRefreshToken = (data: InfoJWT) =>
|
||||||
jwt.sign(data, process.env.REFRESH_TOKEN_SECRET as string, {
|
jwt.sign(data, process.env.REFRESH_TOKEN_SECRET as string, {
|
||||||
expiresIn: '1day'
|
expiresIn: '30 days'
|
||||||
})
|
})
|
||||||
|
|||||||
36
api/src/utils/getCertificates.ts
Normal file
36
api/src/utils/getCertificates.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import path from 'path'
|
||||||
|
import { fileExists, getString, readFile } from '@sasjs/utils'
|
||||||
|
|
||||||
|
export const getCertificates = async () => {
|
||||||
|
const { PRIVATE_KEY, FULL_CHAIN } = process.env
|
||||||
|
|
||||||
|
const keyPath = PRIVATE_KEY ?? (await getFileInput('Private Key (PEM)'))
|
||||||
|
const certPath = FULL_CHAIN ?? (await getFileInput('Full Chain (PEM)'))
|
||||||
|
|
||||||
|
console.log('keyPath: ', keyPath)
|
||||||
|
console.log('certPath: ', certPath)
|
||||||
|
|
||||||
|
const key = await readFile(keyPath)
|
||||||
|
const cert = await readFile(certPath)
|
||||||
|
|
||||||
|
return { key, cert }
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFileInput = async (filename: string): Promise<string> => {
|
||||||
|
const validator = async (filePath: string) => {
|
||||||
|
if (!filePath) return `Path to ${filename} is required.`
|
||||||
|
|
||||||
|
if (!(await fileExists(path.join(process.cwd(), filePath)))) {
|
||||||
|
return 'No file found at provided path.'
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetName = await getString(
|
||||||
|
`Please enter path to ${filename} (relative path): `,
|
||||||
|
validator
|
||||||
|
)
|
||||||
|
|
||||||
|
return targetName
|
||||||
|
}
|
||||||
@@ -1,11 +1,16 @@
|
|||||||
export * from './connectDB'
|
export * from './connectDB'
|
||||||
|
export * from './extractHeaders'
|
||||||
export * from './file'
|
export * from './file'
|
||||||
export * from './generateAccessToken'
|
export * from './generateAccessToken'
|
||||||
export * from './generateAuthCode'
|
export * from './generateAuthCode'
|
||||||
export * from './generateRefreshToken'
|
export * from './generateRefreshToken'
|
||||||
|
export * from './isDebugOn'
|
||||||
|
export * from './getCertificates'
|
||||||
export * from './getDesktopFields'
|
export * from './getDesktopFields'
|
||||||
|
export * from './parseLogToArray'
|
||||||
export * from './removeTokensInDB'
|
export * from './removeTokensInDB'
|
||||||
export * from './saveTokensInDB'
|
export * from './saveTokensInDB'
|
||||||
|
export * from './setProcessVariables'
|
||||||
export * from './sleep'
|
export * from './sleep'
|
||||||
export * from './upload'
|
export * from './upload'
|
||||||
export * from './validation'
|
export * from './validation'
|
||||||
|
|||||||
8
api/src/utils/isDebugOn.ts
Normal file
8
api/src/utils/isDebugOn.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { ExecutionVars } from '../controllers/internal'
|
||||||
|
|
||||||
|
export const isDebugOn = (vars: ExecutionVars) => {
|
||||||
|
const debugValue =
|
||||||
|
typeof vars._debug === 'string' ? parseInt(vars._debug) : vars._debug
|
||||||
|
|
||||||
|
return !!(debugValue && debugValue >= 131)
|
||||||
|
}
|
||||||
9
api/src/utils/parseLogToArray.ts
Normal file
9
api/src/utils/parseLogToArray.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export interface LogLine {
|
||||||
|
line: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const parseLogToArray = (content?: string): LogLine[] => {
|
||||||
|
if (!content) return []
|
||||||
|
|
||||||
|
return content.split('\n').map((line) => ({ line: line }))
|
||||||
|
}
|
||||||
31
api/src/utils/setProcessVariables.ts
Normal file
31
api/src/utils/setProcessVariables.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import path from 'path'
|
||||||
|
import { getRealPath } from '@sasjs/utils'
|
||||||
|
|
||||||
|
import { configuration } from '../../package.json'
|
||||||
|
import { getDesktopFields } from '.'
|
||||||
|
|
||||||
|
export const setProcessVariables = async () => {
|
||||||
|
if (process.env.NODE_ENV === 'test') {
|
||||||
|
process.driveLoc = path.join(process.cwd(), 'tmp')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { MODE } = process.env
|
||||||
|
|
||||||
|
if (MODE?.trim() !== 'server') {
|
||||||
|
const { sasLoc, driveLoc } = await getDesktopFields()
|
||||||
|
|
||||||
|
process.sasLoc = sasLoc
|
||||||
|
process.driveLoc = driveLoc
|
||||||
|
} else {
|
||||||
|
const { SAS_PATH, DRIVE_PATH } = process.env
|
||||||
|
|
||||||
|
process.sasLoc = SAS_PATH ?? configuration.sasPath
|
||||||
|
process.driveLoc = getRealPath(
|
||||||
|
path.join(process.cwd(), DRIVE_PATH ?? 'tmp')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('sasLoc: ', process.sasLoc)
|
||||||
|
console.log('sasDrive: ', process.driveLoc)
|
||||||
|
}
|
||||||
52
api/src/utils/specs/extractHeaders.spec.ts
Normal file
52
api/src/utils/specs/extractHeaders.spec.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { extractHeaders } from '..'
|
||||||
|
|
||||||
|
describe('extractHeaders', () => {
|
||||||
|
it('should return valid http headers', () => {
|
||||||
|
const headers = extractHeaders(`
|
||||||
|
Content-type: application/csv
|
||||||
|
Cache-Control: public, max-age=2000
|
||||||
|
Content-type: application/text
|
||||||
|
Cache-Control: public, max-age=1500
|
||||||
|
Content-type: application/zip
|
||||||
|
Cache-Control: public, max-age=1000
|
||||||
|
`)
|
||||||
|
|
||||||
|
expect(headers).toEqual({
|
||||||
|
'content-type': 'application/zip',
|
||||||
|
'cache-control': 'public, max-age=1000'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not return http headers if last occurrence is blank', () => {
|
||||||
|
const headers = extractHeaders(`
|
||||||
|
Content-type: application/csv
|
||||||
|
Cache-Control: public, max-age=1000
|
||||||
|
Content-type: application/text
|
||||||
|
Content-type:
|
||||||
|
`)
|
||||||
|
|
||||||
|
expect(headers).toEqual({ 'cache-control': 'public, max-age=1000' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return only valid http headers', () => {
|
||||||
|
const headers = extractHeaders(`
|
||||||
|
Content-type[]: application/csv
|
||||||
|
Content//-type: application/text
|
||||||
|
Content()-type: application/zip
|
||||||
|
`)
|
||||||
|
|
||||||
|
expect(headers).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return http headers if empty', () => {
|
||||||
|
const headers = extractHeaders('')
|
||||||
|
|
||||||
|
expect(headers).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return http headers if not provided', () => {
|
||||||
|
const headers = extractHeaders()
|
||||||
|
|
||||||
|
expect(headers).toEqual({})
|
||||||
|
})
|
||||||
|
})
|
||||||
33
api/src/utils/specs/parseLogToArray.spec.ts
Normal file
33
api/src/utils/specs/parseLogToArray.spec.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { parseLogToArray } from '..'
|
||||||
|
|
||||||
|
describe('parseLogToArray', () => {
|
||||||
|
it('should parse log to array type', () => {
|
||||||
|
const log = parseLogToArray(`
|
||||||
|
line 1 of log content
|
||||||
|
line 2 of log content
|
||||||
|
line 3 of log content
|
||||||
|
line 4 of log content
|
||||||
|
`)
|
||||||
|
|
||||||
|
expect(log).toEqual([
|
||||||
|
{ line: '' },
|
||||||
|
{ line: 'line 1 of log content' },
|
||||||
|
{ line: 'line 2 of log content' },
|
||||||
|
{ line: 'line 3 of log content' },
|
||||||
|
{ line: 'line 4 of log content' },
|
||||||
|
{ line: ' ' }
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should parse log to array type if empty', () => {
|
||||||
|
const log = parseLogToArray('')
|
||||||
|
|
||||||
|
expect(log).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should parse log to array type if not provided', () => {
|
||||||
|
const log = parseLogToArray()
|
||||||
|
|
||||||
|
expect(log).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -66,15 +66,18 @@ export const registerClientValidation = (data: any): Joi.ValidationResult =>
|
|||||||
clientSecret: Joi.string().required()
|
clientSecret: Joi.string().required()
|
||||||
}).validate(data)
|
}).validate(data)
|
||||||
|
|
||||||
export const getFileDriveValidation = (data: any): Joi.ValidationResult =>
|
export const fileBodyValidation = (data: any): Joi.ValidationResult =>
|
||||||
Joi.object({
|
Joi.object({
|
||||||
filePath: Joi.string().required()
|
filePath: Joi.string().pattern(/.sas$/).required().messages({
|
||||||
|
'string.pattern.base': `Valid extensions for filePath: .sas`
|
||||||
|
})
|
||||||
}).validate(data)
|
}).validate(data)
|
||||||
|
|
||||||
export const updateFileDriveValidation = (data: any): Joi.ValidationResult =>
|
export const fileParamValidation = (data: any): Joi.ValidationResult =>
|
||||||
Joi.object({
|
Joi.object({
|
||||||
filePath: Joi.string().required(),
|
_filePath: Joi.string().pattern(/.sas$/).required().messages({
|
||||||
fileContent: Joi.string().required()
|
'string.pattern.base': `Valid extensions for filePath: .sas`
|
||||||
|
})
|
||||||
}).validate(data)
|
}).validate(data)
|
||||||
|
|
||||||
export const runSASValidation = (data: any): Joi.ValidationResult =>
|
export const runSASValidation = (data: any): Joi.ValidationResult =>
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
16
package-lock.json
generated
16
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "server",
|
"name": "server",
|
||||||
"version": "0.0.15",
|
"version": "0.0.31",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "server",
|
"name": "server",
|
||||||
"version": "0.0.15",
|
"version": "0.0.31",
|
||||||
"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,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "server",
|
"name": "server",
|
||||||
"version": "0.0.15",
|
"version": "0.0.31",
|
||||||
"description": "NodeJS wrapper for calling the SAS binary executable",
|
"description": "NodeJS wrapper for calling the SAS binary executable",
|
||||||
"repository": "https://github.com/sasjs/server",
|
"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 ..",
|
||||||
"server:start": "cd api && npm run start:prod",
|
"server:start": "cd api && npm run start",
|
||||||
"release": "standard-version",
|
"release": "standard-version",
|
||||||
"lint-api:fix": "npx prettier --write \"api/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
"lint-api:fix": "npx prettier --write \"api/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
||||||
"lint-api": "npx prettier --check \"api/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
"lint-api": "npx prettier --check \"api/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
||||||
|
|||||||
22
restClient/auth.rest
Normal file
22
restClient/auth.rest
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
### Get Auth Code
|
||||||
|
POST http://localhost:5000/SASjsApi/auth/authorize
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"username": "secretuser",
|
||||||
|
"password": "secretpassword",
|
||||||
|
"client_id": "clientID1"
|
||||||
|
}
|
||||||
|
|
||||||
|
### Exchange AuthCode with Access/Refresh Tokens
|
||||||
|
POST http://localhost:5000/SASjsApi/auth/token
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"client_id": "clientID1",
|
||||||
|
"client_secret": "clientID1secret",
|
||||||
|
"code": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRfaWQiOiJjbGllbnRJRDEiLCJ1c2VybmFtZSI6InVzZXJuYW1lMSIsImlzYWRtaW4iOmZhbHNlLCJpc2FjdGl2ZSI6dHJ1ZSwiaWF0IjoxNjM1ODA0MDYxLCJleHAiOjE2MzU4MDQwOTF9.jV7DpBWG7XAGODs22zAW_kWOqVLZvOxmmYJGpSNQ-KM"
|
||||||
|
}
|
||||||
|
|
||||||
|
### Perform logout to deactivate access token instantly
|
||||||
|
DELETE http://localhost:5000/SASjsApi/auth/logout
|
||||||
45
restClient/drive.rest
Normal file
45
restClient/drive.rest
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
###
|
||||||
|
POST http://localhost:5000/SASjsApi/drive/deploy
|
||||||
|
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRfaWQiOiJjbGllbnRJRDEiLCJ1c2VybmFtZSI6InVzZXJuYW1lMSIsImlzYWRtaW4iOmZhbHNlLCJpc2FjdGl2ZSI6dHJ1ZSwiaWF0IjoxNjM1ODA0MDc2LCJleHAiOjE2MzU4OTA0NzZ9.Cx1F54ILgAUtnkit0Wg1K1YVO2RdNjOnTKdPhUtDm5I
|
||||||
|
|
||||||
|
### multipart upload to sas server file
|
||||||
|
POST http://localhost:5000/SASjsApi/drive/file
|
||||||
|
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
|
||||||
|
|
||||||
|
------WebKitFormBoundary7MA4YWxkTrZu0gW
|
||||||
|
Content-Disposition: form-data; name="filePath"
|
||||||
|
|
||||||
|
/saad/files/new.sas
|
||||||
|
------WebKitFormBoundary7MA4YWxkTrZu0gW
|
||||||
|
Content-Disposition: form-data; name="file"; filename="sample_new.sas"
|
||||||
|
Content-Type: application/octet-stream
|
||||||
|
|
||||||
|
< ./sample.sas
|
||||||
|
------WebKitFormBoundary7MA4YWxkTrZu0gW--
|
||||||
|
|
||||||
|
### multipart upload to sas server file text
|
||||||
|
POST http://localhost:5000/SASjsApi/drive/file
|
||||||
|
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
|
||||||
|
|
||||||
|
------WebKitFormBoundary7MA4YWxkTrZu0gW \n
|
||||||
|
Content-Disposition: form-data; name="filePath"
|
||||||
|
|
||||||
|
/saad/files/new2.sas
|
||||||
|
------WebKitFormBoundary7MA4YWxkTrZu0gW
|
||||||
|
Content-Disposition: form-data; name="file"; filename="sample_new.sas"
|
||||||
|
Content-Type: text/plain
|
||||||
|
|
||||||
|
SOME CONTENTS OF SAS FILE IN REQUEST
|
||||||
|
|
||||||
|
------WebKitFormBoundary7MA4YWxkTrZu0gW--
|
||||||
|
|
||||||
|
|
||||||
|
Users
|
||||||
|
"username": "username1",
|
||||||
|
"password": "some password",
|
||||||
|
|
||||||
|
"username": "username2",
|
||||||
|
"password": "some password",
|
||||||
|
Admins
|
||||||
|
"username": "secretuser",
|
||||||
|
"password": "secretpassword",
|
||||||
@@ -23,7 +23,7 @@ Content-Type: application/json
|
|||||||
"client_secret": "newClientSecret"
|
"client_secret": "newClientSecret"
|
||||||
}
|
}
|
||||||
###
|
###
|
||||||
POST https://sas.analytium.co.uk:5002/SASjsApi/auth/authorize
|
POST http://localhost:5000/SASjsApi/auth/authorize
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -45,6 +45,41 @@ Content-Type: application/json
|
|||||||
###
|
###
|
||||||
DELETE http://localhost:5000/SASjsApi/auth/logout
|
DELETE http://localhost:5000/SASjsApi/auth/logout
|
||||||
|
|
||||||
|
###
|
||||||
|
GET http://localhost:5000/SASjsApi/session
|
||||||
|
|
||||||
|
|
||||||
|
### multipart upload to sas server file
|
||||||
|
POST http://localhost:5000/SASjsApi/drive/file
|
||||||
|
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
|
||||||
|
|
||||||
|
------WebKitFormBoundary7MA4YWxkTrZu0gW
|
||||||
|
Content-Disposition: form-data; name="filePath"
|
||||||
|
|
||||||
|
/saad/files/new.sas
|
||||||
|
------WebKitFormBoundary7MA4YWxkTrZu0gW
|
||||||
|
Content-Disposition: form-data; name="file"; filename="sample_new.sas"
|
||||||
|
Content-Type: application/octet-stream
|
||||||
|
|
||||||
|
< ./sample.sas
|
||||||
|
------WebKitFormBoundary7MA4YWxkTrZu0gW--
|
||||||
|
|
||||||
|
### multipart upload to sas server file text
|
||||||
|
POST http://localhost:5000/SASjsApi/drive/file
|
||||||
|
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
|
||||||
|
|
||||||
|
------WebKitFormBoundary7MA4YWxkTrZu0gW \n
|
||||||
|
Content-Disposition: form-data; name="filePath"
|
||||||
|
|
||||||
|
/saad/files/new2.sas
|
||||||
|
------WebKitFormBoundary7MA4YWxkTrZu0gW
|
||||||
|
Content-Disposition: form-data; name="file"; filename="sample_new.sas"
|
||||||
|
Content-Type: text/plain
|
||||||
|
|
||||||
|
SOME CONTENTS OF SAS FILE IN REQUEST
|
||||||
|
|
||||||
|
------WebKitFormBoundary7MA4YWxkTrZu0gW--
|
||||||
|
|
||||||
|
|
||||||
Users
|
Users
|
||||||
"username": "username1",
|
"username": "username1",
|
||||||
1
restClient/sample.sas
Normal file
1
restClient/sample.sas
Normal file
@@ -0,0 +1 @@
|
|||||||
|
some code of sas
|
||||||
2
restClient/session.rest
Normal file
2
restClient/session.rest
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
### Get current user's info via access token
|
||||||
|
GET http://localhost:5000/SASjsApi/session
|
||||||
10
restClient/users.rest
Normal file
10
restClient/users.rest
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
### Create User
|
||||||
|
POST http://localhost:5000/SASjsApi/user
|
||||||
|
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRfaWQiOiJjbGllbnRJRDEiLCJ1c2VybmFtZSI6InNlY3JldHVzZXIiLCJpc2FkbWluIjp0cnVlLCJpc2FjdGl2ZSI6dHJ1ZSwiaWF0IjoxNjM1ODAzOTc3LCJleHAiOjE2MzU4OTAzNzd9.f-FLgLwryKvB5XrihdzaGZajO3d5E5OHEEuJI_03GRI
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"displayname": "User 2",
|
||||||
|
"username": "username2",
|
||||||
|
"password": "some password"
|
||||||
|
}
|
||||||
4
web/.babelrc
Normal file
4
web/.babelrc
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"presets": ["@babel/env", "@babel/react", "@babel/preset-typescript"],
|
||||||
|
"plugins": ["@babel/plugin-proposal-class-properties"]
|
||||||
|
}
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
REACT_APP_PORT_API=[place sasjs server port] default value is 5000
|
PORT_API=[place sasjs server port] default value is 5000
|
||||||
REACT_APP_CLIENT_ID=<place clientId here>
|
CLIENT_ID=<place clientId here>
|
||||||
35256
web/package-lock.json
generated
35256
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -3,10 +3,8 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "npx webpack-dev-server --config webpack.dev.ts --hot",
|
||||||
"build": "react-scripts build",
|
"build": "npx webpack --config webpack.prod.ts"
|
||||||
"test": "react-scripts test",
|
|
||||||
"eject": "react-scripts eject"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.4.1",
|
"@emotion/react": "^11.4.1",
|
||||||
@@ -22,19 +20,43 @@
|
|||||||
"@types/jest": "^26.0.24",
|
"@types/jest": "^26.0.24",
|
||||||
"@types/node": "^12.20.28",
|
"@types/node": "^12.20.28",
|
||||||
"@types/react": "^17.0.27",
|
"@types/react": "^17.0.27",
|
||||||
"@types/react-dom": "^17.0.9",
|
"axios": "^0.24.0",
|
||||||
"axios": "^0.22.0",
|
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-router-dom": "^5.3.0",
|
"react-router-dom": "^5.3.0"
|
||||||
"react-scripts": "4.0.3",
|
|
||||||
"typescript": "^4.4.3"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.16.0",
|
||||||
|
"@babel/node": "^7.16.0",
|
||||||
|
"@babel/plugin-proposal-class-properties": "^7.16.0",
|
||||||
|
"@babel/preset-env": "^7.16.4",
|
||||||
|
"@babel/preset-react": "^7.16.0",
|
||||||
|
"@babel/preset-typescript": "^7.16.0",
|
||||||
|
"@types/dotenv-webpack": "^7.0.3",
|
||||||
"@types/prismjs": "^1.16.6",
|
"@types/prismjs": "^1.16.6",
|
||||||
|
"@types/react": "^17.0.37",
|
||||||
|
"@types/react-dom": "^17.0.11",
|
||||||
"@types/react-router-dom": "^5.3.1",
|
"@types/react-router-dom": "^5.3.1",
|
||||||
|
"babel-loader": "^8.2.3",
|
||||||
"babel-plugin-prismjs": "^2.1.0",
|
"babel-plugin-prismjs": "^2.1.0",
|
||||||
"prettier": "^2.4.1"
|
"copy-webpack-plugin": "^10.0.0",
|
||||||
|
"css-loader": "^6.5.1",
|
||||||
|
"dotenv-webpack": "^7.1.0",
|
||||||
|
"eslint": "^8.5.0",
|
||||||
|
"eslint-config-react-app": "^7.0.0",
|
||||||
|
"eslint-webpack-plugin": "^3.1.1",
|
||||||
|
"file-loader": "^6.2.0",
|
||||||
|
"html-webpack-plugin": "5.5.0",
|
||||||
|
"path": "0.12.7",
|
||||||
|
"prettier": "^2.4.1",
|
||||||
|
"sass": "^1.44.0",
|
||||||
|
"sass-loader": "^12.3.0",
|
||||||
|
"style-loader": "^3.3.1",
|
||||||
|
"ts-loader": "^9.2.6",
|
||||||
|
"typescript": "^4.5.2",
|
||||||
|
"webpack": "5.64.3",
|
||||||
|
"webpack-cli": "^4.9.2",
|
||||||
|
"webpack-dev-server": "4.7.4"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": [
|
"extends": [
|
||||||
|
|||||||
@@ -8,11 +8,10 @@ const headers = {
|
|||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
const { NODE_ENV, REACT_APP_PORT_API } = process.env
|
const NODE_ENV = process.env.NODE_ENV
|
||||||
|
const PORT_API = process.env.PORT_API
|
||||||
const baseUrl =
|
const baseUrl =
|
||||||
NODE_ENV === 'development'
|
NODE_ENV === 'development' ? `http://localhost:${PORT_API ?? 5000}` : ''
|
||||||
? `http://localhost:${REACT_APP_PORT_API ?? 5000}`
|
|
||||||
: ''
|
|
||||||
|
|
||||||
const getAuthCode = async (credentials: any) => {
|
const getAuthCode = async (credentials: any) => {
|
||||||
return fetch(`${baseUrl}/SASjsApi/auth/authorize`, {
|
return fetch(`${baseUrl}/SASjsApi/auth/authorize`, {
|
||||||
@@ -46,7 +45,7 @@ const Login = ({ setTokens, getCodeOnly }: any) => {
|
|||||||
error = false
|
error = false
|
||||||
setErrorMessage('')
|
setErrorMessage('')
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
let { REACT_APP_CLIENT_ID: clientId } = process.env
|
let clientId = process.env.CLIENT_ID
|
||||||
|
|
||||||
if (getCodeOnly) {
|
if (getCodeOnly) {
|
||||||
const params = new URLSearchParams(location.search)
|
const params = new URLSearchParams(location.search)
|
||||||
|
|||||||
@@ -36,11 +36,10 @@ export default function useTokens() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { NODE_ENV, REACT_APP_PORT_API } = process.env
|
const NODE_ENV = process.env.NODE_ENV
|
||||||
|
const PORT_API = process.env.PORT_API
|
||||||
const baseUrl =
|
const baseUrl =
|
||||||
NODE_ENV === 'development'
|
NODE_ENV === 'development' ? `http://localhost:${PORT_API ?? 5000}` : ''
|
||||||
? `http://localhost:${REACT_APP_PORT_API ?? 5000}`
|
|
||||||
: ''
|
|
||||||
|
|
||||||
const isAbsoluteURLRegex = /^(?:\w+:)\/\//
|
const isAbsoluteURLRegex = /^(?:\w+:)\/\//
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const Main = (props: any) => {
|
|||||||
axios
|
axios
|
||||||
.get(`/SASjsApi/drive/file?filePath=${props.selectedFilePath}`)
|
.get(`/SASjsApi/drive/file?filePath=${props.selectedFilePath}`)
|
||||||
.then((res: any) => {
|
.then((res: any) => {
|
||||||
setFileContent(res.data.fileContent)
|
setFileContent(res.data)
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
@@ -42,11 +42,15 @@ const Main = (props: any) => {
|
|||||||
setEditMode(true)
|
setEditMode(true)
|
||||||
} else {
|
} else {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
|
|
||||||
|
const formData = new FormData()
|
||||||
|
|
||||||
|
const stringBlob = new Blob([fileContent], { type: 'text/plain' })
|
||||||
|
formData.append('file', stringBlob, 'filename.sas')
|
||||||
|
formData.append('filePath', props.selectedFilePath)
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.patch(`/SASjsApi/drive/file`, {
|
.patch(`/SASjsApi/drive/file`, formData)
|
||||||
filePath: props.selectedFilePath,
|
|
||||||
fileContent: fileContent
|
|
||||||
})
|
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
setEditMode(false)
|
setEditMode(false)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -46,7 +46,11 @@ const Studio = () => {
|
|||||||
axios
|
axios
|
||||||
.post(`/SASjsApi/code/execute`, { code })
|
.post(`/SASjsApi/code/execute`, { code })
|
||||||
.then((res: any) => {
|
.then((res: any) => {
|
||||||
setLog(`<div><h2>SAS Log</h2><pre>${res?.data?.log}</pre></div>`)
|
const parsedLog = res?.data?.log
|
||||||
|
.map((logLine: any) => logLine.line)
|
||||||
|
.join('\n')
|
||||||
|
|
||||||
|
setLog(`<div><h2>SAS Log</h2><pre>${parsedLog}</pre></div>`)
|
||||||
|
|
||||||
let weboutString: string
|
let weboutString: string
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
<link rel="icon" href="favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
<meta name="description" content="SASjs Server Web Interface" />
|
<meta name="description" content="SASjs Server Web Interface" />
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
manifest.json provides metadata used when your web app is installed on a
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||||
-->
|
-->
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
<link rel="manifest" href="manifest.json" />
|
||||||
<!--
|
<!--
|
||||||
Notice the use of %PUBLIC_URL% in the tags above.
|
Notice the use of %PUBLIC_URL% in the tags above.
|
||||||
It will be replaced with the URL of the `public` folder during the build.
|
It will be replaced with the URL of the `public` folder during the build.
|
||||||
60
web/webpack.common.ts
Normal file
60
web/webpack.common.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import path from 'path'
|
||||||
|
import { Configuration } from 'webpack'
|
||||||
|
import HtmlWebpackPlugin from 'html-webpack-plugin'
|
||||||
|
import CopyPlugin from 'copy-webpack-plugin'
|
||||||
|
import dotenv from 'dotenv-webpack'
|
||||||
|
|
||||||
|
const config: Configuration = {
|
||||||
|
entry: path.join(__dirname, 'src', 'index.tsx'),
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.tsx', '.ts', '.js', '.jsx']
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.(js|jsx)$/,
|
||||||
|
exclude: /node_modules/,
|
||||||
|
use: ['babel-loader']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(ts|tsx)$/,
|
||||||
|
exclude: /node_modules/,
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: 'ts-loader',
|
||||||
|
options: {
|
||||||
|
compilerOptions: {
|
||||||
|
noEmit: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.css$/,
|
||||||
|
exclude: ['/node_modules/'],
|
||||||
|
use: ['style-loader', 'css-loader']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.scss$/,
|
||||||
|
exclude: ['/node_modules/'],
|
||||||
|
use: ['style-loader', 'css-loader', 'sass-loader']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(jpg|jpeg|png|gif|mp3|svg)$/,
|
||||||
|
use: ['file-loader']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
template: path.join(__dirname, 'src', 'index.html')
|
||||||
|
}),
|
||||||
|
new CopyPlugin({
|
||||||
|
patterns: [{ from: 'public' }]
|
||||||
|
}),
|
||||||
|
new dotenv()
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config
|
||||||
28
web/webpack.dev.ts
Normal file
28
web/webpack.dev.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import path from 'path'
|
||||||
|
import { Configuration as WebpackConfiguration } from 'webpack'
|
||||||
|
import { Configuration as WebpackDevServerConfiguration } from 'webpack-dev-server'
|
||||||
|
import { merge } from 'webpack-merge'
|
||||||
|
|
||||||
|
import common from './webpack.common'
|
||||||
|
|
||||||
|
interface Configuration extends WebpackConfiguration {
|
||||||
|
devServer?: WebpackDevServerConfiguration
|
||||||
|
}
|
||||||
|
|
||||||
|
const devConfig: Configuration = merge(common, {
|
||||||
|
mode: 'development',
|
||||||
|
output: {
|
||||||
|
path: path.join(__dirname, 'build'),
|
||||||
|
filename: 'index.bundle.js',
|
||||||
|
publicPath: '/'
|
||||||
|
},
|
||||||
|
devServer: {
|
||||||
|
static: {
|
||||||
|
directory: path.join(__dirname, 'build')
|
||||||
|
},
|
||||||
|
historyApiFallback: true,
|
||||||
|
port: 3000
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default devConfig
|
||||||
19
web/webpack.prod.ts
Normal file
19
web/webpack.prod.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import path from 'path'
|
||||||
|
import { Configuration } from 'webpack'
|
||||||
|
import { merge } from 'webpack-merge'
|
||||||
|
|
||||||
|
import common from './webpack.common'
|
||||||
|
|
||||||
|
const prodConfig: Configuration = merge(common, {
|
||||||
|
mode: 'production',
|
||||||
|
output: {
|
||||||
|
path: path.join(__dirname, 'build'),
|
||||||
|
filename: 'index.bundle.js',
|
||||||
|
publicPath: './'
|
||||||
|
},
|
||||||
|
performance: {
|
||||||
|
hints: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default prodConfig
|
||||||
Reference in New Issue
Block a user