1
0
mirror of https://github.com/sasjs/adapter.git synced 2025-12-15 18:54:36 +00:00

Compare commits

...

113 Commits

Author SHA1 Message Date
Yury Shkoda
122f302bae Merge pull request #728 from sasjs/deps-fix
fix(workflow): added actions/setup-node@v2
2022-06-20 20:44:57 +03:00
Yury Shkoda
c3a0ad1f41 fix(workflow): added actions/setup-node@v2 2022-06-20 20:43:11 +03:00
Yury Shkoda
a28b48f815 Merge pull request #726 from sasjs/deps-fix
fix(workflows): fixed npmpublish workflow
2022-06-20 20:36:13 +03:00
Yury Shkoda
9b6a42e412 fix(workflows): fixed npmpublish workflow 2022-06-20 20:33:44 +03:00
Allan Bowe
db60962c1e Merge pull request #725 from sasjs/allanbowe-patch-1
fix: bumping with README updates
2022-06-20 19:29:15 +02:00
Allan Bowe
1eae59ad3b fix: bumping with README updates 2022-06-20 18:28:47 +01:00
Allan Bowe
d485023d65 Update dependabot.yml 2022-06-20 18:09:07 +01:00
Allan Bowe
c2f21babb4 Merge pull request #723 from sasjs/deps-fix
Regenerated package-lock and fixed linting issues
2022-06-20 19:03:17 +02:00
Yury Shkoda
dd788ae423 chore(lint): fixed linting 2022-06-20 19:48:42 +03:00
Yury Shkoda
a113c95441 chore(deps): added prettier dev dependency 2022-06-20 19:36:12 +03:00
Yury Shkoda
489947bcae chore(lint): fixed linting issues 2022-06-20 19:26:29 +03:00
Yury Shkoda
1596173dda fix(deps): regenerated package-lock 2022-06-20 19:24:09 +03:00
Allan Bowe
bb1b2ddcb2 Merge pull request #719 from sasjs/issue-718
fix: parse the logs before appending the request to request array whe…
2022-06-20 17:53:44 +02:00
a3cc274ef1 chore: no need to appendRequest from then block when there is jobExecutionError 2022-06-10 15:38:03 +05:00
451d0906fa chore: update error message 2022-06-10 15:31:09 +05:00
dd6b89b0d0 fix: parse the logs before appending the request to request array when server type is sasjs 2022-06-09 23:11:47 +05:00
Allan Bowe
f0525c5796 Merge pull request #674 from sasjs/numeric-missing
fix: special missings accept - regular missing .
2022-05-23 14:11:30 +03:00
Allan Bowe
01bcfe176a Merge pull request #712 from sasjs/issue-711
When expires csrf token, re-fetch and empty webout fix
2022-05-23 14:03:42 +03:00
076ed1cc7a chore: added special missing test 2022-05-23 12:59:48 +02:00
0a3289b577 chore(git): Merge branch 'master' into numeric-missing 2022-05-23 12:47:13 +02:00
Allan Bowe
cbb55ff426 chore: updating README in server tests to deploy backend 2022-05-20 17:43:49 +00:00
6d47174a5e fix: csrf token fetch and empty webout promise finish 2022-05-20 16:43:46 +02:00
Allan Bowe
ae13ca523c Merge pull request #474 from sasjs/auto-tests
sasjs-tests automation
2022-05-17 12:17:08 +03:00
843cee4dbe chore: fixed test err handling 2022-05-16 15:42:12 +02:00
Yury Shkoda
1ac88ae102 chore: changed let to const 2022-05-12 10:04:38 +03:00
Yury Shkoda
a79766c00c chore(cypress): removed comment 2022-05-12 09:54:27 +03:00
Yury Shkoda
3bc70b91b8 fix(npm): pined dep versions 2022-05-12 09:49:04 +03:00
Yury Shkoda
b0b1c32180 feat(npm): ignored files not needed for npm package 2022-05-12 09:35:43 +03:00
Yury Shkoda
5b81e0bf4a chore: add blank lines at the end of the files 2022-05-12 09:14:24 +03:00
Yury Shkoda
54a33ac98a chore(lint): fixed lint issues in cypress folder 2022-05-12 09:06:52 +03:00
Yury Shkoda
dd237ceeec feat(lint): add cypress folder to lint scripts 2022-05-12 09:06:05 +03:00
552540fb88 fix: sasjs log not append to requests 2022-05-11 23:00:32 +02:00
6d50d4030e chore(git): Merge branch 'master' into auto-tests 2022-05-11 18:00:03 +02:00
Allan Bowe
8856c3b6ec Merge pull request #708 from sasjs/client-deprecate
ClientID when login to SASJS server deprecation
2022-05-11 17:27:57 +03:00
Saad Jutt
b8ea618f1e fix: removed getAuthCode function 2022-05-11 19:22:01 +05:00
Saad Jutt
10c72e6483 fix(login): making login requet with CSRF for SASJS server 2022-05-11 19:07:30 +05:00
Saad Jutt
9d03b54fba chore: Merge branch 'master' into client-deprecate 2022-05-11 19:02:37 +05:00
Allan Bowe
38ad9abfbc Merge pull request #709 from sasjs/lint-precommit
chore: running lint as pre-commit hook
2022-05-11 15:32:02 +03:00
42d9a85cfa chore: pull request template update 2022-05-11 14:24:09 +02:00
39f14cc5a0 chore: formatting 2022-05-11 12:04:44 +02:00
29a65052dc chore: pre commit hook 2022-05-10 14:54:34 +02:00
63328163ab chore: pre-push run lint 2022-05-10 14:09:41 +02:00
Allan Bowe
aebf4ea8d8 Update pre-commit 2022-05-10 12:22:40 +01:00
Allan Bowe
59e3edf4b3 Update pre-commit 2022-05-10 11:42:35 +01:00
8f309143e9 chore: comment 2022-05-10 12:37:50 +02:00
a65d4257a5 chore: running lint as pre-commit hook 2022-05-10 12:32:05 +02:00
5d2f1d306a style: lint 2022-05-09 16:08:49 +02:00
f7bd63ee7f chore: removing clientid 2022-05-09 16:05:50 +02:00
a4cd320272 chore: utils import fix 2022-05-09 15:47:24 +02:00
c243f25477 fix: deprecating sasjs client id 2022-05-09 13:26:17 +02:00
382a19ccfc chore: ci 2022-05-04 13:19:26 +02:00
acc56e3a53 chore: ci 2022-05-04 13:10:11 +02:00
1d0fb1774a ci: fix 2022-05-04 12:56:56 +02:00
b67340cc70 chore: ci 2022-04-29 16:12:51 +02:00
831023fe1c chore: ci 2022-04-29 15:58:12 +02:00
04f7cdf8ff chore: ci 2022-04-29 15:42:33 +02:00
d01a6a60ad chore: ci 2022-04-29 15:34:45 +02:00
7c17f0f584 chore: ci 2022-04-29 15:25:39 +02:00
092b995d14 ci: test 2022-04-29 15:23:21 +02:00
eb296bf93c chore: ci fix 2022-04-29 14:53:20 +02:00
e2e15ce8e1 fix: sasjs login login callback 2022-04-29 14:26:34 +02:00
3c59db91cd chore(git): Merge branch 'master' into auto-tests 2022-04-29 12:40:51 +02:00
a97b2f43ca chore: sasjs login fix 2022-04-29 12:40:38 +02:00
fdd3a261e5 chore: package-lock refresh 2022-04-29 11:24:24 +02:00
f69e5afaf0 style: lint 2022-04-28 20:48:21 +02:00
7b78f65c4a fix: utils import, tests fix, auto testing script 2022-04-28 20:46:36 +02:00
aabe473ef6 style: lint 2022-04-27 16:26:37 +02:00
8a7d08c3b9 chore(git): Merge branch 'master' into auto-tests 2022-04-27 16:26:02 +02:00
d2ea67e5d6 chore: docs 2022-03-08 18:32:45 +01:00
b0df4cb7ee fix: special missings accept - regular missing . 2022-03-08 18:28:48 +01:00
1e5e803c92 chore(git): Merge branch 'master' into auto-tests 2021-08-16 13:49:38 +02:00
ad34a9a4db chore: sleep removed 2021-08-16 12:12:52 +02:00
11e006741f chore: openvpn version 2021-08-16 11:17:41 +02:00
0bf385d1e0 fix: login issues 2021-07-29 14:27:17 +02:00
b217499b59 chore: addressing comments 2021-07-28 18:18:26 +02:00
d123296359 chore: code comment 2021-07-27 14:55:24 +02:00
50ab866652 style: lint 2021-07-27 14:15:23 +02:00
f1252537a6 chore(git): Merge branch 'fixing-sas9-tests' into auto-tests 2021-07-27 14:12:11 +02:00
b1682b6f32 ci: script test done 2021-07-27 14:07:16 +02:00
ded7990096 fix: viya with web approach adding 2 underscores in front of program param 2021-07-27 14:05:34 +02:00
52e95e3455 ci: script test 2021-07-27 12:38:33 +02:00
1d13252045 ci: script test 2021-07-27 12:19:14 +02:00
f4630309de chore: ci script fixing 2021-07-27 12:18:44 +02:00
27b6e46973 chore(git): Merge branch 'master' into auto-tests 2021-07-26 19:45:23 +02:00
e5262a18d4 chore: sending message to slack if cypress with sasjs-tests fails 2021-07-26 19:38:59 +02:00
cf10e83e91 test: cypress running with debug on and off, testing all request approaches 2021-07-26 19:16:47 +02:00
0bd156141c chore(git): Merge branch 'master' into fixing-sas9-tests 2021-07-26 14:53:52 +02:00
890608a3e8 test: requests for every approach 2021-07-26 14:52:26 +02:00
a615c5fdb6 style: lint 2021-07-24 18:07:17 +02:00
ca7ee83f7f chore: fixing multiple login attempts by adding pause between calling functions 2021-07-24 18:06:15 +02:00
97a530cc66 style: lint 2021-07-22 14:44:13 +02:00
317c8c81a0 chore: JES test disable on SAS9 2021-07-22 13:48:11 +02:00
c87776ca1b chore(git): Merge branch 'master' into fixing-sas9-tests 2021-07-22 13:44:23 +02:00
04032831c3 fix: debug on test & make error and parse log test 2021-07-22 13:43:50 +02:00
c8fb141048 chore: run sasjs testing as a part of npm release 2021-07-20 11:19:41 +02:00
a429581089 test: fix 2021-07-16 12:32:48 +02:00
dd05258dd2 test: fixing 2021-07-16 12:32:35 +02:00
c0e5327e49 chore: cypress fix 2021-07-16 12:09:59 +02:00
ffba2acad0 chore: script fix 2021-07-16 12:02:03 +02:00
64c624bf2e chore: config.json fix 2021-07-16 11:56:15 +02:00
2506d28dd4 chore: script fdix deploy sas9 2021-07-16 11:41:18 +02:00
b6a438d222 chore: script fix 2021-07-16 11:38:17 +02:00
6f24a55a04 chore: script replace in files install timing 2021-07-16 11:35:15 +02:00
55045eb101 chore: script adapter version 2021-07-16 11:33:21 +02:00
f5e367a645 chore: cpn added 2021-07-16 11:29:22 +02:00
017dc3a8b5 chore: script fix 2021-07-16 11:27:53 +02:00
7285a3f40b chore: adding known_hosts 2021-07-16 11:23:49 +02:00
438b39ceca chore: script fix 2021-07-16 11:20:26 +02:00
a39b9ea38f chore: auto deploying sasjs-tests 2021-07-16 11:14:42 +02:00
836c3ba518 chore(git): Merge branch 'master' into auto-tests 2021-07-16 10:22:50 +02:00
2d2b4660dc chore(git): Merge branch 'tests-fixing' into auto-tests 2021-07-16 10:21:13 +02:00
f25035ae1d chore: cypress added and sasjs test written 2021-07-15 14:30:51 +02:00
ff32f648da chore: tests fixing 2021-07-15 13:41:39 +02:00
52 changed files with 17712 additions and 5312 deletions

12
.git-hooks/pre-commit Executable file
View 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

View File

@@ -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: 2

30
.github/vpn/config.ovpn vendored Normal file
View 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

View File

@@ -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 }}

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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]
[![npm](https://img.shields.io/npm/dt/@sasjs/adapter)]() [![npm](https://img.shields.io/npm/dt/@sasjs/adapter)]()
![Snyk Vulnerabilities for npm package](https://img.shields.io/snyk/vulnerabilities/npm/@sasjs/adapter) ![Snyk Vulnerabilities for npm package](https://img.shields.io/snyk/vulnerabilities/npm/@sasjs/adapter)
[![License](https://img.shields.io/apm/l/atomic-design-ui.svg)](/LICENSE) [![License](https://img.shields.io/apm/l/atomic-design-ui.svg)](/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!
@@ -155,7 +153,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 +219,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 */
@@ -314,7 +312,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
View File

@@ -0,0 +1,11 @@
{
"chromeWebSecurity": false,
"defaultCommandTimeout": 20000,
"env": {
"sasjsTestsUrl": "",
"username": "",
"password": "",
"screenshotOnRunFailure": false,
"testingFinishTimeout": 600000
}
}

View 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()
})
})
})
})
})
})
})
})

View 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
View 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
View 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
View 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
}
}
]
}
]
}
}

14516
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -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,6 +61,7 @@
"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": "18.0.0",
@@ -65,6 +71,8 @@
"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
View 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

View File

@@ -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.

File diff suppressed because it is too large Load Diff

View File

@@ -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">

View File

@@ -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')
} }
} }
] ]

View File

@@ -1,9 +1,35 @@
import SASjs from '@sasjs/adapter' import SASjs from '@sasjs/adapter'
import { TestSuite } from '@sasjs/test-framework' import { TestSuite } from '@sasjs/test-framework'
const stringData: any = { table1: [{ col1: 'first col value' }] }
export const computeTests = (adapter: SASjs): TestSuite => ({ export const computeTests = (adapter: SASjs): 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',

View File

@@ -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
} }

View File

@@ -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',

View File

@@ -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)
} }

View File

@@ -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)
}) })
}) })

View File

@@ -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)
} }

View File

@@ -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

View File

@@ -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
} }

View File

@@ -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.')
}) })

View File

@@ -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'

View File

@@ -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.`

View File

@@ -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.')

View 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')
}) })

View File

@@ -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()

View File

@@ -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'
@@ -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,12 @@ export class AuthManager {
} }
private async getNewLoginForm() { private async getNewLoginForm() {
if (this.serverType === ServerType.Sasjs) {
// server will be sending CSRF cookie,
// http client will use it automatically
return this.requestClient.get('/', undefined)
}
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 +257,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 }
} }
@@ -384,5 +358,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)
}

View File

@@ -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
}

View File

@@ -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')
}) })

View File

@@ -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')
}) })

View File

@@ -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)
}) })

View File

@@ -35,7 +35,7 @@ describe('refreshTokensForSasjs', () => {
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).toContain('Error while refreshing tokens')
}) })

View File

@@ -63,7 +63,7 @@ describe('refreshTokensForViya', () => {
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).toContain('Error while refreshing tokens')
}) })

View File

@@ -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
}) })

View File

@@ -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 its 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

View File

@@ -19,6 +19,7 @@ import {
parseSourceCode, parseSourceCode,
createAxiosInstance createAxiosInstance
} from '../utils' } from '../utils'
import { InvalidCsrfError } from '../types/errors/InvalidCsrfError'
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,24 @@ export class RequestClient implements HttpClient {
throw e throw e
} }
if (e instanceof InvalidCsrfError) {
// Fetching root will inject CSRF token in cookie
await this.httpClient
.get('/', {
withCredentials: true
})
.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 +603,17 @@ 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 (response.data.toLowerCase() === 'invalid csrf token!') {
throw new InvalidCsrfError()
}
break break
case 401: case 401:
if (typeof response.data === 'object') { if (typeof response.data === 'object') {

View File

@@ -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)
) )

View File

@@ -54,6 +54,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 }],

View File

@@ -0,0 +1,9 @@
export class InvalidCsrfError extends Error {
constructor() {
const message = 'Invalid CSRF token!'
super(`Auth error: ${message}`)
this.name = 'InvalidCsrfError'
Object.setPrototypeOf(this, InvalidCsrfError.prototype)
}
}

View File

@@ -1,4 +1,4 @@
import { isSpecialMissing } from '@sasjs/utils' import { isSpecialMissing } from '@sasjs/utils/input/validators'
/** /**
* Converts the given JSON object array to a CSV string. * Converts the given JSON object array to a CSV string.

View File

@@ -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()
} }

View File

@@ -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
} }

View File

@@ -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 = ''