mirror of
https://github.com/sasjs/adapter.git
synced 2026-01-03 18:50:05 +00:00
Compare commits
142 Commits
snyk-upgra
...
v3.10.11
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92be5a2dca | ||
|
|
f58f2eba97 | ||
|
|
e37bb182c3 | ||
|
|
504777603c | ||
| 706cbe5513 | |||
| 88eadd27aa | |||
| 4ed9f87434 | |||
|
|
f0f80a1c1f | ||
| d0d8d58945 | |||
|
|
657721d7a3 | ||
|
|
a39faa0f4b | ||
|
|
7b8fb774cc | ||
|
|
982c4c329c | ||
| 8617e2dc57 | |||
|
|
d3d62f6888 | ||
|
|
bf35e52962 | ||
| 22eca50e3f | |||
|
|
eb83101dbf | ||
|
|
56d84e1940 | ||
|
|
283800dfa6 | ||
|
|
c073d72dd4 | ||
|
|
f5d40eaaf7 | ||
|
|
8e9cf98985 | ||
|
|
79ba044dea | ||
|
|
9329dc848a | ||
|
|
98c492e85e | ||
| d1fcc2ca0a | |||
|
|
122f302bae | ||
|
|
c3a0ad1f41 | ||
|
|
a28b48f815 | ||
|
|
9b6a42e412 | ||
|
|
db60962c1e | ||
|
|
1eae59ad3b | ||
|
|
d485023d65 | ||
|
|
c2f21babb4 | ||
|
|
dd788ae423 | ||
|
|
a113c95441 | ||
|
|
489947bcae | ||
|
|
1596173dda | ||
|
|
bb1b2ddcb2 | ||
| a3cc274ef1 | |||
| 451d0906fa | |||
| dd6b89b0d0 | |||
|
|
f602d5baf0 | ||
|
|
4744dbf196 | ||
|
|
f0525c5796 | ||
|
|
01bcfe176a | ||
| 076ed1cc7a | |||
| 0a3289b577 | |||
|
|
cbb55ff426 | ||
| 6d47174a5e | |||
|
|
ae13ca523c | ||
| 843cee4dbe | |||
|
|
1ac88ae102 | ||
|
|
a79766c00c | ||
|
|
3bc70b91b8 | ||
|
|
b0b1c32180 | ||
|
|
5b81e0bf4a | ||
|
|
54a33ac98a | ||
|
|
dd237ceeec | ||
| 552540fb88 | |||
| 6d50d4030e | |||
|
|
8856c3b6ec | ||
|
|
b8ea618f1e | ||
|
|
10c72e6483 | ||
|
|
9d03b54fba | ||
|
|
38ad9abfbc | ||
| 42d9a85cfa | |||
| 39f14cc5a0 | |||
| 29a65052dc | |||
| 63328163ab | |||
|
|
aebf4ea8d8 | ||
|
|
59e3edf4b3 | ||
| 8f309143e9 | |||
| a65d4257a5 | |||
| 5d2f1d306a | |||
| f7bd63ee7f | |||
| a4cd320272 | |||
| c243f25477 | |||
| 382a19ccfc | |||
| acc56e3a53 | |||
| 1d0fb1774a | |||
| b67340cc70 | |||
| 831023fe1c | |||
| 04f7cdf8ff | |||
| d01a6a60ad | |||
| 7c17f0f584 | |||
| 092b995d14 | |||
| eb296bf93c | |||
| e2e15ce8e1 | |||
| 3c59db91cd | |||
| a97b2f43ca | |||
| fdd3a261e5 | |||
| f69e5afaf0 | |||
| 7b78f65c4a | |||
| aabe473ef6 | |||
| 8a7d08c3b9 | |||
| d2ea67e5d6 | |||
| b0df4cb7ee | |||
| 1e5e803c92 | |||
| ad34a9a4db | |||
| 11e006741f | |||
| 0bf385d1e0 | |||
| b217499b59 | |||
| d123296359 | |||
| 50ab866652 | |||
| f1252537a6 | |||
| b1682b6f32 | |||
| ded7990096 | |||
| 52e95e3455 | |||
| 1d13252045 | |||
| f4630309de | |||
| 27b6e46973 | |||
| e5262a18d4 | |||
| cf10e83e91 | |||
| 0bd156141c | |||
| 890608a3e8 | |||
| a615c5fdb6 | |||
| ca7ee83f7f | |||
| 97a530cc66 | |||
| 317c8c81a0 | |||
| c87776ca1b | |||
| 04032831c3 | |||
| c8fb141048 | |||
| a429581089 | |||
| dd05258dd2 | |||
| c0e5327e49 | |||
| ffba2acad0 | |||
| 64c624bf2e | |||
| 2506d28dd4 | |||
| b6a438d222 | |||
| 6f24a55a04 | |||
| 55045eb101 | |||
| f5e367a645 | |||
| 017dc3a8b5 | |||
| 7285a3f40b | |||
| 438b39ceca | |||
| a39b9ea38f | |||
| 836c3ba518 | |||
| 2d2b4660dc | |||
| f25035ae1d | |||
| ff32f648da |
12
.git-hooks/pre-commit
Executable file
12
.git-hooks/pre-commit
Executable file
@@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Using `--silent` helps for showing any errs in the first line of the response
|
||||||
|
# The first line is picked up by the VS Code GIT UI popup when rc is not 0
|
||||||
|
|
||||||
|
if npm run --silent lint:silent ; then
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
npm run --silent lint:fix
|
||||||
|
echo "❌ Prettier check failed! We ran lint:fix for you. Please add & commit again."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
8
.github/dependabot.yml
vendored
8
.github/dependabot.yml
vendored
@@ -1,7 +1,7 @@
|
|||||||
version: 2
|
version: 2
|
||||||
updates:
|
updates:
|
||||||
- package-ecosystem: npm
|
- package-ecosystem: "npm"
|
||||||
directory: '/'
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: monthly
|
interval: "monthly"
|
||||||
open-pull-requests-limit: 2
|
open-pull-requests-limit: 1
|
||||||
|
|||||||
30
.github/vpn/config.ovpn
vendored
Normal file
30
.github/vpn/config.ovpn
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
cipher AES-256-CBC
|
||||||
|
setenv FORWARD_COMPATIBLE 1
|
||||||
|
client
|
||||||
|
server-poll-timeout 4
|
||||||
|
nobind
|
||||||
|
remote vpn.analytium.co.uk 1194 udp
|
||||||
|
remote vpn.analytium.co.uk 1194 udp
|
||||||
|
remote vpn.analytium.co.uk 443 tcp
|
||||||
|
remote vpn.analytium.co.uk 1194 udp
|
||||||
|
remote vpn.analytium.co.uk 1194 udp
|
||||||
|
remote vpn.analytium.co.uk 1194 udp
|
||||||
|
remote vpn.analytium.co.uk 1194 udp
|
||||||
|
remote vpn.analytium.co.uk 1194 udp
|
||||||
|
dev tun
|
||||||
|
dev-type tun
|
||||||
|
ns-cert-type server
|
||||||
|
setenv opt tls-version-min 1.0 or-highest
|
||||||
|
reneg-sec 604800
|
||||||
|
sndbuf 0
|
||||||
|
rcvbuf 0
|
||||||
|
# NOTE: LZO commands are pushed by the Access Server at connect time.
|
||||||
|
# NOTE: The below line doesn't disable LZO.
|
||||||
|
comp-lzo no
|
||||||
|
verb 3
|
||||||
|
setenv PUSH_PEER_INFO
|
||||||
|
|
||||||
|
ca ca.crt
|
||||||
|
cert user.crt
|
||||||
|
key user.key
|
||||||
|
tls-auth tls.key 1
|
||||||
67
.github/workflows/build.yml
vendored
67
.github/workflows/build.yml
vendored
@@ -4,7 +4,6 @@
|
|||||||
name: SASjs Build
|
name: SASjs Build
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -22,19 +21,77 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: npm
|
cache: npm
|
||||||
|
|
||||||
- name: Check npm audit
|
- name: Check npm audit
|
||||||
run: npm audit --production --audit-level=low
|
run: npm audit --production --audit-level=low
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
- name: Check code style
|
- name: Check code style
|
||||||
run: npm run lint
|
run: npm run lint
|
||||||
|
|
||||||
- name: Run unit tests
|
- name: Run unit tests
|
||||||
run: npm test
|
run: npm test
|
||||||
- name: Generate coverage report
|
|
||||||
uses: artiomtr/jest-coverage-report-action@v2.0-rc.2
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
- name: Build Package
|
- name: Build Package
|
||||||
run: npm run package:lib
|
run: npm run package:lib
|
||||||
env:
|
env:
|
||||||
CI: true
|
CI: true
|
||||||
|
|
||||||
|
- name: Install SSH Key
|
||||||
|
uses: shimataro/ssh-key-action@v2
|
||||||
|
with:
|
||||||
|
key: ${{ secrets.DCGITLAB_KEY }}
|
||||||
|
known_hosts: 'placeholder'
|
||||||
|
|
||||||
|
- name: Write VPN Files
|
||||||
|
run: |
|
||||||
|
echo "$CA_CRT" > .github/vpn/ca.crt
|
||||||
|
echo "$USER_CRT" > .github/vpn/user.crt
|
||||||
|
echo "$USER_KEY" > .github/vpn/user.key
|
||||||
|
echo "$TLS_KEY" > .github/vpn/tls.key
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
CA_CRT: ${{ secrets.CA_CRT}}
|
||||||
|
USER_CRT: ${{ secrets.USER_CRT }}
|
||||||
|
USER_KEY: ${{ secrets.USER_KEY }}
|
||||||
|
TLS_KEY: ${{ secrets.TLS_KEY }}
|
||||||
|
|
||||||
|
- name: Install Open VPN
|
||||||
|
run: |
|
||||||
|
sudo apt install apt-transport-https
|
||||||
|
sudo wget https://swupdate.openvpn.net/repos/openvpn-repo-pkg-key.pub
|
||||||
|
sudo apt-key add openvpn-repo-pkg-key.pub
|
||||||
|
sudo wget -O /etc/apt/sources.list.d/openvpn3.list https://swupdate.openvpn.net/community/openvpn3/repos/openvpn3-focal.list
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install openvpn3=16~beta+focal
|
||||||
|
|
||||||
|
- name: Start Open VPN 3
|
||||||
|
run: openvpn3 session-start --config .github/vpn/config.ovpn
|
||||||
|
|
||||||
|
- name: Deploy sasjs-tests
|
||||||
|
run: |
|
||||||
|
npm install -g replace-in-files-cli
|
||||||
|
cd sasjs-tests
|
||||||
|
replace-in-files --regex='"@sasjs/adapter".*' --replacement='"@sasjs/adapter":"latest",' ./package.json
|
||||||
|
npm i
|
||||||
|
replace-in-files --regex='"serverUrl".*' --replacement='"serverUrl":"${{ secrets.SASJS_SERVER_URL }}",' ./public/config.json
|
||||||
|
replace-in-files --regex='"userName".*' --replacement='"userName":"${{ secrets.SASJS_USERNAME }}",' ./public/config.json
|
||||||
|
replace-in-files --regex='"password".*' --replacement='"password":"${{ secrets.SASJS_PASSWORD }}",' ./public/config.json
|
||||||
|
replace-in-files --regex='"serverType".*' --replacement='"serverType":"SASJS",' ./public/config.json
|
||||||
|
npm run update:adapter && npm run build
|
||||||
|
scp -o stricthostkeychecking=no -r ./build/* ${{ secrets.DCGITLAB_DEPLOY_PATH_VIYA }}
|
||||||
|
|
||||||
|
- name: Run cypress on sasjs
|
||||||
|
run: |
|
||||||
|
replace-in-files --regex='"sasjsTestsUrl".*' --replacement='"sasjsTestsUrl":"${{ secrets.SASJS_TEST_URL_VIYA }}",' ./cypress.json
|
||||||
|
replace-in-files --regex='"username".*' --replacement='"username":"${{ secrets.SASJS_USERNAME }}",' ./cypress.json
|
||||||
|
replace-in-files --regex='"password".*' --replacement='"password":"${{ secrets.SASJS_PASSWORD }}",' ./cypress.json
|
||||||
|
sh ./sasjs-cypress-run.sh ${{ secrets.DISCORD_WEBHOOK }} https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}
|
||||||
|
|
||||||
|
# For some reason if coverage report action is run before other commands, those commands can't access the directories and files on which they depend on
|
||||||
|
- name: Generate coverage report
|
||||||
|
uses: artiomtr/jest-coverage-report-action@v2.0-rc.2
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
17
.github/workflows/npmpublish.yml
vendored
17
.github/workflows/npmpublish.yml
vendored
@@ -11,15 +11,28 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [lts/fermium]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- uses: actions/checkout@v2
|
||||||
uses: actions/checkout@v2
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
cache: npm
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
- name: Check code style
|
- name: Check code style
|
||||||
run: npm run lint
|
run: npm run lint
|
||||||
|
|
||||||
- name: Build Project
|
- name: Build Project
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
- name: Semantic Release
|
- name: Semantic Release
|
||||||
uses: cycjimmy/semantic-release-action@v2
|
uses: cycjimmy/semantic-release-action@v2
|
||||||
env:
|
env:
|
||||||
|
|||||||
10
.npmignore
10
.npmignore
@@ -4,3 +4,13 @@ docs/
|
|||||||
*.md
|
*.md
|
||||||
*.spec.ts
|
*.spec.ts
|
||||||
.all-contributorsrc
|
.all-contributorsrc
|
||||||
|
cypress/
|
||||||
|
.gitpod.yml
|
||||||
|
.prettierrc
|
||||||
|
cypress.json
|
||||||
|
jest.config.js
|
||||||
|
sasjs-cypress-run.sh
|
||||||
|
tsconfig.json
|
||||||
|
tslint.json
|
||||||
|
typedoc.json
|
||||||
|
webpack.config.js
|
||||||
|
|||||||
@@ -16,5 +16,5 @@ No PR (that involves a non-trivial code change) should be merged, unless all ite
|
|||||||
|
|
||||||
|
|
||||||
- [ ] All `sasjs-cli` unit tests are passing (`npm test`).
|
- [ ] All `sasjs-cli` unit tests are passing (`npm test`).
|
||||||
- [ ] All `sasjs-tests` are passing (instructions available [here](https://github.com/sasjs/adapter/blob/master/sasjs-tests/README.md)).
|
- (CI Runs this) All `sasjs-tests` are passing. If you want to run it manually (instructions available [here](https://github.com/sasjs/adapter/blob/master/sasjs-tests/README.md)).
|
||||||
- [ ] [Data Controller](https://datacontroller.io) builds and is functional on both SAS 9 and Viya
|
- [ ] [Data Controller](https://datacontroller.io) builds and is functional on both SAS 9 and Viya
|
||||||
|
|||||||
32
README.md
32
README.md
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
[![npm package][npm-image]][npm-url]
|
[![npm package][npm-image]][npm-url]
|
||||||
[![Github Workflow][githubworkflow-image]][githubworkflow-url]
|
[![Github Workflow][githubworkflow-image]][githubworkflow-url]
|
||||||
[![Dependency Status][dependency-image]][dependency-url]
|
|
||||||
[]()
|
[]()
|
||||||

|

|
||||||
[](/LICENSE)
|
[](/LICENSE)
|
||||||
@@ -16,7 +15,6 @@
|
|||||||
[githubworkflow-image]:https://github.com/sasjs/adapter/actions/workflows/build.yml/badge.svg
|
[githubworkflow-image]:https://github.com/sasjs/adapter/actions/workflows/build.yml/badge.svg
|
||||||
[githubworkflow-url]:https://github.com/sasjs/adapter/blob/main/.github/workflows/build.yml
|
[githubworkflow-url]:https://github.com/sasjs/adapter/blob/main/.github/workflows/build.yml
|
||||||
[dependency-image]:https://david-dm.org/sasjs/adapter.svg
|
[dependency-image]:https://david-dm.org/sasjs/adapter.svg
|
||||||
[dependency-url]:https://github.com/sasjs/adapter/blob/main/package.json
|
|
||||||
|
|
||||||
SASjs is a open-source framework for building Web Apps on SAS® platforms. You can use as much or as little of it as you like. This repository contains the JS adapter, the part that handles the to/from SAS communication on the client side. There are 3 ways to install it:
|
SASjs is a open-source framework for building Web Apps on SAS® platforms. You can use as much or as little of it as you like. This repository contains the JS adapter, the part that handles the to/from SAS communication on the client side. There are 3 ways to install it:
|
||||||
|
|
||||||
@@ -32,7 +30,7 @@ For more information on building web apps with SAS, check out [sasjs.io](https:/
|
|||||||
|
|
||||||
## None of this makes sense. How do I build an app with it?
|
## None of this makes sense. How do I build an app with it?
|
||||||
|
|
||||||
Ok ok. Deploy this [example.html](https://raw.githubusercontent.com/sasjs/adapter/master/example.html) file to your web server, and update `servertype` to `SAS9` or `SASVIYA` depending on your backend.
|
Ok ok. Deploy this [example.html](https://raw.githubusercontent.com/sasjs/adapter/master/example.html) file to your web server, and update `servertype` to `SAS9`, `SASVIYA`, or `SASJS` depending on your backend.
|
||||||
|
|
||||||
The backend part can be deployed as follows:
|
The backend part can be deployed as follows:
|
||||||
|
|
||||||
@@ -52,7 +50,7 @@ parmcards4;
|
|||||||
%webout(OBJ,areas)
|
%webout(OBJ,areas)
|
||||||
%webout(CLOSE)
|
%webout(CLOSE)
|
||||||
;;;;
|
;;;;
|
||||||
%mp_createwebservice(path=&appLoc/common,name=getdata)
|
%mx_createwebservice(path=&appLoc/common,name=getdata)
|
||||||
```
|
```
|
||||||
|
|
||||||
You now have a simple web app with a backend service!
|
You now have a simple web app with a backend service!
|
||||||
@@ -96,10 +94,10 @@ const sasJs = new SASjs({your config})
|
|||||||
More on the config later.
|
More on the config later.
|
||||||
|
|
||||||
### SAS Logon
|
### SAS Logon
|
||||||
All authentication from the adapter is done against SASLogon. There are two approaches that can be taken, which are configured using the `LoginMechanism` attribute of the sasJs config object (above):
|
All authentication from the adapter is done against SASLogon. There are two approaches that can be taken, which are configured using the `loginMechanism` attribute of the sasJs config object (above):
|
||||||
|
|
||||||
* `LoginMechanism:'Redirected'` - this approach enables authentication through a SASLogon window, supporting complex authentication flows (such as 2FA) and avoids the need to handle passwords in the application itself. The styling of the window can be modified using CSS.
|
* `loginMechanism:'Redirected'` - this approach enables authentication through a SASLogon window, supporting complex authentication flows (such as 2FA) and avoids the need to handle passwords in the application itself. The styling of the window can be modified using CSS.
|
||||||
* `LoginMechanism:'Default'` - this approach requires that the username and password are captured, and used within the `.login()` method. This can be helpful for development, or automated testing.
|
* `loginMechanism:'Default'` - this approach requires that the username and password are captured, and used within the `.login()` method. This can be helpful for development, or automated testing.
|
||||||
|
|
||||||
Sample code for logging in with the `Default` approach:
|
Sample code for logging in with the `Default` approach:
|
||||||
|
|
||||||
@@ -127,7 +125,11 @@ sasJs.request("/path/to/my/service", dataObject)
|
|||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
We supply the path to the SAS service, and a data object. The data object can be null (for services with no input), or can contain one or more tables in the following format:
|
We supply the path to the SAS service, and a data object.
|
||||||
|
|
||||||
|
If the path starts with a `/` then it should be a full path to the service. If there is no leading `/` then it is relative to the `appLoc`.
|
||||||
|
|
||||||
|
The data object can be null (for services with no input), or can contain one or more "tables" in the following format:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
let dataObject={
|
let dataObject={
|
||||||
@@ -143,7 +145,9 @@ let dataObject={
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
There are optional parameters such as a config object and a callback login function.
|
These tables (`tablewith2cols1row` and `tablewith1col2rows`) will be created in SAS WORK after running `%webout(FETCH)` in your SAS service.
|
||||||
|
|
||||||
|
The `request()` method also has optional parameters such as a config object and a callback login function.
|
||||||
|
|
||||||
The response object will contain returned tables and columns. Table names are always lowercase, and column names uppercase.
|
The response object will contain returned tables and columns. Table names are always lowercase, and column names uppercase.
|
||||||
|
|
||||||
@@ -155,7 +159,7 @@ The SAS type (char/numeric) of the values is determined according to a set of ru
|
|||||||
|
|
||||||
* If the values are numeric, the SAS type is numeric
|
* If the values are numeric, the SAS type is numeric
|
||||||
* If the values are all string, the SAS type is character
|
* If the values are all string, the SAS type is character
|
||||||
* If the values contain a single character (a-Z + underscore) AND a numeric, then the SAS type is numeric (with special missing values).
|
* If the values contain a single character (a-Z + underscore + .) AND a numeric, then the SAS type is numeric (with special missing values).
|
||||||
* `null` is set to either '.' or '' depending on the assigned or derived type per the above rules. If entire column is `null` then the type will be numeric.
|
* `null` is set to either '.' or '' depending on the assigned or derived type per the above rules. If entire column is `null` then the type will be numeric.
|
||||||
|
|
||||||
The following table illustrates the formats applied to columns under various scenarios:
|
The following table illustrates the formats applied to columns under various scenarios:
|
||||||
@@ -221,7 +225,7 @@ The SAS side is handled by a number of macros in the [macro core](https://github
|
|||||||
The following snippet shows the process of SAS tables arriving / leaving:
|
The following snippet shows the process of SAS tables arriving / leaving:
|
||||||
|
|
||||||
```sas
|
```sas
|
||||||
/* fetch all input tables sent from frontend - they arrive as work tables */
|
/* convert frontend input tables from into SASWORK datasets */
|
||||||
%webout(FETCH)
|
%webout(FETCH)
|
||||||
|
|
||||||
/* some sas code */
|
/* some sas code */
|
||||||
@@ -250,6 +254,8 @@ Where an entire column is made up of special missing numerics, there would be no
|
|||||||
%webout(OBJ,a,missing=STRING,showmeta=YES)
|
%webout(OBJ,a,missing=STRING,showmeta=YES)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The `%webout()` macro itself is just a wrapper for the [mp_jsonout](https://core.sasjs.io/mp__jsonout_8sas.html) macro.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Configuration on the client side involves passing an object on startup, which can also be passed with each request. Technical documentation on the SASjsConfig class is available [here](https://adapter.sasjs.io/classes/types.sasjsconfig.html). The main config items are:
|
Configuration on the client side involves passing an object on startup, which can also be passed with each request. Technical documentation on the SASjsConfig class is available [here](https://adapter.sasjs.io/classes/types.sasjsconfig.html). The main config items are:
|
||||||
@@ -258,7 +264,7 @@ Configuration on the client side involves passing an object on startup, which ca
|
|||||||
* `serverType` - either `SAS9`, `SASVIYA` or `SASJS`. The `SASJS` server type is for use with [sasjs/server](https://github.com/sasjs/server).
|
* `serverType` - either `SAS9`, `SASVIYA` or `SASJS`. The `SASJS` server type is for use with [sasjs/server](https://github.com/sasjs/server).
|
||||||
* `serverUrl` - the location (including http protocol and port) of the SAS Server. Can be omitted, eg if serving directly from the SAS Web Server, or in streaming mode.
|
* `serverUrl` - the location (including http protocol and port) of the SAS Server. Can be omitted, eg if serving directly from the SAS Web Server, or in streaming mode.
|
||||||
* `debug` - if `true` then SAS Logs and extra debug information is returned.
|
* `debug` - if `true` then SAS Logs and extra debug information is returned.
|
||||||
* `LoginMechanism` - either `Default` or `Redirected`. See [SAS Logon](#sas-logon) section.
|
* `loginMechanism` - either `Default` or `Redirected`. See [SAS Logon](#sas-logon) section.
|
||||||
* `useComputeApi` - Only relevant when the serverType is `SASVIYA`. If `true` the [Compute API](#using-the-compute-api) is used. If `false` the [JES API](#using-the-jes-api) is used. If `null` or `undefined` the [Web](#using-jes-web-app) approach is used.
|
* `useComputeApi` - Only relevant when the serverType is `SASVIYA`. If `true` the [Compute API](#using-the-compute-api) is used. If `false` the [JES API](#using-the-jes-api) is used. If `null` or `undefined` the [Web](#using-jes-web-app) approach is used.
|
||||||
* `contextName` - Compute context on which the requests will be called. If missing or not provided, defaults to `Job Execution Compute context`.
|
* `contextName` - Compute context on which the requests will be called. If missing or not provided, defaults to `Job Execution Compute context`.
|
||||||
* `requestHistoryLimit` - Request history limit. Increasing this limit may affect browser performance, especially with debug (logs) enabled. Default is 10.
|
* `requestHistoryLimit` - Request history limit. Increasing this limit may affect browser performance, especially with debug (logs) enabled. Default is 10.
|
||||||
@@ -314,7 +320,7 @@ For more information and examples specific to this adapter you can check out the
|
|||||||
|
|
||||||
For more information on building web apps in general, check out these [resources](https://sasjs.io/training/resources/) or contact the [author](https://www.linkedin.com/in/allanbowe/) directly.
|
For more information on building web apps in general, check out these [resources](https://sasjs.io/training/resources/) or contact the [author](https://www.linkedin.com/in/allanbowe/) directly.
|
||||||
|
|
||||||
If you are a SAS 9 or SAS Viya customer you can also request a copy of [Data Controller](https://datacontroller.io) - free for up to 5 users, this tool makes use of all parts of the SASjs framework.
|
As a SAS customer you can also request a copy of [Data Controller](https://datacontroller.io) - free for up to 5 users, this tool makes use of all parts of the SASjs framework.
|
||||||
|
|
||||||
|
|
||||||
## Star Gazing
|
## Star Gazing
|
||||||
|
|||||||
11
cypress.json
Normal file
11
cypress.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"chromeWebSecurity": false,
|
||||||
|
"defaultCommandTimeout": 20000,
|
||||||
|
"env": {
|
||||||
|
"sasjsTestsUrl": "",
|
||||||
|
"username": "",
|
||||||
|
"password": "",
|
||||||
|
"screenshotOnRunFailure": false,
|
||||||
|
"testingFinishTimeout": 600000
|
||||||
|
}
|
||||||
|
}
|
||||||
78
cypress/integration/sasjs.tests.ts
Normal file
78
cypress/integration/sasjs.tests.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
const sasjsTestsUrl = Cypress.env('sasjsTestsUrl')
|
||||||
|
const username = Cypress.env('username')
|
||||||
|
const password = Cypress.env('password')
|
||||||
|
const testingFinishTimeout = Cypress.env('testingFinishTimeout')
|
||||||
|
|
||||||
|
context('sasjs-tests', function () {
|
||||||
|
this.beforeAll(() => {
|
||||||
|
cy.visit(sasjsTestsUrl)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.beforeEach(() => {
|
||||||
|
cy.reload()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should have all tests successfull', (done) => {
|
||||||
|
cy.get('body').then(($body) => {
|
||||||
|
if ($body.find('input[placeholder="User Name"]').length > 0) {
|
||||||
|
cy.get('input[placeholder="User Name"]').type(username)
|
||||||
|
cy.get('input[placeholder="Password"]').type(password)
|
||||||
|
cy.get('.submit-button').click()
|
||||||
|
}
|
||||||
|
|
||||||
|
cy.get('input[placeholder="User Name"]', { timeout: 40000 })
|
||||||
|
.should('not.exist')
|
||||||
|
.then(() => {
|
||||||
|
cy.get('.ui.massive.icon.primary.left.labeled.button')
|
||||||
|
.click()
|
||||||
|
.then(() => {
|
||||||
|
cy.get('.ui.massive.loading.primary.button', {
|
||||||
|
timeout: testingFinishTimeout
|
||||||
|
})
|
||||||
|
.should('not.exist')
|
||||||
|
.then(() => {
|
||||||
|
cy.get('span.icon.failed')
|
||||||
|
.should('not.exist')
|
||||||
|
.then(() => {
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should have all tests successfull with debug on', (done) => {
|
||||||
|
cy.get('body').then(($body) => {
|
||||||
|
if ($body.find('input[placeholder="User Name"]').length > 0) {
|
||||||
|
cy.get('input[placeholder="User Name"]').type(username)
|
||||||
|
cy.get('input[placeholder="Password"]').type(password)
|
||||||
|
cy.get('.submit-button').click()
|
||||||
|
}
|
||||||
|
|
||||||
|
cy.get('.ui.fitted.toggle.checkbox label')
|
||||||
|
.click()
|
||||||
|
.then(() => {
|
||||||
|
cy.get('input[placeholder="User Name"]', { timeout: 40000 })
|
||||||
|
.should('not.exist')
|
||||||
|
.then(() => {
|
||||||
|
cy.get('.ui.massive.icon.primary.left.labeled.button')
|
||||||
|
.click()
|
||||||
|
.then(() => {
|
||||||
|
cy.get('.ui.massive.loading.primary.button', {
|
||||||
|
timeout: testingFinishTimeout
|
||||||
|
})
|
||||||
|
.should('not.exist')
|
||||||
|
.then(() => {
|
||||||
|
cy.get('span.icon.failed')
|
||||||
|
.should('not.exist')
|
||||||
|
.then(() => {
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
31
cypress/plugins/cy-ts-preprocessor.js
Normal file
31
cypress/plugins/cy-ts-preprocessor.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
const wp = require('@cypress/webpack-preprocessor')
|
||||||
|
|
||||||
|
const webpackOptions = {
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.ts', '.js']
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.ts$/,
|
||||||
|
loaders: ['ts-loader'],
|
||||||
|
exclude: [/node_modules/]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(html|css)$/,
|
||||||
|
loader: 'raw-loader',
|
||||||
|
exclude: /\.async\.(html|css)$/
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.async\.(html|css)$/,
|
||||||
|
loaders: ['file?name=[name].[hash].[ext]', 'extract']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
webpackOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = wp(options)
|
||||||
42
cypress/plugins/index.js
Normal file
42
cypress/plugins/index.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
// ***********************************************************
|
||||||
|
// This example plugins/index.js can be used to load plugins
|
||||||
|
//
|
||||||
|
// You can change the location of this file or turn off loading
|
||||||
|
// the plugins file with the 'pluginsFile' configuration option.
|
||||||
|
//
|
||||||
|
// You can read more here:
|
||||||
|
// https://on.cypress.io/plugins-guide
|
||||||
|
// ***********************************************************
|
||||||
|
|
||||||
|
// This function is called when a project is opened or re-opened (e.g. due to
|
||||||
|
// the project's config changing)
|
||||||
|
|
||||||
|
const wp = require('@cypress/webpack-preprocessor')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Cypress.PluginConfig}
|
||||||
|
*/
|
||||||
|
module.exports = (on, config) => {
|
||||||
|
// `on` is used to hook into various events Cypress emits
|
||||||
|
// `config` is the resolved Cypress config
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
webpackOptions: require('../webpack.config.js')
|
||||||
|
}
|
||||||
|
on('file:preprocessor', wp(options))
|
||||||
|
|
||||||
|
on('before:browser:launch', (browser = {}, launchOptions) => {
|
||||||
|
if (browser.name === 'chrome') {
|
||||||
|
launchOptions.args.push('--disable-site-isolation-trials')
|
||||||
|
launchOptions.args.push('--auto-open-devtools-for-tabs')
|
||||||
|
launchOptions.args.push('--aggressive-cache-discard')
|
||||||
|
launchOptions.args.push('--disable-cache')
|
||||||
|
launchOptions.args.push('--disable-application-cache')
|
||||||
|
launchOptions.args.push('--disable-offline-load-stale-cache')
|
||||||
|
launchOptions.args.push('--disk-cache-size=0')
|
||||||
|
|
||||||
|
return launchOptions
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
10
cypress/tsconfig.json
Normal file
10
cypress/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true,
|
||||||
|
"baseUrl": "../node_modules",
|
||||||
|
"target": "es6",
|
||||||
|
"lib": ["es2019", "dom"],
|
||||||
|
"types": ["cypress"]
|
||||||
|
},
|
||||||
|
"include": ["**/*.ts"]
|
||||||
|
}
|
||||||
23
cypress/webpack.config.js
Normal file
23
cypress/webpack.config.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
module.exports = {
|
||||||
|
mode: 'development',
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.ts', '.js']
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.ts$/,
|
||||||
|
exclude: [/node_modules/],
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: 'ts-loader',
|
||||||
|
options: {
|
||||||
|
// skip typechecking for speed
|
||||||
|
transpileOnly: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
16971
package-lock.json
generated
16971
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
16
package.json
16
package.json
@@ -8,14 +8,17 @@
|
|||||||
"build": "rimraf build && rimraf node && mkdir node && copyfiles -u 1 \"./src/**/*\" ./node && webpack && rimraf build/src && rimraf node",
|
"build": "rimraf build && rimraf node && mkdir node && copyfiles -u 1 \"./src/**/*\" ./node && webpack && rimraf build/src && rimraf node",
|
||||||
"package:lib": "npm run build && copyfiles ./package.json ./checkNodeVersion.js build && cd build && npm version \"5.0.0\" && npm pack",
|
"package:lib": "npm run build && copyfiles ./package.json ./checkNodeVersion.js build && cd build && npm version \"5.0.0\" && npm pack",
|
||||||
"publish:lib": "npm run build && cd build && npm publish",
|
"publish:lib": "npm run build && cd build && npm publish",
|
||||||
"lint:fix": "npx prettier --write \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --write \"sasjs-tests/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\"",
|
"lint:fix": "npx prettier --loglevel silent --write \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --loglevel silent --write \"sasjs-tests/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --loglevel silent --write \"cypress/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\"",
|
||||||
"lint": "npx prettier --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --check \"sasjs-tests/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\"",
|
"lint": "npx prettier --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --check \"sasjs-tests/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --check \"cypress/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\"",
|
||||||
|
"lint:silent": "npx prettier --loglevel silent --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --loglevel silent --check \"sasjs-tests/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --loglevel silent --check \"cypress/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\"",
|
||||||
"test": "jest --silent --coverage",
|
"test": "jest --silent --coverage",
|
||||||
"prepublishOnly": "cp -r ./build/* . && rm -rf ./build",
|
"prepublishOnly": "cp -r ./build/* . && rm -rf ./build",
|
||||||
"postpublish": "git clean -fd",
|
"postpublish": "git clean -fd",
|
||||||
"semantic-release": "semantic-release",
|
"semantic-release": "semantic-release",
|
||||||
"typedoc": "node createTSDocs",
|
"typedoc": "node createTSDocs",
|
||||||
"prepare": "git rev-parse --git-dir && git config core.hooksPath ./.git-hooks && git config core.autocrlf false || true"
|
"prepare": "git rev-parse --git-dir && git config core.hooksPath ./.git-hooks && git config core.autocrlf false || true",
|
||||||
|
"cypress": "cypress open",
|
||||||
|
"cy:run": "cypress run"
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
@@ -40,6 +43,7 @@
|
|||||||
},
|
},
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@cypress/webpack-preprocessor": "5.9.1",
|
||||||
"@types/axios": "0.14.0",
|
"@types/axios": "0.14.0",
|
||||||
"@types/express": "4.17.13",
|
"@types/express": "4.17.13",
|
||||||
"@types/form-data": "2.5.0",
|
"@types/form-data": "2.5.0",
|
||||||
@@ -49,6 +53,7 @@
|
|||||||
"@types/tough-cookie": "4.0.1",
|
"@types/tough-cookie": "4.0.1",
|
||||||
"copyfiles": "2.4.1",
|
"copyfiles": "2.4.1",
|
||||||
"cp": "0.2.0",
|
"cp": "0.2.0",
|
||||||
|
"cypress": "7.7.0",
|
||||||
"dotenv": "16.0.0",
|
"dotenv": "16.0.0",
|
||||||
"express": "4.17.3",
|
"express": "4.17.3",
|
||||||
"jest": "27.4.7",
|
"jest": "27.4.7",
|
||||||
@@ -56,15 +61,18 @@
|
|||||||
"node-polyfill-webpack-plugin": "1.1.4",
|
"node-polyfill-webpack-plugin": "1.1.4",
|
||||||
"path": "0.12.7",
|
"path": "0.12.7",
|
||||||
"pem": "1.14.6",
|
"pem": "1.14.6",
|
||||||
|
"prettier": "2.7.1",
|
||||||
"process": "0.11.10",
|
"process": "0.11.10",
|
||||||
"rimraf": "3.0.2",
|
"rimraf": "3.0.2",
|
||||||
"semantic-release": "18.0.0",
|
"semantic-release": "19.0.3",
|
||||||
"terser-webpack-plugin": "5.3.1",
|
"terser-webpack-plugin": "5.3.1",
|
||||||
"ts-jest": "27.1.3",
|
"ts-jest": "27.1.3",
|
||||||
"ts-loader": "9.2.6",
|
"ts-loader": "9.2.6",
|
||||||
"tslint": "6.1.3",
|
"tslint": "6.1.3",
|
||||||
"tslint-config-prettier": "1.18.0",
|
"tslint-config-prettier": "1.18.0",
|
||||||
"typedoc": "0.22.11",
|
"typedoc": "0.22.11",
|
||||||
|
"typedoc-neo-theme": "1.1.1",
|
||||||
|
"typedoc-plugin-external-module-name": "4.0.6",
|
||||||
"typedoc-plugin-rename-defaults": "0.4.0",
|
"typedoc-plugin-rename-defaults": "0.4.0",
|
||||||
"typescript": "4.5.5",
|
"typescript": "4.5.5",
|
||||||
"webpack": "5.69.0",
|
"webpack": "5.69.0",
|
||||||
|
|||||||
10
sasjs-cypress-run.sh
Executable file
10
sasjs-cypress-run.sh
Executable file
@@ -0,0 +1,10 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if npm run cy:run -- --spec "cypress/integration/sasjs.tests.ts" ; then
|
||||||
|
echo "Cypress sasjs testing passed!"
|
||||||
|
else
|
||||||
|
curl -X POST --header "Content-Type:application/json" --data '{"username":"GitHub CI - Adapter SASJS-TESTS (FAIL)", "content":"Automated sasjs-tests failed on the @sasjs/adapter PR on following link.\n'$2'", "avatar_url":"https://i.ibb.co/Lpk7Xvq/error-outline.png"}' $1
|
||||||
|
|
||||||
|
echo "Cypress sasjs testing failed!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -60,7 +60,7 @@ If you'd like to deploy just `sasjs-tests` without changing the adapter version,
|
|||||||
|
|
||||||
The below services need to be created on your SAS server, at the location specified as the `appLoc` in the SASjs configuration.
|
The below services need to be created on your SAS server, at the location specified as the `appLoc` in the SASjs configuration.
|
||||||
|
|
||||||
### SAS 9
|
The code below will work on ALL SAS platforms (Viya, SAS 9 EBI, SASjs Server).
|
||||||
|
|
||||||
```sas
|
```sas
|
||||||
filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
|
filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
|
||||||
@@ -70,76 +70,32 @@ parmcards4;
|
|||||||
%webout(FETCH)
|
%webout(FETCH)
|
||||||
%webout(OPEN)
|
%webout(OPEN)
|
||||||
%macro x();
|
%macro x();
|
||||||
%do i=1 %to &_webin_file_count; %webout(OBJ,&&_webin_name&i,missing=STRING,showmeta=YES) %end;
|
%if %symexist(sasjs_tables) %then %do i=1 %to %sysfunc(countw(&sasjs_tables));
|
||||||
%mend; %x()
|
|
||||||
%webout(CLOSE)
|
|
||||||
;;;;
|
|
||||||
%mm_createwebservice(path=/Public/app/common,name=sendObj)
|
|
||||||
parmcards4;
|
|
||||||
%webout(FETCH)
|
|
||||||
%webout(OPEN)
|
|
||||||
%macro x();
|
|
||||||
%do i=1 %to &_webin_file_count; %webout(ARR,&&_webin_name&i,missing=STRING,showmeta=YES) %end;
|
|
||||||
%mend; %x()
|
|
||||||
%webout(CLOSE)
|
|
||||||
;;;;
|
|
||||||
%mm_createwebservice(path=/Public/app/common,name=sendArr)
|
|
||||||
parmcards4;
|
|
||||||
data work.macvars;
|
|
||||||
set sashelp.vmacro;
|
|
||||||
run;
|
|
||||||
%webout(OPEN)
|
|
||||||
%webout(OBJ,macvars)
|
|
||||||
%webout(CLOSE)
|
|
||||||
;;;;
|
|
||||||
%mm_createwebservice(path=/Public/app/common,name=sendMacVars)
|
|
||||||
parmcards4;
|
|
||||||
let he who hath understanding, reckon the number of the beast
|
|
||||||
;;;;
|
|
||||||
%mm_createwebservice(path=/Public/app/common,name=makeErr)
|
|
||||||
parmcards4;
|
|
||||||
%webout(OPEN)
|
|
||||||
data _null_;
|
|
||||||
file _webout;
|
|
||||||
put ' the discovery channel ';
|
|
||||||
run;
|
|
||||||
%webout(CLOSE)
|
|
||||||
;;;;
|
|
||||||
%mm_createwebservice(path=/Public/app/common,name=invalidJSON)
|
|
||||||
```
|
|
||||||
|
|
||||||
### SAS Viya
|
|
||||||
|
|
||||||
```sas
|
|
||||||
filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
|
|
||||||
%inc mc;
|
|
||||||
filename ft15f001 temp lrecl=1000;
|
|
||||||
parmcards4;
|
|
||||||
%webout(FETCH)
|
|
||||||
%webout(OPEN)
|
|
||||||
%macro x();
|
|
||||||
%do i=1 %to %sysfunc(countw(&sasjs_tables));
|
|
||||||
%let table=%scan(&sasjs_tables,&i);
|
%let table=%scan(&sasjs_tables,&i);
|
||||||
%webout(OBJ,&table,missing=STRING,showmeta=YES)
|
%webout(OBJ,&table,missing=STRING,showmeta=YES)
|
||||||
%end;
|
%end;
|
||||||
%mend;
|
%else %do i=1 %to &_webin_file_count;
|
||||||
%x()
|
%webout(OBJ,&&_webin_name&i,missing=STRING,showmeta=YES)
|
||||||
|
%end;
|
||||||
|
%mend; %x()
|
||||||
%webout(CLOSE)
|
%webout(CLOSE)
|
||||||
;;;;
|
;;;;
|
||||||
%mp_createwebservice(path=/Public/app/common,name=sendObj)
|
%mx_createwebservice(path=/Public/app/common,name=sendObj)
|
||||||
parmcards4;
|
parmcards4;
|
||||||
%webout(FETCH)
|
%webout(FETCH)
|
||||||
%webout(OPEN)
|
%webout(OPEN)
|
||||||
%macro x();
|
%macro x();
|
||||||
%do i=1 %to %sysfunc(countw(&sasjs_tables));
|
%if %symexist(sasjs_tables) %then %do i=1 %to %sysfunc(countw(&sasjs_tables));
|
||||||
%let table=%scan(&sasjs_tables,&i);
|
%let table=%scan(&sasjs_tables,&i);
|
||||||
%webout(ARR,&table,missing=STRING,showmeta=YES)
|
%webout(ARR,&table,missing=STRING,showmeta=YES)
|
||||||
%end;
|
%end;
|
||||||
%mend;
|
%else %do i=1 %to &_webin_file_count;
|
||||||
%x()
|
%webout(ARR,&&_webin_name&i,missing=STRING,showmeta=YES)
|
||||||
|
%end;
|
||||||
|
%mend; %x()
|
||||||
%webout(CLOSE)
|
%webout(CLOSE)
|
||||||
;;;;
|
;;;;
|
||||||
%mp_createwebservice(path=/Public/app/common,name=sendArr)
|
%mx_createwebservice(path=/Public/app/common,name=sendArr)
|
||||||
parmcards4;
|
parmcards4;
|
||||||
data work.macvars;
|
data work.macvars;
|
||||||
set sashelp.vmacro;
|
set sashelp.vmacro;
|
||||||
@@ -148,14 +104,14 @@ parmcards4;
|
|||||||
%webout(OBJ,macvars)
|
%webout(OBJ,macvars)
|
||||||
%webout(CLOSE)
|
%webout(CLOSE)
|
||||||
;;;;
|
;;;;
|
||||||
%mp_createwebservice(path=/Public/app/common,name=sendMacVars)
|
%mx_createwebservice(path=/Public/app/common,name=sendMacVars)
|
||||||
parmcards4;
|
parmcards4;
|
||||||
If you can keep your head when all about you
|
If you can keep your head when all about you
|
||||||
Are losing theirs and blaming it on you,
|
Are losing theirs and blaming it on you,
|
||||||
If you can trust yourself when all men doubt you,
|
If you can trust yourself when all men doubt you,
|
||||||
But make allowance for their doubting too;
|
But make allowance for their doubting too;
|
||||||
;;;;
|
;;;;
|
||||||
%mp_createwebservice(path=/Public/app/common,name=makeErr)
|
%mx_createwebservice(path=/Public/app/common,name=makeErr)
|
||||||
parmcards4;
|
parmcards4;
|
||||||
%webout(OPEN)
|
%webout(OPEN)
|
||||||
data _null_;
|
data _null_;
|
||||||
@@ -164,7 +120,7 @@ data _null_;
|
|||||||
run;
|
run;
|
||||||
%webout(CLOSE)
|
%webout(CLOSE)
|
||||||
;;;;
|
;;;;
|
||||||
%mp_createwebservice(path=/Public/app/common,name=invalidJSON)
|
%mx_createwebservice(path=/Public/app/common,name=invalidJSON)
|
||||||
```
|
```
|
||||||
|
|
||||||
You should now be able to access the tests in your browser at the deployed path on your server.
|
You should now be able to access the tests in your browser at the deployed path on your server.
|
||||||
|
|||||||
31403
sasjs-tests/package-lock.json
generated
31403
sasjs-tests/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sasjs/adapter": "file:../build/sasjs-adapter-5.0.0.tgz",
|
"@sasjs/adapter": "file:../build/sasjs-adapter-5.0.0.tgz",
|
||||||
"@sasjs/test-framework": "^1.4.3",
|
"@sasjs/test-framework": "^1.5.6",
|
||||||
"@types/jest": "^26.0.20",
|
"@types/jest": "^26.0.20",
|
||||||
"@types/node": "^14.14.41",
|
"@types/node": "^14.14.41",
|
||||||
"@types/react": "^17.0.1",
|
"@types/react": "^17.0.1",
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.1",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-scripts": "^4.0.2",
|
"react-scripts": "^5.0.1",
|
||||||
"typescript": "^4.1.3"
|
"typescript": "^4.1.3"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -43,6 +43,6 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"node-sass": "^6.0.1"
|
"node-sass": "^7.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,12 +11,13 @@ import { fileUploadTests } from './testSuites/FileUpload'
|
|||||||
const App = (): ReactElement<{}> => {
|
const App = (): ReactElement<{}> => {
|
||||||
const { adapter, config } = useContext(AppContext)
|
const { adapter, config } = useContext(AppContext)
|
||||||
const [testSuites, setTestSuites] = useState<TestSuite[]>([])
|
const [testSuites, setTestSuites] = useState<TestSuite[]>([])
|
||||||
|
const appLoc = config.sasJsConfig.appLoc
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (adapter) {
|
if (adapter) {
|
||||||
const testSuites = [
|
const testSuites = [
|
||||||
basicTests(adapter, config.userName, config.password),
|
basicTests(adapter, config.userName, config.password),
|
||||||
sendArrTests(adapter),
|
sendArrTests(adapter, appLoc),
|
||||||
sendObjTests(adapter),
|
sendObjTests(adapter),
|
||||||
specialCaseTests(adapter),
|
specialCaseTests(adapter),
|
||||||
sasjsRequestTests(adapter),
|
sasjsRequestTests(adapter),
|
||||||
@@ -24,12 +25,12 @@ const App = (): ReactElement<{}> => {
|
|||||||
]
|
]
|
||||||
|
|
||||||
if (adapter.getSasjsConfig().serverType === 'SASVIYA') {
|
if (adapter.getSasjsConfig().serverType === 'SASVIYA') {
|
||||||
testSuites.push(computeTests(adapter))
|
testSuites.push(computeTests(adapter, appLoc))
|
||||||
}
|
}
|
||||||
|
|
||||||
setTestSuites(testSuites)
|
setTestSuites(testSuites)
|
||||||
}
|
}
|
||||||
}, [adapter, config])
|
}, [adapter, config, appLoc])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app">
|
<div className="app">
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const Login = (): ReactElement<{}> => {
|
|||||||
const appContext = useContext(AppContext)
|
const appContext = useContext(AppContext)
|
||||||
|
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
(e) => {
|
(e: any) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
appContext.adapter.logIn(username, password).then((res) => {
|
appContext.adapter.logIn(username, password).then((res) => {
|
||||||
appContext.setIsLoggedIn(res.isLoggedIn)
|
appContext.setIsLoggedIn(res.isLoggedIn)
|
||||||
@@ -28,7 +28,7 @@ const Login = (): ReactElement<{}> => {
|
|||||||
placeholder="User Name"
|
placeholder="User Name"
|
||||||
value={username}
|
value={username}
|
||||||
required
|
required
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
onChange={(e: any) => setUsername(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
@@ -38,7 +38,7 @@ const Login = (): ReactElement<{}> => {
|
|||||||
type="password"
|
type="password"
|
||||||
value={password}
|
value={password}
|
||||||
required
|
required
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e: any) => setPassword(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" className="submit-button">
|
<button type="submit" className="submit-button">
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export const basicTests = (
|
|||||||
'Should fail on first attempt and should log the user in on second attempt',
|
'Should fail on first attempt and should log the user in on second attempt',
|
||||||
test: async () => {
|
test: async () => {
|
||||||
await adapter.logOut()
|
await adapter.logOut()
|
||||||
await adapter.logIn('invalid', 'invalid')
|
await adapter.logIn('invalid', 'invalid').catch((err: any) => {})
|
||||||
return await adapter.logIn(userName, password)
|
return await adapter.logIn(userName, password)
|
||||||
},
|
},
|
||||||
assertion: (response: any) =>
|
assertion: (response: any) =>
|
||||||
@@ -161,26 +161,17 @@ export const basicTests = (
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Request with extra attributes on JES approach',
|
title: 'Web request',
|
||||||
description:
|
description: 'Should run the request with old web approach',
|
||||||
'Should complete successful request with extra attributes present in response',
|
|
||||||
test: async () => {
|
test: async () => {
|
||||||
const config: Partial<SASjsConfig> = {
|
const config: Partial<SASjsConfig> = {
|
||||||
useComputeApi: false
|
useComputeApi: false
|
||||||
}
|
}
|
||||||
|
|
||||||
return await adapter.request(
|
return await adapter.request('common/sendArr', stringData, config)
|
||||||
'common/sendArr',
|
|
||||||
stringData,
|
|
||||||
config,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
['file', 'data']
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
assertion: (response: any) => {
|
assertion: (response: any) => {
|
||||||
const responseKeys: any = Object.keys(response)
|
return response.table1[0][0] === stringData.table1[0].col1
|
||||||
return responseKeys.includes('file') && responseKeys.includes('data')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,15 +1,41 @@
|
|||||||
import SASjs from '@sasjs/adapter'
|
import SASjs from '@sasjs/adapter'
|
||||||
import { TestSuite } from '@sasjs/test-framework'
|
import { TestSuite } from '@sasjs/test-framework'
|
||||||
|
|
||||||
export const computeTests = (adapter: SASjs): TestSuite => ({
|
const stringData: any = { table1: [{ col1: 'first col value' }] }
|
||||||
|
|
||||||
|
export const computeTests = (adapter: SASjs, appLoc: string): TestSuite => ({
|
||||||
name: 'Compute',
|
name: 'Compute',
|
||||||
tests: [
|
tests: [
|
||||||
|
{
|
||||||
|
title: 'Compute API request',
|
||||||
|
description: 'Should run the request with compute API approach',
|
||||||
|
test: async () => {
|
||||||
|
return await adapter.request('common/sendArr', stringData)
|
||||||
|
},
|
||||||
|
assertion: (response: any) => {
|
||||||
|
return response.table1[0][0] === stringData.table1[0].col1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'JES API request',
|
||||||
|
description: 'Should run the request with JES API approach',
|
||||||
|
test: async () => {
|
||||||
|
const config = {
|
||||||
|
useComputeApi: false
|
||||||
|
}
|
||||||
|
|
||||||
|
return await adapter.request('common/sendArr', stringData, config)
|
||||||
|
},
|
||||||
|
assertion: (response: any) => {
|
||||||
|
return response.table1[0][0] === stringData.table1[0].col1
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'Start Compute Job - not waiting for result',
|
title: 'Start Compute Job - not waiting for result',
|
||||||
description: 'Should start a compute job and return the session',
|
description: 'Should start a compute job and return the session',
|
||||||
test: () => {
|
test: () => {
|
||||||
const data: any = { table1: [{ col1: 'first col value' }] }
|
const data: any = { table1: [{ col1: 'first col value' }] }
|
||||||
return adapter.startComputeJob('/Public/app/common/sendArr', data)
|
return adapter.startComputeJob(`${appLoc}/common/sendArr`, data)
|
||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => {
|
||||||
const expectedProperties = ['id', 'applicationName', 'attributes']
|
const expectedProperties = ['id', 'applicationName', 'attributes']
|
||||||
|
|||||||
@@ -45,14 +45,14 @@ const getLargeObjectData = () => {
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
export const sendArrTests = (adapter: SASjs): TestSuite => ({
|
export const sendArrTests = (adapter: SASjs, appLoc: string): TestSuite => ({
|
||||||
name: 'sendArr',
|
name: 'sendArr',
|
||||||
tests: [
|
tests: [
|
||||||
{
|
{
|
||||||
title: 'Absolute paths',
|
title: 'Absolute paths',
|
||||||
description: 'Should work with absolute paths to SAS jobs',
|
description: 'Should work with absolute paths to SAS jobs',
|
||||||
test: () => {
|
test: () => {
|
||||||
return adapter.request('/Public/app/common/sendArr', stringData)
|
return adapter.request(`${appLoc}/common/sendArr`, stringData)
|
||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => {
|
||||||
return res.table1[0][0] === stringData.table1[0].col1
|
return res.table1[0][0] === stringData.table1[0].col1
|
||||||
@@ -86,7 +86,7 @@ export const sendArrTests = (adapter: SASjs): TestSuite => ({
|
|||||||
'Should error out with long string values over 32765 characters',
|
'Should error out with long string values over 32765 characters',
|
||||||
test: () => {
|
test: () => {
|
||||||
const data = getLongStringData(32767)
|
const data = getLongStringData(32767)
|
||||||
return adapter.request('common/sendArr', data).catch((e) => e)
|
return adapter.request('common/sendArr', data).catch((e: any) => e)
|
||||||
},
|
},
|
||||||
assertion: (error: any) => {
|
assertion: (error: any) => {
|
||||||
return !!error && !!error.error && !!error.error.message
|
return !!error && !!error.error && !!error.error.message
|
||||||
@@ -138,7 +138,7 @@ export const sendArrTests = (adapter: SASjs): TestSuite => ({
|
|||||||
result =
|
result =
|
||||||
result &&
|
result &&
|
||||||
res.table1[index][3] ===
|
res.table1[index][3] ===
|
||||||
(multipleRowsWithNulls.table1[index].col4 || ' ')
|
(multipleRowsWithNulls.table1[index].col4 || '')
|
||||||
})
|
})
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -164,7 +164,7 @@ export const sendArrTests = (adapter: SASjs): TestSuite => ({
|
|||||||
result =
|
result =
|
||||||
result &&
|
result &&
|
||||||
res.table1[index][3] ===
|
res.table1[index][3] ===
|
||||||
(multipleColumnsWithNulls.table1[index].col4 || ' ')
|
(multipleColumnsWithNulls.table1[index].col4 || '')
|
||||||
})
|
})
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -182,7 +182,9 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
|||||||
const invalidData: any = {
|
const invalidData: any = {
|
||||||
'1InvalidTable': [{ col1: 42 }]
|
'1InvalidTable': [{ col1: 42 }]
|
||||||
}
|
}
|
||||||
return adapter.request('common/sendObj', invalidData).catch((e) => e)
|
return adapter
|
||||||
|
.request('common/sendObj', invalidData)
|
||||||
|
.catch((e: any) => e)
|
||||||
},
|
},
|
||||||
assertion: (error: any) =>
|
assertion: (error: any) =>
|
||||||
!!error && !!error.error && !!error.error.message
|
!!error && !!error.error && !!error.error.message
|
||||||
@@ -194,7 +196,9 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
|||||||
const invalidData: any = {
|
const invalidData: any = {
|
||||||
'an invalidTable': [{ col1: 42 }]
|
'an invalidTable': [{ col1: 42 }]
|
||||||
}
|
}
|
||||||
return adapter.request('common/sendObj', invalidData).catch((e) => e)
|
return adapter
|
||||||
|
.request('common/sendObj', invalidData)
|
||||||
|
.catch((e: any) => e)
|
||||||
},
|
},
|
||||||
assertion: (error: any) =>
|
assertion: (error: any) =>
|
||||||
!!error && !!error.error && !!error.error.message
|
!!error && !!error.error && !!error.error.message
|
||||||
@@ -206,7 +210,9 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
|||||||
const invalidData: any = {
|
const invalidData: any = {
|
||||||
'anInvalidTable#': [{ col1: 42 }]
|
'anInvalidTable#': [{ col1: 42 }]
|
||||||
}
|
}
|
||||||
return adapter.request('common/sendObj', invalidData).catch((e) => e)
|
return adapter
|
||||||
|
.request('common/sendObj', invalidData)
|
||||||
|
.catch((e: any) => e)
|
||||||
},
|
},
|
||||||
assertion: (error: any) =>
|
assertion: (error: any) =>
|
||||||
!!error && !!error.error && !!error.error.message
|
!!error && !!error.error && !!error.error.message
|
||||||
@@ -219,7 +225,9 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
|||||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx: [{ col1: 42 }]
|
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx: [{ col1: 42 }]
|
||||||
}
|
}
|
||||||
|
|
||||||
return adapter.request('common/sendObj', invalidData).catch((e) => e)
|
return adapter
|
||||||
|
.request('common/sendObj', invalidData)
|
||||||
|
.catch((e: any) => e)
|
||||||
},
|
},
|
||||||
assertion: (error: any) =>
|
assertion: (error: any) =>
|
||||||
!!error && !!error.error && !!error.error.message
|
!!error && !!error.error && !!error.error.message
|
||||||
@@ -231,7 +239,9 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
|||||||
const invalidData: any = {
|
const invalidData: any = {
|
||||||
inData: [[{ data: 'value' }]]
|
inData: [[{ data: 'value' }]]
|
||||||
}
|
}
|
||||||
return adapter.request('common/sendObj', invalidData).catch((e) => e)
|
return adapter
|
||||||
|
.request('common/sendObj', invalidData)
|
||||||
|
.catch((e: any) => e)
|
||||||
},
|
},
|
||||||
assertion: (error: any) =>
|
assertion: (error: any) =>
|
||||||
!!error && !!error.error && !!error.error.message
|
!!error && !!error.error && !!error.error.message
|
||||||
@@ -265,7 +275,7 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
|||||||
test: () => {
|
test: () => {
|
||||||
return adapter
|
return adapter
|
||||||
.request('common/sendObj', getLongStringData(32767))
|
.request('common/sendObj', getLongStringData(32767))
|
||||||
.catch((e) => e)
|
.catch((e: any) => e)
|
||||||
},
|
},
|
||||||
assertion: (error: any) => {
|
assertion: (error: any) => {
|
||||||
return !!error && !!error.error && !!error.error.message
|
return !!error && !!error.error && !!error.error.message
|
||||||
@@ -329,7 +339,7 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
|||||||
result =
|
result =
|
||||||
result &&
|
result &&
|
||||||
res.table1[index].COL4 ===
|
res.table1[index].COL4 ===
|
||||||
(multipleRowsWithNulls.table1[index].col4 || ' ')
|
(multipleRowsWithNulls.table1[index].col4 || '')
|
||||||
})
|
})
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -358,7 +368,7 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
|||||||
result =
|
result =
|
||||||
result &&
|
result &&
|
||||||
res.table1[index].COL4 ===
|
res.table1[index].COL4 ===
|
||||||
(multipleColumnsWithNulls.table1[index].col4 || ' ')
|
(multipleColumnsWithNulls.table1[index].col4 || '')
|
||||||
})
|
})
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ const moreSpecialCharData: any = {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const stringData: any = { table1: [{ col1: 'first col value' }] }
|
||||||
|
|
||||||
const getWideData = () => {
|
const getWideData = () => {
|
||||||
const cols: any = {}
|
const cols: any = {}
|
||||||
for (let i = 1; i <= 10000; i++) {
|
for (let i = 1; i <= 10000; i++) {
|
||||||
@@ -269,6 +271,34 @@ export const specialCaseTests = (adapter: SASjs): TestSuite => ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Request with extra attributes on JES approach',
|
||||||
|
description:
|
||||||
|
'Should complete successful request with extra attributes present in response',
|
||||||
|
test: async () => {
|
||||||
|
if (adapter.getSasjsConfig().serverType !== 'SASVIYA')
|
||||||
|
return Promise.resolve('skip')
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
useComputeApi: false
|
||||||
|
}
|
||||||
|
|
||||||
|
return await adapter.request(
|
||||||
|
'common/sendArr',
|
||||||
|
stringData,
|
||||||
|
config,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
['file', 'data']
|
||||||
|
)
|
||||||
|
},
|
||||||
|
assertion: (response: any) => {
|
||||||
|
if (response === 'skip') return true
|
||||||
|
|
||||||
|
const responseKeys: any = Object.keys(response)
|
||||||
|
return responseKeys.includes('file') && responseKeys.includes('data')
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'Special missing values',
|
title: 'Special missing values',
|
||||||
description: 'Should support special missing values',
|
description: 'Should support special missing values',
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export const assert = (
|
|||||||
} else {
|
} else {
|
||||||
result = expression()
|
result = expression()
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
console.error(message)
|
console.error(message)
|
||||||
throw new Error(message)
|
throw new Error(message)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ describe('SASViyaApiClient', () => {
|
|||||||
.mockImplementation(() => Promise.reject('Not Found'))
|
.mockImplementation(() => Promise.reject('Not Found'))
|
||||||
const error = await sasViyaApiClient
|
const error = await sasViyaApiClient
|
||||||
.createFolder('test', '/foo')
|
.createFolder('test', '/foo')
|
||||||
.catch((e) => e)
|
.catch((e: any) => e)
|
||||||
expect(error).toBeInstanceOf(RootFolderNotFoundError)
|
expect(error).toBeInstanceOf(RootFolderNotFoundError)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
81
src/SASjs.ts
81
src/SASjs.ts
@@ -1,4 +1,4 @@
|
|||||||
import { compareTimestamps, asyncForEach } from './utils'
|
import { compareTimestamps, asyncForEach, validateInput } from './utils'
|
||||||
import {
|
import {
|
||||||
SASjsConfig,
|
SASjsConfig,
|
||||||
UploadFile,
|
UploadFile,
|
||||||
@@ -592,15 +592,6 @@ export default class SASjs {
|
|||||||
'A username and password are required when using the default login mechanism.'
|
'A username and password are required when using the default login mechanism.'
|
||||||
)
|
)
|
||||||
|
|
||||||
if (this.sasjsConfig.serverType === ServerType.Sasjs) {
|
|
||||||
if (!clientId)
|
|
||||||
throw new Error(
|
|
||||||
'A username, password and clientId are required when using the default login mechanism with server type SASJS.'
|
|
||||||
)
|
|
||||||
|
|
||||||
return this.authManager!.logInSasjs(username, password, clientId)
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.authManager!.logIn(username, password)
|
return this.authManager!.logIn(username, password)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -695,7 +686,7 @@ export default class SASjs {
|
|||||||
...config
|
...config
|
||||||
}
|
}
|
||||||
|
|
||||||
const validationResult = this.validateInput(data)
|
const validationResult = validateInput(data)
|
||||||
|
|
||||||
// status is true if the data passes validation checks above
|
// status is true if the data passes validation checks above
|
||||||
if (validationResult.status) {
|
if (validationResult.status) {
|
||||||
@@ -757,74 +748,6 @@ export default class SASjs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* This function validates the input data structure and table naming convention
|
|
||||||
*
|
|
||||||
* @param data A json object that contains one or more tables, it can also be null
|
|
||||||
* @returns An object which contains two attributes: 1) status: boolean, 2) msg: string
|
|
||||||
*/
|
|
||||||
private validateInput(data: { [key: string]: any } | null): {
|
|
||||||
status: boolean
|
|
||||||
msg: string
|
|
||||||
} {
|
|
||||||
if (data === null) return { status: true, msg: '' }
|
|
||||||
|
|
||||||
const isSasFormatsTable = (key: string) =>
|
|
||||||
key.match(/^\$.*/) && Object.keys(data).includes(key.replace(/^\$/, ''))
|
|
||||||
|
|
||||||
for (const key in data) {
|
|
||||||
if (!key.match(/^[a-zA-Z_]/) && !isSasFormatsTable(key)) {
|
|
||||||
return {
|
|
||||||
status: false,
|
|
||||||
msg: 'First letter of table should be alphabet or underscore.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!key.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/) && !isSasFormatsTable(key)) {
|
|
||||||
return { status: false, msg: 'Table name should be alphanumeric.' }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key.length > 32) {
|
|
||||||
return {
|
|
||||||
status: false,
|
|
||||||
msg: 'Maximum length for table name could be 32 characters.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.getType(data[key]) !== 'Array' && !isSasFormatsTable(key)) {
|
|
||||||
return {
|
|
||||||
status: false,
|
|
||||||
msg: 'Parameter data contains invalid table structure.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < data[key].length; i++) {
|
|
||||||
if (this.getType(data[key][i]) !== 'object') {
|
|
||||||
return {
|
|
||||||
status: false,
|
|
||||||
msg: `Table ${key} contains invalid structure.`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { status: true, msg: '' }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* this function returns the type of variable
|
|
||||||
*
|
|
||||||
* @param data it could be anything, like string, array, object etc.
|
|
||||||
* @returns a string which tells the type of input parameter
|
|
||||||
*/
|
|
||||||
private getType(data: any): string {
|
|
||||||
if (Array.isArray(data)) {
|
|
||||||
return 'Array'
|
|
||||||
} else {
|
|
||||||
return typeof data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the folders and services at the given location `appLoc` on the given server `serverUrl`.
|
* Creates the folders and services at the given location `appLoc` on the given server `serverUrl`.
|
||||||
* @param serviceJson - the JSON specifying the folders and services to be created.
|
* @param serviceJson - the JSON specifying the folders and services to be created.
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { ExecutionQuery } from './types'
|
|||||||
import { RequestClient } from './request/RequestClient'
|
import { RequestClient } from './request/RequestClient'
|
||||||
import { getAccessTokenForSasjs } from './auth/getAccessTokenForSasjs'
|
import { getAccessTokenForSasjs } from './auth/getAccessTokenForSasjs'
|
||||||
import { refreshTokensForSasjs } from './auth/refreshTokensForSasjs'
|
import { refreshTokensForSasjs } from './auth/refreshTokensForSasjs'
|
||||||
import { getAuthCodeForSasjs } from './auth/getAuthCodeForSasjs'
|
|
||||||
import { parseWeboutResponse } from './utils'
|
import { parseWeboutResponse } from './utils'
|
||||||
import { getTokens } from './auth/getTokens'
|
import { getTokens } from './auth/getTokens'
|
||||||
|
|
||||||
@@ -114,20 +113,6 @@ export class SASjsApiClient {
|
|||||||
public async refreshTokens(refreshToken: string): Promise<SASjsAuthResponse> {
|
public async refreshTokens(refreshToken: string): Promise<SASjsAuthResponse> {
|
||||||
return refreshTokensForSasjs(this.requestClient, refreshToken)
|
return refreshTokensForSasjs(this.requestClient, refreshToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Performs a login authenticate and returns an auth code for the given client.
|
|
||||||
* @param username - a string representing the username.
|
|
||||||
* @param password - a string representing the password.
|
|
||||||
* @param clientId - the client ID to authenticate with.
|
|
||||||
*/
|
|
||||||
public async getAuthCode(
|
|
||||||
username: string,
|
|
||||||
password: string,
|
|
||||||
clientId: string
|
|
||||||
) {
|
|
||||||
return getAuthCodeForSasjs(this.requestClient, username, password, clientId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo move to sasjs/utils
|
// todo move to sasjs/utils
|
||||||
|
|||||||
@@ -240,7 +240,7 @@ export async function executeScript(
|
|||||||
|
|
||||||
jobResult = await requestClient
|
jobResult = await requestClient
|
||||||
.get<any>(resultLink, access_token, 'text/plain')
|
.get<any>(resultLink, access_token, 'text/plain')
|
||||||
.catch(async (e) => {
|
.catch(async (e: any) => {
|
||||||
if (e instanceof NotFoundError) {
|
if (e instanceof NotFoundError) {
|
||||||
if (logLink) {
|
if (logLink) {
|
||||||
const logUrl = `${logLink.href}/content`
|
const logUrl = `${logLink.href}/content`
|
||||||
@@ -271,7 +271,7 @@ export async function executeScript(
|
|||||||
})
|
})
|
||||||
|
|
||||||
return { result: jobResult?.result, log }
|
return { result: jobResult?.result, log }
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
interface HttpError {
|
interface HttpError {
|
||||||
status: number
|
status: number
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ describe('executeScript', () => {
|
|||||||
'test',
|
'test',
|
||||||
['%put hello'],
|
['%put hello'],
|
||||||
'test context'
|
'test context'
|
||||||
).catch((e) => e)
|
).catch((e: any) => e)
|
||||||
|
|
||||||
expect(error).toContain('Error while getting session.')
|
expect(error).toContain('Error while getting session.')
|
||||||
})
|
})
|
||||||
@@ -128,7 +128,7 @@ describe('executeScript', () => {
|
|||||||
false,
|
false,
|
||||||
defaultPollOptions,
|
defaultPollOptions,
|
||||||
true
|
true
|
||||||
).catch((e) => e)
|
).catch((e: any) => e)
|
||||||
|
|
||||||
expect(error).toContain('Error while getting session variable.')
|
expect(error).toContain('Error while getting session variable.')
|
||||||
})
|
})
|
||||||
@@ -297,7 +297,7 @@ describe('executeScript', () => {
|
|||||||
false,
|
false,
|
||||||
defaultPollOptions,
|
defaultPollOptions,
|
||||||
true
|
true
|
||||||
).catch((e) => e)
|
).catch((e: any) => e)
|
||||||
|
|
||||||
expect(error).toContain('Error while posting job')
|
expect(error).toContain('Error while posting job')
|
||||||
})
|
})
|
||||||
@@ -367,7 +367,7 @@ describe('executeScript', () => {
|
|||||||
true,
|
true,
|
||||||
defaultPollOptions,
|
defaultPollOptions,
|
||||||
true
|
true
|
||||||
).catch((e) => e)
|
).catch((e: any) => e)
|
||||||
|
|
||||||
expect(error).toContain('Error while polling job status.')
|
expect(error).toContain('Error while polling job status.')
|
||||||
})
|
})
|
||||||
@@ -393,7 +393,7 @@ describe('executeScript', () => {
|
|||||||
true,
|
true,
|
||||||
defaultPollOptions,
|
defaultPollOptions,
|
||||||
true
|
true
|
||||||
).catch((e) => e)
|
).catch((e: any) => e)
|
||||||
|
|
||||||
expect(fetchLogsModule.fetchLogByChunks).toHaveBeenCalledWith(
|
expect(fetchLogsModule.fetchLogByChunks).toHaveBeenCalledWith(
|
||||||
requestClient,
|
requestClient,
|
||||||
@@ -468,7 +468,7 @@ describe('executeScript', () => {
|
|||||||
true,
|
true,
|
||||||
defaultPollOptions,
|
defaultPollOptions,
|
||||||
true
|
true
|
||||||
).catch((e) => e)
|
).catch((e: any) => e)
|
||||||
|
|
||||||
expect(fetchLogsModule.fetchLogByChunks).toHaveBeenCalledWith(
|
expect(fetchLogsModule.fetchLogByChunks).toHaveBeenCalledWith(
|
||||||
requestClient,
|
requestClient,
|
||||||
@@ -501,7 +501,7 @@ describe('executeScript', () => {
|
|||||||
true,
|
true,
|
||||||
defaultPollOptions,
|
defaultPollOptions,
|
||||||
true
|
true
|
||||||
).catch((e) => e)
|
).catch((e: any) => e)
|
||||||
|
|
||||||
expect(fetchLogsModule.fetchLogByChunks).toHaveBeenCalledWith(
|
expect(fetchLogsModule.fetchLogByChunks).toHaveBeenCalledWith(
|
||||||
requestClient,
|
requestClient,
|
||||||
@@ -561,7 +561,7 @@ describe('executeScript', () => {
|
|||||||
true,
|
true,
|
||||||
defaultPollOptions,
|
defaultPollOptions,
|
||||||
true
|
true
|
||||||
).catch((e) => e)
|
).catch((e: any) => e)
|
||||||
|
|
||||||
expect(requestClient.get).toHaveBeenCalledWith(
|
expect(requestClient.get).toHaveBeenCalledWith(
|
||||||
`/compute/sessions/${mockSession.id}/filerefs/_webout/content`,
|
`/compute/sessions/${mockSession.id}/filerefs/_webout/content`,
|
||||||
@@ -622,7 +622,7 @@ describe('executeScript', () => {
|
|||||||
true,
|
true,
|
||||||
defaultPollOptions,
|
defaultPollOptions,
|
||||||
true
|
true
|
||||||
).catch((e) => e)
|
).catch((e: any) => e)
|
||||||
|
|
||||||
expect(error).toContain('Error while clearing session.')
|
expect(error).toContain('Error while clearing session.')
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ describe('pollJobState', () => {
|
|||||||
false,
|
false,
|
||||||
undefined,
|
undefined,
|
||||||
defaultPollOptions
|
defaultPollOptions
|
||||||
).catch((e) => e)
|
).catch((e: any) => e)
|
||||||
|
|
||||||
expect((error as Error).message).toContain('Job state link was not found.')
|
expect((error as Error).message).toContain('Job state link was not found.')
|
||||||
})
|
})
|
||||||
@@ -238,7 +238,7 @@ describe('pollJobState', () => {
|
|||||||
false,
|
false,
|
||||||
undefined,
|
undefined,
|
||||||
defaultPollOptions
|
defaultPollOptions
|
||||||
).catch((e) => e)
|
).catch((e: any) => e)
|
||||||
|
|
||||||
expect(error.message).toEqual(
|
expect(error.message).toEqual(
|
||||||
'Error while polling job state for job j0b: Status Error'
|
'Error while polling job state for job j0b: Status Error'
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ describe('saveLog', () => {
|
|||||||
|
|
||||||
it('should throw an error when a valid access token is not provided', async () => {
|
it('should throw an error when a valid access token is not provided', async () => {
|
||||||
const error = await saveLog(mockJob, requestClient, 0, 100, stream).catch(
|
const error = await saveLog(mockJob, requestClient, 0, 100, stream).catch(
|
||||||
(e) => e
|
(e: any) => e
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(error.message).toContain(
|
expect(error.message).toContain(
|
||||||
@@ -33,7 +33,7 @@ describe('saveLog', () => {
|
|||||||
100,
|
100,
|
||||||
stream,
|
stream,
|
||||||
't0k3n'
|
't0k3n'
|
||||||
).catch((e) => e)
|
).catch((e: any) => e)
|
||||||
|
|
||||||
expect(error.message).toContain(
|
expect(error.message).toContain(
|
||||||
`Log URL for job ${mockJob.id} was not found.`
|
`Log URL for job ${mockJob.id} was not found.`
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ describe('uploadTables', () => {
|
|||||||
.mockImplementation(() => 'ERROR: LARGE STRING LENGTH')
|
.mockImplementation(() => 'ERROR: LARGE STRING LENGTH')
|
||||||
|
|
||||||
const error = await uploadTables(requestClient, data, 't0k3n').catch(
|
const error = await uploadTables(requestClient, data, 't0k3n').catch(
|
||||||
(e) => e
|
(e: any) => e
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(requestClient.uploadFile).not.toHaveBeenCalled()
|
expect(requestClient.uploadFile).not.toHaveBeenCalled()
|
||||||
@@ -46,7 +46,7 @@ describe('uploadTables', () => {
|
|||||||
.mockImplementation(() => Promise.reject('Upload Error'))
|
.mockImplementation(() => Promise.reject('Upload Error'))
|
||||||
|
|
||||||
const error = await uploadTables(requestClient, data, 't0k3n').catch(
|
const error = await uploadTables(requestClient, data, 't0k3n').catch(
|
||||||
(e) => e
|
(e: any) => e
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(error).toContain('Error while uploading file.')
|
expect(error).toContain('Error while uploading file.')
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ describe('writeStream', () => {
|
|||||||
jest
|
jest
|
||||||
.spyOn(stream, 'write')
|
.spyOn(stream, 'write')
|
||||||
.mockImplementation((_, callback) => callback(new Error('Test Error')))
|
.mockImplementation((_, callback) => callback(new Error('Test Error')))
|
||||||
const error = await writeStream(stream, content).catch((e) => e)
|
const error = await writeStream(stream, content).catch((e: any) => e)
|
||||||
|
|
||||||
expect(error.message).toEqual('Test Error')
|
expect(error.message).toEqual('Test Error')
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export const writeStream = async (
|
|||||||
stream: WriteStream,
|
stream: WriteStream,
|
||||||
content: string
|
content: string
|
||||||
): Promise<void> =>
|
): Promise<void> =>
|
||||||
stream.write(content + '\n', (e) => {
|
stream.write(content + '\n', (e: any) => {
|
||||||
if (e) return Promise.reject(e)
|
if (e) return Promise.reject(e)
|
||||||
|
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import { ServerType } from '@sasjs/utils/types'
|
|||||||
import { RequestClient } from '../request/RequestClient'
|
import { RequestClient } from '../request/RequestClient'
|
||||||
import { LoginOptions, LoginResult } from '../types/Login'
|
import { LoginOptions, LoginResult } from '../types/Login'
|
||||||
import { serialize } from '../utils'
|
import { serialize } from '../utils'
|
||||||
import { getAccessTokenForSasjs } from './getAccessTokenForSasjs'
|
|
||||||
import { getAuthCodeForSasjs } from './getAuthCodeForSasjs'
|
|
||||||
import { openWebPage } from './openWebPage'
|
import { openWebPage } from './openWebPage'
|
||||||
import { verifySas9Login } from './verifySas9Login'
|
import { verifySas9Login } from './verifySas9Login'
|
||||||
import { verifySasViyaLogin } from './verifySasViyaLogin'
|
import { verifySasViyaLogin } from './verifySasViyaLogin'
|
||||||
@@ -25,7 +23,7 @@ export class AuthManager {
|
|||||||
? '/SASLogon/logout?'
|
? '/SASLogon/logout?'
|
||||||
: this.serverType === ServerType.SasViya
|
: this.serverType === ServerType.SasViya
|
||||||
? '/SASLogon/logout.do?'
|
? '/SASLogon/logout.do?'
|
||||||
: '/SASjsApi/auth/logout'
|
: '/SASLogon/logout'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -83,39 +81,6 @@ export class AuthManager {
|
|||||||
return { isLoggedIn: false, userName: '' }
|
return { isLoggedIn: false, userName: '' }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Logs into the SAS server with the supplied credentials.
|
|
||||||
* @param userName - a string representing the username.
|
|
||||||
* @param password - a string representing the password.
|
|
||||||
* @param clientId - a string representing the client ID.
|
|
||||||
* @returns - a boolean `isLoggedin` and a string `username`
|
|
||||||
*/
|
|
||||||
public async logInSasjs(
|
|
||||||
username: string,
|
|
||||||
password: string,
|
|
||||||
clientId: string
|
|
||||||
): Promise<LoginResult> {
|
|
||||||
const isLoggedIn = await this.sendLoginRequestSasjs(
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
clientId
|
|
||||||
)
|
|
||||||
.then((res) => {
|
|
||||||
this.userName = username
|
|
||||||
this.requestClient.saveLocalStorageToken(
|
|
||||||
res.access_token,
|
|
||||||
res.refresh_token
|
|
||||||
)
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
.catch(() => false)
|
|
||||||
|
|
||||||
return {
|
|
||||||
isLoggedIn,
|
|
||||||
userName: this.userName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logs into the SAS server with the supplied credentials.
|
* Logs into the SAS server with the supplied credentials.
|
||||||
* @param username - a string representing the username.
|
* @param username - a string representing the username.
|
||||||
@@ -152,7 +117,7 @@ export class AuthManager {
|
|||||||
|
|
||||||
let loginResponse = await this.sendLoginRequest(loginForm, loginParams)
|
let loginResponse = await this.sendLoginRequest(loginForm, loginParams)
|
||||||
|
|
||||||
let isLoggedIn = isLogInSuccess(loginResponse)
|
let isLoggedIn = isLogInSuccess(this.serverType, loginResponse)
|
||||||
|
|
||||||
if (!isLoggedIn) {
|
if (!isLoggedIn) {
|
||||||
if (isCredentialsVerifyError(loginResponse)) {
|
if (isCredentialsVerifyError(loginResponse)) {
|
||||||
@@ -196,6 +161,17 @@ export class AuthManager {
|
|||||||
loginForm: { [key: string]: any },
|
loginForm: { [key: string]: any },
|
||||||
loginParams: { [key: string]: any }
|
loginParams: { [key: string]: any }
|
||||||
) {
|
) {
|
||||||
|
if (this.serverType === ServerType.Sasjs) {
|
||||||
|
const { username, password } = loginParams
|
||||||
|
const { result: loginResponse } = await this.requestClient.post<string>(
|
||||||
|
this.loginUrl,
|
||||||
|
{ username, password },
|
||||||
|
undefined
|
||||||
|
)
|
||||||
|
|
||||||
|
return loginResponse
|
||||||
|
}
|
||||||
|
|
||||||
for (const key in loginForm) {
|
for (const key in loginForm) {
|
||||||
loginParams[key] = loginForm[key]
|
loginParams[key] = loginForm[key]
|
||||||
}
|
}
|
||||||
@@ -215,19 +191,6 @@ export class AuthManager {
|
|||||||
return loginResponse
|
return loginResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
private async sendLoginRequestSasjs(
|
|
||||||
username: string,
|
|
||||||
password: string,
|
|
||||||
clientId: string
|
|
||||||
) {
|
|
||||||
const authCode = await getAuthCodeForSasjs(
|
|
||||||
this.requestClient,
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
clientId
|
|
||||||
)
|
|
||||||
return getAccessTokenForSasjs(this.requestClient, clientId, authCode)
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* Checks whether a session is active, or login is required.
|
* Checks whether a session is active, or login is required.
|
||||||
* @returns - a promise which resolves with an object containing three values
|
* @returns - a promise which resolves with an object containing three values
|
||||||
@@ -248,8 +211,7 @@ export class AuthManager {
|
|||||||
//Residue can happen in case of session expiration
|
//Residue can happen in case of session expiration
|
||||||
await this.logOut()
|
await this.logOut()
|
||||||
|
|
||||||
if (this.serverType !== ServerType.Sasjs)
|
loginForm = await this.getNewLoginForm()
|
||||||
loginForm = await this.getNewLoginForm()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
@@ -260,6 +222,20 @@ export class AuthManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async getNewLoginForm() {
|
private async getNewLoginForm() {
|
||||||
|
if (this.serverType === ServerType.Sasjs) {
|
||||||
|
// server will be sending CSRF token in response,
|
||||||
|
// need to save in cookie so that,
|
||||||
|
// http client will use it automatically
|
||||||
|
return this.requestClient.get('/', undefined).then(({ result }) => {
|
||||||
|
const cookie =
|
||||||
|
/<script>document.cookie = '(XSRF-TOKEN=.*; Max-Age=86400; SameSite=Strict; Path=\/;)'<\/script>/.exec(
|
||||||
|
result as string
|
||||||
|
)?.[1]
|
||||||
|
|
||||||
|
if (cookie) document.cookie = cookie
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const { result: formResponse } = await this.requestClient.get<string>(
|
const { result: formResponse } = await this.requestClient.get<string>(
|
||||||
this.loginUrl.replace('.do', ''),
|
this.loginUrl.replace('.do', ''),
|
||||||
undefined,
|
undefined,
|
||||||
@@ -289,6 +265,12 @@ export class AuthManager {
|
|||||||
const isLoggedIn = loginResponse !== 'authErr'
|
const isLoggedIn = loginResponse !== 'authErr'
|
||||||
const userName = isLoggedIn ? this.extractUserName(loginResponse) : ''
|
const userName = isLoggedIn ? this.extractUserName(loginResponse) : ''
|
||||||
|
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
//We will logout to make sure cookies are removed and login form is presented
|
||||||
|
//Residue can happen in case of session expiration
|
||||||
|
await this.logOut()
|
||||||
|
}
|
||||||
|
|
||||||
return { isLoggedIn, userName }
|
return { isLoggedIn, userName }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,19 +342,9 @@ export class AuthManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Logs out of the configured SAS server.
|
* Logs out of the configured SAS server.
|
||||||
* @param accessToken - an optional access token is required for SASjs server type.
|
*
|
||||||
*/
|
*/
|
||||||
public async logOut() {
|
public async logOut() {
|
||||||
if (this.serverType === ServerType.Sasjs) {
|
|
||||||
return this.requestClient
|
|
||||||
.delete(this.logoutUrl)
|
|
||||||
.catch(() => true)
|
|
||||||
.finally(() => {
|
|
||||||
this.requestClient.clearLocalStorageTokens()
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
this.requestClient.clearCsrfTokens()
|
this.requestClient.clearCsrfTokens()
|
||||||
|
|
||||||
return this.requestClient.get(this.logoutUrl, undefined).then(() => true)
|
return this.requestClient.get(this.logoutUrl, undefined).then(() => true)
|
||||||
@@ -384,5 +356,8 @@ const isCredentialsVerifyError = (response: string): boolean =>
|
|||||||
response
|
response
|
||||||
)
|
)
|
||||||
|
|
||||||
const isLogInSuccess = (response: string): boolean =>
|
const isLogInSuccess = (serverType: ServerType, response: any): boolean => {
|
||||||
/You have signed in/gm.test(response)
|
if (serverType === ServerType.Sasjs) return response?.loggedin
|
||||||
|
|
||||||
|
return /You have signed in/gm.test(response)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
import { prefixMessage } from '@sasjs/utils/error'
|
|
||||||
import { RequestClient } from '../request/RequestClient'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Performs a login authenticate and returns an auth code for the given client.
|
|
||||||
* @param requestClient - the pre-configured HTTP request client
|
|
||||||
* @param username - a string representing the username.
|
|
||||||
* @param password - a string representing the password.
|
|
||||||
* @param clientId - the client ID to authenticate with.
|
|
||||||
*/
|
|
||||||
export const getAuthCodeForSasjs = async (
|
|
||||||
requestClient: RequestClient,
|
|
||||||
username: string,
|
|
||||||
password: string,
|
|
||||||
clientId: string
|
|
||||||
) => {
|
|
||||||
const url = '/SASjsApi/auth/authorize'
|
|
||||||
const data = { username, password, clientId }
|
|
||||||
|
|
||||||
const { code: authCode } = await requestClient
|
|
||||||
.post<{ code: string }>(url, data, undefined)
|
|
||||||
.then((res) => res.result)
|
|
||||||
.catch((err) => {
|
|
||||||
throw prefixMessage(
|
|
||||||
err,
|
|
||||||
'Error while authenticating with provided username, password and clientId. '
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
return authCode
|
|
||||||
}
|
|
||||||
@@ -28,7 +28,7 @@ export async function refreshTokensForSasjs(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
throw prefixMessage(err, 'Error while refreshing tokens')
|
throw prefixMessage(err, 'Error while refreshing tokens: ')
|
||||||
})
|
})
|
||||||
|
|
||||||
return authResponse
|
return authResponse
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export async function refreshTokensForViya(
|
|||||||
)
|
)
|
||||||
.then((res) => res.result)
|
.then((res) => res.result)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
throw prefixMessage(err, 'Error while refreshing tokens')
|
throw prefixMessage(err, 'Error while refreshing tokens: ')
|
||||||
})
|
})
|
||||||
|
|
||||||
return authResponse
|
return authResponse
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ describe('getAccessTokenForSasjs', () => {
|
|||||||
requestClient,
|
requestClient,
|
||||||
authConfig.client,
|
authConfig.client,
|
||||||
authConfig.refresh_token
|
authConfig.refresh_token
|
||||||
).catch((e) => e)
|
).catch((e: any) => e)
|
||||||
|
|
||||||
expect(error).toContain('Error while getting access token')
|
expect(error).toContain('Error while getting access token')
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ describe('getAccessTokenForViya', () => {
|
|||||||
authConfig.client,
|
authConfig.client,
|
||||||
authConfig.secret,
|
authConfig.secret,
|
||||||
authConfig.refresh_token
|
authConfig.refresh_token
|
||||||
).catch((e) => e)
|
).catch((e: any) => e)
|
||||||
|
|
||||||
expect(error).toContain('Error while getting access token')
|
expect(error).toContain('Error while getting access token')
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -62,7 +62,9 @@ describe('getTokens', () => {
|
|||||||
const expectedError =
|
const expectedError =
|
||||||
'Unable to obtain new access token. Your refresh token has expired.'
|
'Unable to obtain new access token. Your refresh token has expired.'
|
||||||
|
|
||||||
const error = await getTokens(requestClient, authConfig).catch((e) => e)
|
const error = await getTokens(requestClient, authConfig).catch(
|
||||||
|
(e: any) => e
|
||||||
|
)
|
||||||
|
|
||||||
expect(error.message).toEqual(expectedError)
|
expect(error.message).toEqual(expectedError)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -27,17 +27,20 @@ describe('refreshTokensForSasjs', () => {
|
|||||||
|
|
||||||
it('should handle errors while refreshing tokens', async () => {
|
it('should handle errors while refreshing tokens', async () => {
|
||||||
setupMocks()
|
setupMocks()
|
||||||
|
|
||||||
const refresh_token = generateToken(30)
|
const refresh_token = generateToken(30)
|
||||||
|
const tokenError = 'unable to verify the first certificate'
|
||||||
|
|
||||||
jest
|
jest
|
||||||
.spyOn(requestClient, 'post')
|
.spyOn(requestClient, 'post')
|
||||||
.mockImplementation(() => Promise.reject('Token Error'))
|
.mockImplementation(() => Promise.reject(tokenError))
|
||||||
|
|
||||||
const error = await refreshTokensForSasjs(
|
const error = await refreshTokensForSasjs(
|
||||||
requestClient,
|
requestClient,
|
||||||
refresh_token
|
refresh_token
|
||||||
).catch((e) => e)
|
).catch((e: any) => e)
|
||||||
|
|
||||||
expect(error).toContain('Error while refreshing tokens')
|
expect(error).toEqual(`Error while refreshing tokens: ${tokenError}`)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -46,26 +46,29 @@ describe('refreshTokensForViya', () => {
|
|||||||
|
|
||||||
it('should handle errors while refreshing tokens', async () => {
|
it('should handle errors while refreshing tokens', async () => {
|
||||||
setupMocks()
|
setupMocks()
|
||||||
|
|
||||||
const access_token = generateToken(30)
|
const access_token = generateToken(30)
|
||||||
const refresh_token = generateToken(30)
|
const refresh_token = generateToken(30)
|
||||||
|
const tokenError = 'unable to verify the first certificate'
|
||||||
const authConfig: AuthConfig = {
|
const authConfig: AuthConfig = {
|
||||||
access_token,
|
access_token,
|
||||||
refresh_token,
|
refresh_token,
|
||||||
client: 'cl13nt',
|
client: 'cl13nt',
|
||||||
secret: 's3cr3t'
|
secret: 's3cr3t'
|
||||||
}
|
}
|
||||||
|
|
||||||
jest
|
jest
|
||||||
.spyOn(requestClient, 'post')
|
.spyOn(requestClient, 'post')
|
||||||
.mockImplementation(() => Promise.reject('Token Error'))
|
.mockImplementation(() => Promise.reject(tokenError))
|
||||||
|
|
||||||
const error = await refreshTokensForViya(
|
const error = await refreshTokensForViya(
|
||||||
requestClient,
|
requestClient,
|
||||||
authConfig.client,
|
authConfig.client,
|
||||||
authConfig.secret,
|
authConfig.secret,
|
||||||
authConfig.refresh_token
|
authConfig.refresh_token
|
||||||
).catch((e) => e)
|
).catch((e: any) => e)
|
||||||
|
|
||||||
expect(error).toContain('Error while refreshing tokens')
|
expect(error).toEqual(`Error while refreshing tokens: ${tokenError}`)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as NodeFormData from 'form-data'
|
import * as NodeFormData from 'form-data'
|
||||||
import { convertToCSV } from '../utils/convertToCsv'
|
import { convertToCSV, isFormatsTable } from '../utils/convertToCsv'
|
||||||
import { splitChunks } from '../utils/splitChunks'
|
import { splitChunks } from '../utils/splitChunks'
|
||||||
|
|
||||||
export const generateTableUploadForm = (
|
export const generateTableUploadForm = (
|
||||||
@@ -13,7 +13,8 @@ export const generateTableUploadForm = (
|
|||||||
for (const tableName in data) {
|
for (const tableName in data) {
|
||||||
tableCounter++
|
tableCounter++
|
||||||
|
|
||||||
sasjsTables.push(tableName)
|
// Formats table should not be sent as part of 'sasjs_tables'
|
||||||
|
if (!isFormatsTable(tableName)) sasjsTables.push(tableName)
|
||||||
|
|
||||||
const csv = convertToCSV(data, tableName)
|
const csv = convertToCSV(data, tableName)
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ describe('generateFileUploadForm', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const BlobMock = jest.fn()
|
const BlobMock = jest.fn()
|
||||||
|
|
||||||
;(global as any).FormData = FormDataMock
|
;(global as any).FormData = FormDataMock
|
||||||
;(global as any).Blob = BlobMock
|
;(global as any).Blob = BlobMock
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export class WebJobExecutor extends BaseJobExecutor {
|
|||||||
if (jobUri.length > 0) {
|
if (jobUri.length > 0) {
|
||||||
apiUrl += '&_job=' + jobUri
|
apiUrl += '&_job=' + jobUri
|
||||||
/**
|
/**
|
||||||
* Using both _job and _program parameters will cause a conflict in the JES web app, as it’s not clear whether or not the server should make the extra fetch for the job uri.
|
* Using both _job and _program parameters will cause a conflict in the JES web app, as it's not clear whether or not the server should make the extra fetch for the job uri.
|
||||||
* To handle this, we add the extra underscore and recreate the _program variable in the SAS side of the SASjs adapter so it remains available for backend developers.
|
* To handle this, we add the extra underscore and recreate the _program variable in the SAS side of the SASjs adapter so it remains available for backend developers.
|
||||||
*/
|
*/
|
||||||
apiUrl = apiUrl.replace('_program=', '__program=')
|
apiUrl = apiUrl.replace('_program=', '__program=')
|
||||||
@@ -176,6 +176,18 @@ export class WebJobExecutor extends BaseJobExecutor {
|
|||||||
log: parsedSasjsServerLog
|
log: parsedSasjsServerLog
|
||||||
}
|
}
|
||||||
: res
|
: res
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.serverType === ServerType.Sasjs &&
|
||||||
|
res.result._webout.length < 1
|
||||||
|
) {
|
||||||
|
throw new JobExecutionError(
|
||||||
|
0,
|
||||||
|
`No webout was returned by job ${program}. Server type is SASJS and the calling function is WebJobExecutor. Please check the SAS log for more info.`,
|
||||||
|
parsedSasjsServerLog
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
this.requestClient!.appendRequest(resObj, sasJob, config.debug)
|
this.requestClient!.appendRequest(resObj, sasJob, config.debug)
|
||||||
|
|
||||||
let jsonResponse = res.result
|
let jsonResponse = res.result
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
parseSourceCode,
|
parseSourceCode,
|
||||||
createAxiosInstance
|
createAxiosInstance
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
|
import { InvalidSASjsCsrfError } from '../types/errors/InvalidSASjsCsrfError'
|
||||||
|
|
||||||
export interface HttpClient {
|
export interface HttpClient {
|
||||||
get<T>(
|
get<T>(
|
||||||
@@ -206,7 +207,7 @@ export class RequestClient implements HttpClient {
|
|||||||
|
|
||||||
return this.parseResponse<T>(response)
|
return this.parseResponse<T>(response)
|
||||||
})
|
})
|
||||||
.catch(async (e) => {
|
.catch(async (e: any) => {
|
||||||
return await this.handleError(
|
return await this.handleError(
|
||||||
e,
|
e,
|
||||||
() =>
|
() =>
|
||||||
@@ -247,7 +248,7 @@ export class RequestClient implements HttpClient {
|
|||||||
|
|
||||||
return this.parseResponse<T>(response)
|
return this.parseResponse<T>(response)
|
||||||
})
|
})
|
||||||
.catch(async (e) => {
|
.catch(async (e: any) => {
|
||||||
return await this.handleError(e, () =>
|
return await this.handleError(e, () =>
|
||||||
this.post<T>(url, data, accessToken, contentType, overrideHeaders)
|
this.post<T>(url, data, accessToken, contentType, overrideHeaders)
|
||||||
)
|
)
|
||||||
@@ -271,7 +272,7 @@ export class RequestClient implements HttpClient {
|
|||||||
throwIfError(response)
|
throwIfError(response)
|
||||||
return this.parseResponse<T>(response)
|
return this.parseResponse<T>(response)
|
||||||
})
|
})
|
||||||
.catch(async (e) => {
|
.catch(async (e: any) => {
|
||||||
return await this.handleError(e, () =>
|
return await this.handleError(e, () =>
|
||||||
this.put<T>(url, data, accessToken, overrideHeaders)
|
this.put<T>(url, data, accessToken, overrideHeaders)
|
||||||
)
|
)
|
||||||
@@ -290,7 +291,7 @@ export class RequestClient implements HttpClient {
|
|||||||
throwIfError(response)
|
throwIfError(response)
|
||||||
return this.parseResponse<T>(response)
|
return this.parseResponse<T>(response)
|
||||||
})
|
})
|
||||||
.catch(async (e) => {
|
.catch(async (e: any) => {
|
||||||
return await this.handleError(e, () => this.delete<T>(url, accessToken))
|
return await this.handleError(e, () => this.delete<T>(url, accessToken))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -308,7 +309,7 @@ export class RequestClient implements HttpClient {
|
|||||||
throwIfError(response)
|
throwIfError(response)
|
||||||
return this.parseResponse<T>(response)
|
return this.parseResponse<T>(response)
|
||||||
})
|
})
|
||||||
.catch(async (e) => {
|
.catch(async (e: any) => {
|
||||||
return await this.handleError(e, () =>
|
return await this.handleError(e, () =>
|
||||||
this.patch<T>(url, data, accessToken)
|
this.patch<T>(url, data, accessToken)
|
||||||
)
|
)
|
||||||
@@ -498,6 +499,32 @@ export class RequestClient implements HttpClient {
|
|||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (e instanceof InvalidSASjsCsrfError) {
|
||||||
|
// Fetching root and creating CSRF cookie
|
||||||
|
await this.httpClient
|
||||||
|
.get('/', {
|
||||||
|
withCredentials: true
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
const cookie =
|
||||||
|
/<script>document.cookie = '(XSRF-TOKEN=.*; Max-Age=86400; SameSite=Strict; Path=\/;)'<\/script>/.exec(
|
||||||
|
response.data
|
||||||
|
)?.[1]
|
||||||
|
|
||||||
|
if (cookie) document.cookie = cookie
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
throw prefixMessage(err, 'Error while re-fetching CSRF token.')
|
||||||
|
})
|
||||||
|
|
||||||
|
return await callback().catch((err: any) => {
|
||||||
|
throw prefixMessage(
|
||||||
|
err,
|
||||||
|
'Error while executing callback in handleError. '
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (response?.status === 403 || response?.status === 449) {
|
if (response?.status === 403 || response?.status === 449) {
|
||||||
this.parseAndSetCsrfToken(response)
|
this.parseAndSetCsrfToken(response)
|
||||||
|
|
||||||
@@ -584,9 +611,20 @@ export class RequestClient implements HttpClient {
|
|||||||
export const throwIfError = (response: AxiosResponse) => {
|
export const throwIfError = (response: AxiosResponse) => {
|
||||||
switch (response.status) {
|
switch (response.status) {
|
||||||
case 400:
|
case 400:
|
||||||
if (typeof response.data === 'object') {
|
if (
|
||||||
|
typeof response.data === 'object' &&
|
||||||
|
response.data.error === 'invalid_grant'
|
||||||
|
) {
|
||||||
|
// In SASVIYA when trying to get access token, if auth code is wrong status code will be 400 so in such case we return login required error.
|
||||||
throw new LoginRequiredError(response.data)
|
throw new LoginRequiredError(response.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof response.data === 'string' &&
|
||||||
|
response.data.toLowerCase() === 'invalid csrf token!'
|
||||||
|
) {
|
||||||
|
throw new InvalidSASjsCsrfError()
|
||||||
|
}
|
||||||
break
|
break
|
||||||
case 401:
|
case 401:
|
||||||
if (typeof response.data === 'object') {
|
if (typeof response.data === 'object') {
|
||||||
@@ -676,7 +714,14 @@ const parseError = (data: string) => {
|
|||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// There are some edge cases in which the SAS mp_abort macro
|
||||||
|
// (https://core.sasjs.io/mp__abort_8sas.html) is unable to
|
||||||
|
// provide a clean exit. In this case the JSON response will
|
||||||
|
// be wrapped in >>weboutBEGIN<< and >>weboutEND<< strings.
|
||||||
|
// Therefore, if the first string exists, we won't throw an
|
||||||
|
// error just yet (the parser may yet throw one instead)
|
||||||
const hasError =
|
const hasError =
|
||||||
|
!data?.match(/>>weboutBEGIN<</) &&
|
||||||
!!data?.match(/Stored Process Error/i) &&
|
!!data?.match(/Stored Process Error/i) &&
|
||||||
!!data?.match(/This request completed with errors./i)
|
!!data?.match(/This request completed with errors./i)
|
||||||
if (hasError) {
|
if (hasError) {
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export class Sas9RequestClient extends RequestClient {
|
|||||||
throwIfError(response)
|
throwIfError(response)
|
||||||
return this.parseResponse<T>(response)
|
return this.parseResponse<T>(response)
|
||||||
})
|
})
|
||||||
.catch(async (e) => {
|
.catch(async (e: any) => {
|
||||||
return await this.handleError(
|
return await this.handleError(
|
||||||
e,
|
e,
|
||||||
() =>
|
() =>
|
||||||
@@ -113,7 +113,7 @@ export class Sas9RequestClient extends RequestClient {
|
|||||||
throwIfError(response)
|
throwIfError(response)
|
||||||
return this.parseResponse<T>(response)
|
return this.parseResponse<T>(response)
|
||||||
})
|
})
|
||||||
.catch(async (e) => {
|
.catch(async (e: any) => {
|
||||||
return await this.handleError(e, () =>
|
return await this.handleError(e, () =>
|
||||||
this.post<T>(url, data, accessToken, contentType, overrideHeaders)
|
this.post<T>(url, data, accessToken, contentType, overrideHeaders)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,13 +11,16 @@ describe('formatDataForRequest', () => {
|
|||||||
{ var1: 'string', var2: 232, nullvar: '_' },
|
{ var1: 'string', var2: 232, nullvar: '_' },
|
||||||
{ var1: 'string', var2: 232, nullvar: 0 },
|
{ var1: 'string', var2: 232, nullvar: 0 },
|
||||||
{ var1: 'string', var2: 232, nullvar: 'z' },
|
{ var1: 'string', var2: 232, nullvar: 'z' },
|
||||||
{ var1: 'string', var2: 232, nullvar: null }
|
{ var1: 'string', var2: 232, nullvar: null },
|
||||||
|
{ var1: 'string', var2: 232, nullvar: '.A' },
|
||||||
|
{ var1: 'string', var2: 232, nullvar: '._' },
|
||||||
|
{ var1: 'string', var2: 232, nullvar: '.' }
|
||||||
],
|
],
|
||||||
[`$${testTable}`]: { formats: { var1: '$char12.', nullvar: 'best.' } }
|
[`$${testTable}`]: { formats: { var1: '$char12.', nullvar: 'best.' } }
|
||||||
}
|
}
|
||||||
|
|
||||||
const expectedOutput = {
|
const expectedOutput = {
|
||||||
sasjs1data: `var1:$char12. var2:best. nullvar:best.\r\nstring,232,.a\r\nstring,232,.b\r\nstring,232,._\r\nstring,232,0\r\nstring,232,.z\r\nstring,232,.`,
|
sasjs1data: `var1:$char12. var2:best. nullvar:best.\r\nstring,232,.a\r\nstring,232,.b\r\nstring,232,._\r\nstring,232,0\r\nstring,232,.z\r\nstring,232,.\r\nstring,232,.a\r\nstring,232,._\r\nstring,232,.`,
|
||||||
sasjs_tables: testTable
|
sasjs_tables: testTable
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,6 +57,17 @@ describe('formatDataForRequest', () => {
|
|||||||
expect(formatDataForRequest(data)).toEqual(expectedOutput)
|
expect(formatDataForRequest(data)).toEqual(expectedOutput)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should accept . as special missing value', () => {
|
||||||
|
let tableWithMissingValues = {
|
||||||
|
[testTable]: [{ var: '.' }, { var: 0 }],
|
||||||
|
[`$${testTable}`]: { formats: { var: 'best.' } }
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
formatDataForRequest(tableWithMissingValues)
|
||||||
|
).not.toThrowError()
|
||||||
|
})
|
||||||
|
|
||||||
it('should throw an error if special missing values is not valid', () => {
|
it('should throw an error if special missing values is not valid', () => {
|
||||||
let tableWithMissingValues = {
|
let tableWithMissingValues = {
|
||||||
[testTable]: [{ var: 'AA' }, { var: 0 }],
|
[testTable]: [{ var: 'AA' }, { var: 0 }],
|
||||||
|
|||||||
9
src/types/errors/InvalidSASjsCsrfError.ts
Normal file
9
src/types/errors/InvalidSASjsCsrfError.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export class InvalidSASjsCsrfError extends Error {
|
||||||
|
constructor() {
|
||||||
|
const message = 'Invalid CSRF token!'
|
||||||
|
|
||||||
|
super(`Auth error: ${message}`)
|
||||||
|
this.name = 'InvalidSASjsCsrfError'
|
||||||
|
Object.setPrototypeOf(this, InvalidSASjsCsrfError.prototype)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { isSpecialMissing } from '@sasjs/utils'
|
import { isSpecialMissing } from '@sasjs/utils/input/validators'
|
||||||
|
import { prefixMessage } from '@sasjs/utils/error'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts the given JSON object array to a CSV string.
|
* Converts the given JSON object array to a CSV string.
|
||||||
@@ -9,7 +10,10 @@ export const convertToCSV = (
|
|||||||
tableName: string
|
tableName: string
|
||||||
) => {
|
) => {
|
||||||
if (!data[tableName]) {
|
if (!data[tableName]) {
|
||||||
throw new Error('No table provided to be converted to CSV')
|
throw prefixMessage(
|
||||||
|
'No table provided to be converted to CSV.',
|
||||||
|
'Error while converting to CSV. '
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const table = data[tableName]
|
const table = data[tableName]
|
||||||
@@ -137,7 +141,9 @@ export const convertToCSV = (
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return `.${value.toLowerCase()}`
|
const dot = value.includes('.') ? '' : '.'
|
||||||
|
|
||||||
|
return `${dot}${value.toLowerCase()}`
|
||||||
}
|
}
|
||||||
|
|
||||||
// if there any present, it should have preceding (") for escaping
|
// if there any present, it should have preceding (") for escaping
|
||||||
@@ -170,6 +176,12 @@ export const convertToCSV = (
|
|||||||
return finalCSV
|
return finalCSV
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if table is table of formats (table name should start from '$' character).
|
||||||
|
* @param tableName - table name.
|
||||||
|
*/
|
||||||
|
export const isFormatsTable = (tableName: string) => /^\$.*/.test(tableName)
|
||||||
|
|
||||||
const getByteSize = (str: string) => {
|
const getByteSize = (str: string) => {
|
||||||
let byteSize = str.length
|
let byteSize = str.length
|
||||||
for (let i = str.length - 1; i >= 0; i--) {
|
for (let i = str.length - 1; i >= 0; i--) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { convertToCSV } from './convertToCsv'
|
import { convertToCSV, isFormatsTable } from './convertToCsv'
|
||||||
import { splitChunks } from './splitChunks'
|
import { splitChunks } from './splitChunks'
|
||||||
|
|
||||||
export const formatDataForRequest = (data: any) => {
|
export const formatDataForRequest = (data: any) => {
|
||||||
@@ -8,7 +8,7 @@ export const formatDataForRequest = (data: any) => {
|
|||||||
|
|
||||||
for (const tableName in data) {
|
for (const tableName in data) {
|
||||||
if (
|
if (
|
||||||
tableName.match(/^\$.*/) &&
|
isFormatsTable(tableName) &&
|
||||||
Object.keys(data).includes(tableName.replace(/^\$/, ''))
|
Object.keys(data).includes(tableName.replace(/^\$/, ''))
|
||||||
) {
|
) {
|
||||||
continue
|
continue
|
||||||
@@ -16,7 +16,8 @@ export const formatDataForRequest = (data: any) => {
|
|||||||
|
|
||||||
tableCounter++
|
tableCounter++
|
||||||
|
|
||||||
sasjsTables.push(tableName)
|
// Formats table should not be sent as part of 'sasjs_tables'
|
||||||
|
if (!isFormatsTable(tableName)) sasjsTables.push(tableName)
|
||||||
|
|
||||||
const csv = convertToCSV(data, tableName)
|
const csv = convertToCSV(data, tableName)
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const getValidJson = (str: string | object): object => {
|
|||||||
if (str === '') return {}
|
if (str === '') return {}
|
||||||
|
|
||||||
return JSON.parse(str)
|
return JSON.parse(str)
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
if (e instanceof JsonParseArrayError) throw e
|
if (e instanceof JsonParseArrayError) throw e
|
||||||
throw new InvalidJsonError()
|
throw new InvalidJsonError()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
|
export * from './appendExtraResponseAttributes'
|
||||||
export * from './asyncForEach'
|
export * from './asyncForEach'
|
||||||
export * from './compareTimestamps'
|
export * from './compareTimestamps'
|
||||||
export * from './convertToCsv'
|
export * from './convertToCsv'
|
||||||
export * from './createAxiosInstance'
|
export * from './createAxiosInstance'
|
||||||
export * from './delay'
|
export * from './delay'
|
||||||
|
export * from './fetchLogByChunks'
|
||||||
|
export * from './getValidJson'
|
||||||
export * from './isNode'
|
export * from './isNode'
|
||||||
export * from './isRelativePath'
|
export * from './isRelativePath'
|
||||||
export * from './isUri'
|
export * from './isUri'
|
||||||
export * from './isUrl'
|
export * from './isUrl'
|
||||||
export * from './needsRetry'
|
export * from './needsRetry'
|
||||||
export * from './parseGeneratedCode'
|
export * from './parseGeneratedCode'
|
||||||
export * from './parseSourceCode'
|
|
||||||
export * from './parseSasViyaLog'
|
export * from './parseSasViyaLog'
|
||||||
|
export * from './parseSourceCode'
|
||||||
|
export * from './parseViyaDebugResponse'
|
||||||
|
export * from './parseWeboutResponse'
|
||||||
export * from './serialize'
|
export * from './serialize'
|
||||||
export * from './splitChunks'
|
export * from './splitChunks'
|
||||||
export * from './parseWeboutResponse'
|
export * from './validateInput'
|
||||||
export * from './fetchLogByChunks'
|
|
||||||
export * from './getValidJson'
|
|
||||||
export * from './parseViyaDebugResponse'
|
|
||||||
export * from './appendExtraResponseAttributes'
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export const parseSasViyaLog = (logResponse: { items: any[] }) => {
|
|||||||
log = logResponse.items
|
log = logResponse.items
|
||||||
? logResponse.items.map((i) => i.line).join('\n')
|
? logResponse.items.map((i) => i.line).join('\n')
|
||||||
: JSON.stringify(logResponse)
|
: JSON.stringify(logResponse)
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
console.error('An error has occurred while parsing the log response', e)
|
console.error('An error has occurred while parsing the log response', e)
|
||||||
log = logResponse
|
log = logResponse
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const parseWeboutResponse = (response: string, url?: string): string => {
|
|||||||
sasResponse = response
|
sasResponse = response
|
||||||
.split('>>weboutBEGIN<<')[1]
|
.split('>>weboutBEGIN<<')[1]
|
||||||
.split('>>weboutEND<<')[0]
|
.split('>>weboutEND<<')[0]
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
if (url) throw new WeboutResponseError(url)
|
if (url) throw new WeboutResponseError(url)
|
||||||
|
|
||||||
sasResponse = ''
|
sasResponse = ''
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { convertToCSV } from './convertToCsv'
|
import { convertToCSV, isFormatsTable } from '../convertToCsv'
|
||||||
|
|
||||||
describe('convertToCsv', () => {
|
describe('convertToCsv', () => {
|
||||||
const tableName = 'testTable'
|
const tableName = 'testTable'
|
||||||
@@ -216,7 +216,9 @@ describe('convertToCsv', () => {
|
|||||||
const data = { [tableName]: [{ var1: 'string' }] }
|
const data = { [tableName]: [{ var1: 'string' }] }
|
||||||
|
|
||||||
expect(() => convertToCSV(data, 'wrongTableName')).toThrow(
|
expect(() => convertToCSV(data, 'wrongTableName')).toThrow(
|
||||||
new Error('No table provided to be converted to CSV')
|
new Error(
|
||||||
|
'Error while converting to CSV. No table provided to be converted to CSV.'
|
||||||
|
)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -226,3 +228,15 @@ describe('convertToCsv', () => {
|
|||||||
expect(convertToCSV(data, tableName)).toEqual('')
|
expect(convertToCSV(data, tableName)).toEqual('')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('isFormatsTable', () => {
|
||||||
|
const tableName = 'sometable'
|
||||||
|
|
||||||
|
it('should return true if table name match pattern of formats table', () => {
|
||||||
|
expect(isFormatsTable(`$${tableName}`)).toEqual(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false if table name does not match pattern of formats table', () => {
|
||||||
|
expect(isFormatsTable(tableName)).toEqual(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
84
src/utils/spec/validateInput.spec.ts
Normal file
84
src/utils/spec/validateInput.spec.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import {
|
||||||
|
validateInput,
|
||||||
|
INVALID_TABLE_STRUCTURE,
|
||||||
|
MORE_INFO
|
||||||
|
} from '../validateInput'
|
||||||
|
|
||||||
|
const tableArray = [{ col1: 'first col value' }]
|
||||||
|
const stringData: any = { table1: tableArray }
|
||||||
|
|
||||||
|
describe('validateInput', () => {
|
||||||
|
it('should not return an error message if input data valid', () => {
|
||||||
|
const validationResult = validateInput(stringData)
|
||||||
|
expect(validationResult).toEqual({
|
||||||
|
status: true,
|
||||||
|
msg: ''
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not return an error message if input data is null', () => {
|
||||||
|
const validationResult = validateInput(null)
|
||||||
|
expect(validationResult).toEqual({
|
||||||
|
status: true,
|
||||||
|
msg: ''
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an error message if input data is an array', () => {
|
||||||
|
const validationResult = validateInput(tableArray)
|
||||||
|
expect(validationResult).toEqual({
|
||||||
|
status: false,
|
||||||
|
msg: INVALID_TABLE_STRUCTURE
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an error message if first letter of table is neither alphabet nor underscore', () => {
|
||||||
|
const validationResult = validateInput({ '1stTable': tableArray })
|
||||||
|
expect(validationResult).toEqual({
|
||||||
|
status: false,
|
||||||
|
msg: 'First letter of table should be alphabet or underscore.'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an error message if table name contains a character other than alphanumeric or underscore', () => {
|
||||||
|
const validationResult = validateInput({ 'table!': tableArray })
|
||||||
|
expect(validationResult).toEqual({
|
||||||
|
status: false,
|
||||||
|
msg: 'Table name should be alphanumeric.'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an error message if length of table name contains exceeds 32', () => {
|
||||||
|
const validationResult = validateInput({
|
||||||
|
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx: tableArray
|
||||||
|
})
|
||||||
|
expect(validationResult).toEqual({
|
||||||
|
status: false,
|
||||||
|
msg: 'Maximum length for table name could be 32 characters.'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an error message if table does not have array of objects', () => {
|
||||||
|
const validationResult = validateInput({ table: stringData })
|
||||||
|
expect(validationResult).toEqual({
|
||||||
|
status: false,
|
||||||
|
msg: INVALID_TABLE_STRUCTURE
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an error message if a table array has an item other than object', () => {
|
||||||
|
const validationResult = validateInput({ table1: ['invalid'] })
|
||||||
|
expect(validationResult).toEqual({
|
||||||
|
status: false,
|
||||||
|
msg: `Table table1 contains invalid structure. ${MORE_INFO}`
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an error message if a row in a table contains an column with undefined value', () => {
|
||||||
|
const validationResult = validateInput({ table1: [{ column: undefined }] })
|
||||||
|
expect(validationResult).toEqual({
|
||||||
|
status: false,
|
||||||
|
msg: `A row in table table1 contains invalid value. Can't assign undefined to column.`
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
90
src/utils/validateInput.ts
Normal file
90
src/utils/validateInput.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
export const MORE_INFO =
|
||||||
|
'For more info see https://sasjs.io/sasjs-adapter/#request-response'
|
||||||
|
export const INVALID_TABLE_STRUCTURE = `Parameter data contains invalid table structure. ${MORE_INFO}`
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function validates the input data structure and table naming convention
|
||||||
|
*
|
||||||
|
* @param data A json object that contains one or more tables, it can also be null
|
||||||
|
* @returns An object which contains two attributes: 1) status: boolean, 2) msg: string
|
||||||
|
*/
|
||||||
|
export const validateInput = (
|
||||||
|
data: { [key: string]: any } | null
|
||||||
|
): {
|
||||||
|
status: boolean
|
||||||
|
msg: string
|
||||||
|
} => {
|
||||||
|
if (data === null) return { status: true, msg: '' }
|
||||||
|
|
||||||
|
if (getType(data) !== 'object') {
|
||||||
|
return {
|
||||||
|
status: false,
|
||||||
|
msg: INVALID_TABLE_STRUCTURE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSasFormatsTable = (key: string) =>
|
||||||
|
key.match(/^\$.*/) && Object.keys(data).includes(key.replace(/^\$/, ''))
|
||||||
|
|
||||||
|
for (const key in data) {
|
||||||
|
if (!key.match(/^[a-zA-Z_]/) && !isSasFormatsTable(key)) {
|
||||||
|
return {
|
||||||
|
status: false,
|
||||||
|
msg: 'First letter of table should be alphabet or underscore.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!key.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/) && !isSasFormatsTable(key)) {
|
||||||
|
return { status: false, msg: 'Table name should be alphanumeric.' }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.length > 32) {
|
||||||
|
return {
|
||||||
|
status: false,
|
||||||
|
msg: 'Maximum length for table name could be 32 characters.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getType(data[key]) !== 'Array' && !isSasFormatsTable(key)) {
|
||||||
|
return {
|
||||||
|
status: false,
|
||||||
|
msg: INVALID_TABLE_STRUCTURE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of data[key]) {
|
||||||
|
if (getType(item) !== 'object') {
|
||||||
|
return {
|
||||||
|
status: false,
|
||||||
|
msg: `Table ${key} contains invalid structure. ${MORE_INFO}`
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const attributes = Object.keys(item)
|
||||||
|
for (const attribute of attributes) {
|
||||||
|
if (item[attribute] === undefined) {
|
||||||
|
return {
|
||||||
|
status: false,
|
||||||
|
msg: `A row in table ${key} contains invalid value. Can't assign undefined to ${attribute}.`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { status: true, msg: '' }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* this function returns the type of variable
|
||||||
|
*
|
||||||
|
* @param data it could be anything, like string, array, object etc.
|
||||||
|
* @returns a string which tells the type of input parameter
|
||||||
|
*/
|
||||||
|
const getType = (data: any): string => {
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
return 'Array'
|
||||||
|
} else {
|
||||||
|
return typeof data
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user