1
0
mirror of https://github.com/sasjs/adapter.git synced 2026-01-05 11:40:06 +00:00

Compare commits

...

58 Commits

Author SHA1 Message Date
Yury Shkoda
22edcb0a8e Merge pull request #810 from sasjs/pollJobState-improvements
Poll job state improvements
2023-07-05 11:15:42 +03:00
Yury Shkoda
aedf5c1734 chore: Merge branch 'master' of github.com:sasjs/adapter into pollJobState-improvements 2023-07-05 10:49:12 +03:00
Yury Shkoda
784bd20ee0 Merge pull request #814 from sasjs/issue-811-fixed
Issue 811 fixed
2023-07-05 10:27:04 +03:00
Yury Shkoda
61db1e0609 test: fixed unit tests 2023-06-23 18:04:48 +03:00
Yury Shkoda
5c589a6af3 chore: reverted dev changes to build.yml 2023-06-23 17:52:46 +03:00
Yury Shkoda
275cd6dbd3 chore: debugging 2023-06-23 17:20:16 +03:00
Yury Shkoda
d874e07889 fix(file-uploader): fixed parsing response for SASJS 2023-06-23 16:37:25 +03:00
Yury Shkoda
1648cf28d5 chore: Merge branch 'master' of github.com:sasjs/adapter into issue-811-fixed 2023-06-23 15:26:01 +03:00
a4aaeba31c fix: file upload with sasjs server type, json not parsed 2023-06-22 12:48:40 +02:00
Yury Shkoda
6bf68a315c fix(sasjs-utils): fixed imports 2023-06-22 13:37:07 +03:00
Allan Bowe
c0f78d0c1e fix: updating example.html to use v4 of the adapter 2023-06-22 08:56:32 +01:00
Yury Shkoda
e0aebc169f chore: debugging 2023-06-21 18:28:39 +03:00
Yury Shkoda
9a50e5cb63 chore: debugging 2023-06-21 18:16:20 +03:00
Yury Shkoda
a51923dad7 chore: debugging 2023-06-21 18:01:21 +03:00
Yury Shkoda
9aee77f0e3 chore: debugging 2023-06-21 17:44:24 +03:00
Yury Shkoda
c32d037063 chore: debugging 2023-06-21 17:34:16 +03:00
Yury Shkoda
94f7492c31 chore: debugging 2023-06-21 17:19:13 +03:00
Yury Shkoda
d29e0a0f57 chore(sasjs-tests): bumped node-sass 2023-06-21 16:36:22 +03:00
Yury Shkoda
8d7cc11db5 chore(sasjs-tests): bumped node-sass 2023-06-21 16:35:33 +03:00
Yury Shkoda
28e9d1cc6b test(get-token): covered getTokenRequestErrorPrefix 2023-06-21 16:34:26 +03:00
Yury Shkoda
375cec48ca feat(get-token): improved error prefix 2023-06-21 16:33:45 +03:00
7d826685f7 Merge pull request #813 from sasjs/sasjs-job-execute
fix: issue with parsing json in sasjs job executor
2023-06-15 16:08:08 +02:00
f42f6bca00 fix: issue with parsing json in sasjs job executor 2023-06-14 15:08:11 +02:00
Yury Shkoda
4440e5d1f9 fix(types): fixed PollOptions exports 2023-05-17 14:10:17 +03:00
Yury Shkoda
f484a5a6a1 refactor(poll-job-state): updated types and func attributes 2023-05-17 11:16:35 +03:00
Yury Shkoda
5c74186bab feat(poll-strategy): added subsequentStrategies to PollStrategy 2023-05-16 17:48:04 +03:00
Yury Shkoda
ea68c3dff3 docs(poll-job-state): updated docs 2023-05-16 17:42:27 +03:00
Yury Shkoda
153b285670 chore(poll-job-status): renamed PollOptions to PollStrategy and added docs 2023-05-15 16:32:07 +03:00
Yury Shkoda
f9f4aa5aa6 chore(reviewer-lottery): removed QA group 2023-05-15 14:53:55 +03:00
Yury Shkoda
bd02656b3c docs(poll-job-state): added comments 2023-05-15 14:36:18 +03:00
Yury Shkoda
991519a13d fix(execute-job): added error object if it present 2023-05-15 14:26:24 +03:00
Yury Shkoda
615c9d012e feat(poll-job-state): implemented polling strategies 2023-05-15 14:24:11 +03:00
Yury Shkoda
bd872e0e75 Merge pull request #809 from sasjs/webout-issue
fix(sasjs-request-client): fixed response parsing
2023-05-10 16:26:13 +03:00
Yury Shkoda
a14a1663fc fix(sasjs-request-client): fixed response parsing 2023-05-10 16:07:25 +03:00
Yury Shkoda
2482a0c674 Merge pull request #807 from sasjs/issue-245
fix(deploy-service-pack): fixed redeployment
2023-05-09 17:12:11 +03:00
Yury Shkoda
a19de50e67 fix(create-folder): improved error message 2023-05-09 16:59:18 +03:00
Yury Shkoda
860c9f907c fix(deploy-service-pack): fixed redeployment 2023-05-09 10:43:23 +03:00
Allan Bowe
d7126a6878 Merge pull request #802 from sasjs/fix-ci
chore(ci): install vpn and run sasjs tests
2023-05-04 10:51:34 +01:00
Yury Shkoda
a69ebd0fd3 Merge pull request #806 from sasjs/sasjs-job-execution
fix(job-execution): fixed webout for SASJS server type
2023-05-04 12:42:11 +03:00
Yury Shkoda
0f47326bb6 chore(execute-script): improved code style 2023-05-04 12:21:22 +03:00
Yury Shkoda
2d6efa2437 fix(job-execution): fixed webout for SASJS server type 2023-05-04 10:57:24 +03:00
Yury Shkoda
da7579a2bb Merge pull request #805 from sasjs/webpack-fix
fix(packaging): fixed output file for Node
2023-04-21 17:28:52 +03:00
Yury Shkoda
657e415c0c fix(packaging): fixed output file for Node 2023-04-21 16:02:47 +03:00
bfefdb65a3 chore: addressed comments 2023-04-18 00:45:03 +05:00
bbe52562da ci: sasjs tests 2023-04-10 13:15:41 +02:00
48917bb4d9 ci: sasjs tests 2023-04-10 12:22:28 +02:00
81c3d2a3dc style: lint 2023-04-10 12:10:41 +02:00
0052d5d340 ci: sasjs tests logged in fix 2023-04-10 12:08:01 +02:00
a633bda24d ci: sasjs tests 2023-04-10 10:05:10 +02:00
e951ea0ab2 chore: quick fix 2023-04-08 01:13:11 +05:00
b75ab13eb7 chore: update the command for running cypress 2023-04-07 23:50:11 +05:00
4de34cc8b0 chore: add streaming config and fix apploc 2023-04-07 17:06:08 +05:00
23a789b383 chore: add refresh token to .env.4gl 2023-04-06 23:13:23 +05:00
156f1b1180 chore: create .env.4gl at proper place 2023-04-06 22:43:32 +05:00
491e36d703 chore: install sasjs cli 2023-04-06 22:31:51 +05:00
06b6e48a16 chore: deploy the streaming app to sas9.4gl.io 2023-04-06 22:07:18 +05:00
9bebd356ca chore: deamonize npm start process 2023-04-06 16:28:33 +05:00
171f9bc7b9 chore(ci): install vpn and run sasjs tests locally 2023-04-06 16:03:16 +05:00
45 changed files with 3858 additions and 17034 deletions

View File

@@ -5,7 +5,3 @@ groups:
- YuryShkoda - YuryShkoda
- medjedovicm - medjedovicm
- sabhas - sabhas
- name: SASjs QA
reviewers: 1
usernames:
- VladislavParhomchik

25
.github/vpn/config.ovpn vendored Normal file
View File

@@ -0,0 +1,25 @@
# Client
client
tls-client
dev tun
# this will connect with whatever proto DNS tells us (https://community.openvpn.net/openvpn/ticket/934)
proto tcp
remote vpn.4gl.io 7494
resolv-retry infinite
cipher AES-256-CBC
auth SHA256
script-security 2
keepalive 10 120
remote-cert-tls server
# Keys
ca ca.crt
cert user.crt
key user.key
tls-auth tls.key 1
# Security
nobind
persist-key
persist-tun
verb 3

View File

@@ -12,7 +12,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [lts/fermium] node-version: [lts/hydrogen]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@@ -39,11 +39,33 @@ jobs:
env: env:
CI: true CI: true
- name: Install SSH Key - name: Write VPN Files
uses: shimataro/ssh-key-action@v2 run: |
with: echo "$CA_CRT" > .github/vpn/ca.crt
key: ${{ secrets.DCGITLAB_KEY }} echo "$USER_CRT" > .github/vpn/user.crt
known_hosts: 'placeholder' 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-jammy.list
sudo apt update
sudo apt install openvpn3=17~betaUb22042+jammy
- name: Start Open VPN 3
run: openvpn3 session-start --config .github/vpn/config.ovpn
- name: install pm2
run: npm i -g pm2
- name: Deploy sasjs-tests - name: Deploy sasjs-tests
run: | run: |
@@ -55,12 +77,12 @@ jobs:
replace-in-files --regex='"userName".*' --replacement='"userName":"${{ secrets.SASJS_USERNAME }}",' ./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='"password".*' --replacement='"password":"${{ secrets.SASJS_PASSWORD }}",' ./public/config.json
replace-in-files --regex='"serverType".*' --replacement='"serverType":"SASJS",' ./public/config.json replace-in-files --regex='"serverType".*' --replacement='"serverType":"SASJS",' ./public/config.json
npm run update:adapter && npm run build npm run update:adapter
scp -o stricthostkeychecking=no -r ./build/* ${{ secrets.DCGITLAB_DEPLOY_PATH_VIYA }} pm2 start --name sasjs-test npm -- start
- name: Run cypress on sasjs - name: Run cypress on sasjs
run: | run: |
replace-in-files --regex='"sasjsTestsUrl".*' --replacement='"sasjsTestsUrl":"${{ secrets.SASJS_TEST_URL_VIYA }}",' ./cypress.json replace-in-files --regex='"sasjsTestsUrl".*' --replacement='"sasjsTestsUrl":"http://localhost:3000",' ./cypress.json
replace-in-files --regex='"username".*' --replacement='"username":"${{ secrets.SASJS_USERNAME }}",' ./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 replace-in-files --regex='"password".*' --replacement='"password":"${{ secrets.SASJS_PASSWORD }}",' ./cypress.json
sh ./sasjs-tests/sasjs-cypress-run.sh ${{ secrets.MATRIX_TOKEN }} https://github.com/${{github.repository}}/actions/runs/${{github.run_id}} sh ./sasjs-tests/sasjs-cypress-run.sh ${{ secrets.MATRIX_TOKEN }} https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}

View File

@@ -14,65 +14,83 @@ context('sasjs-tests', function () {
it('Should have all tests successfull', (done) => { it('Should have all tests successfull', (done) => {
cy.get('body').then(($body) => { cy.get('body').then(($body) => {
if ($body.find('input[placeholder="User Name"]').length > 0) { cy.wait(1000).then(() => {
cy.get('input[placeholder="User Name"]').type(username) const startButton = $body.find(
cy.get('input[placeholder="Password"]').type(password) '.ui.massive.icon.primary.left.labeled.button'
cy.get('.submit-button').click() )[0]
}
cy.get('input[placeholder="User Name"]', { timeout: 40000 }) if (
.should('not.exist') !startButton ||
.then(() => { (startButton && !Cypress.dom.isVisible(startButton))
cy.get('.ui.massive.icon.primary.left.labeled.button') ) {
.click() cy.get('input[placeholder="User Name"]').type(username)
.then(() => { cy.get('input[placeholder="Password"]').type(password)
cy.get('.ui.massive.loading.primary.button', { cy.get('.submit-button').click()
timeout: testingFinishTimeout }
})
.should('not.exist') cy.get('input[placeholder="User Name"]', { timeout: 40000 })
.then(() => { .should('not.exist')
cy.get('span.icon.failed') .then(() => {
.should('not.exist') cy.get('.ui.massive.icon.primary.left.labeled.button')
.then(() => { .click()
done() .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) => { it('Should have all tests successfull with debug on', (done) => {
cy.get('body').then(($body) => { cy.get('body').then(($body) => {
if ($body.find('input[placeholder="User Name"]').length > 0) { cy.wait(1000).then(() => {
cy.get('input[placeholder="User Name"]').type(username) const startButton = $body.find(
cy.get('input[placeholder="Password"]').type(password) '.ui.massive.icon.primary.left.labeled.button'
cy.get('.submit-button').click() )[0]
}
cy.get('.ui.fitted.toggle.checkbox label') if (
.click() !startButton ||
.then(() => { (startButton && !Cypress.dom.isVisible(startButton))
cy.get('input[placeholder="User Name"]', { timeout: 40000 }) ) {
.should('not.exist') cy.get('input[placeholder="User Name"]').type(username)
.then(() => { cy.get('input[placeholder="Password"]').type(password)
cy.get('.ui.massive.icon.primary.left.labeled.button') cy.get('.submit-button').click()
.click() }
.then(() => {
cy.get('.ui.massive.loading.primary.button', { cy.get('.ui.fitted.toggle.checkbox label')
timeout: testingFinishTimeout .click()
}) .then(() => {
.should('not.exist') cy.get('input[placeholder="User Name"]', { timeout: 40000 })
.then(() => { .should('not.exist')
cy.get('span.icon.failed') .then(() => {
.should('not.exist') cy.get('.ui.massive.icon.primary.left.labeled.button')
.then(() => { .click()
done() .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,25 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })

20
cypress/support/index.js Normal file
View File

@@ -0,0 +1,20 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')

View File

@@ -1,7 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<script src="https://cdn.jsdelivr.net/combine/npm/chart.js@2.9.3,npm/jquery@3.5.1,npm/@sasjs/adapter@1"></script> <script src="https://cdn.jsdelivr.net/combine/npm/chart.js@2.9.3,npm/jquery@3.5.1,npm/@sasjs/adapter@4"></script>
<script> <script>
var sasJs = new SASjs.default({ var sasJs = new SASjs.default({
appLoc: "/Public/app/readme" appLoc: "/Public/app/readme"

14
package-lock.json generated
View File

@@ -32,7 +32,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.5", "pem": "1.14.5",
"prettier": "2.7.1", "prettier": "2.8.7",
"process": "0.11.10", "process": "0.11.10",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"semantic-release": "19.0.3", "semantic-release": "19.0.3",
@@ -13925,9 +13925,9 @@
} }
}, },
"node_modules/prettier": { "node_modules/prettier": {
"version": "2.7.1", "version": "2.8.7",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz",
"integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", "integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==",
"dev": true, "dev": true,
"bin": { "bin": {
"prettier": "bin-prettier.js" "prettier": "bin-prettier.js"
@@ -27416,9 +27416,9 @@
"dev": true "dev": true
}, },
"prettier": { "prettier": {
"version": "2.7.1", "version": "2.8.7",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz",
"integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", "integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==",
"dev": true "dev": true
}, },
"pretty-bytes": { "pretty-bytes": {

View File

@@ -60,7 +60,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.5", "pem": "1.14.5",
"prettier": "2.7.1", "prettier": "2.8.7",
"process": "0.11.10", "process": "0.11.10",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"semantic-release": "19.0.3", "semantic-release": "19.0.3",

View File

@@ -13,10 +13,7 @@
# misc # misc
.DS_Store .DS_Store
.env.local .env.*
.env.development.local
.env.test.local
.env.production.local
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*

File diff suppressed because it is too large Load Diff

View File

@@ -4,15 +4,14 @@
"homepage": ".", "homepage": ".",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@sasjs/adapter": "file:../build/sasjs-adapter-5.0.0.tgz",
"@sasjs/test-framework": "1.5.7", "@sasjs/test-framework": "1.5.7",
"@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": "^16.0.1",
"@types/react-dom": "^17.0.0", "@types/react-dom": "^16.0.0",
"@types/react-router-dom": "^5.1.7", "@types/react-router-dom": "^5.1.7",
"react": "^17.0.1", "react": "^16.0.1",
"react-dom": "^17.0.1", "react-dom": "^16.0.1",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-scripts": "^5.0.1", "react-scripts": "^5.0.1",
"typescript": "^4.1.3" "typescript": "^4.1.3"
@@ -22,7 +21,7 @@
"build": "react-scripts build", "build": "react-scripts build",
"test": "react-scripts test", "test": "react-scripts test",
"eject": "react-scripts eject", "eject": "react-scripts eject",
"update:adapter": "cd .. && npm run package:lib && cd sasjs-tests && npm i ../build/sasjs-adapter-5.0.0.tgz --legacy-peer-deps", "update:adapter": "cd .. && npm run package:lib && cd sasjs-tests && npm i ../build/sasjs-adapter-5.0.0.tgz",
"deploy:tests": "rsync -avhe ssh ./build/* --delete $SSH_ACCOUNT:$DEPLOY_PATH || npm run deploy:tests-win", "deploy:tests": "rsync -avhe ssh ./build/* --delete $SSH_ACCOUNT:$DEPLOY_PATH || npm run deploy:tests-win",
"deploy:tests-win": "scp %DEPLOY_PATH% ./build/*", "deploy:tests-win": "scp %DEPLOY_PATH% ./build/*",
"deploy": "npm run update:adapter && npm run build && npm run deploy:tests" "deploy": "npm run update:adapter && npm run build && npm run deploy:tests"
@@ -43,6 +42,6 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"node-sass": "7.0.3" "node-sass": "9.0.0"
} }
} }

View File

@@ -3,10 +3,10 @@
"password": "", "password": "",
"sasJsConfig": { "sasJsConfig": {
"serverUrl": "", "serverUrl": "",
"appLoc": "/Public/app/adapter-tests", "appLoc": "/Public/app/adapter-tests/services",
"serverType": "SASJS", "serverType": "SASJS",
"debug": false, "debug": false,
"contextName": "sasjs adapter compute context", "contextName": "sasjs adapter compute context",
"useComputeApi": true "useComputeApi": true
} }
} }

View File

@@ -1,17 +1,29 @@
{ {
"$schema": "https://cli.sasjs.io/sasjsconfig-schema.json", "$schema": "https://cli.sasjs.io/sasjsconfig-schema.json",
"serviceConfig": { "serviceConfig": {
"serviceFolders": [ "serviceFolders": ["sasjs/common"]
"sasjs/common"
]
}, },
"defaultTarget": "4gl", "defaultTarget": "4gl",
"targets": [ "targets": [
{ {
"name": "4gl", "name": "4gl",
"serverType": "SASJS",
"serverUrl": "https://sas9.4gl.io", "serverUrl": "https://sas9.4gl.io",
"appLoc": "/Public/app/adapter-tests" "serverType": "SASJS",
"httpsAgentOptions": {
"allowInsecureRequests": false
},
"appLoc": "/Public/app/adapter-tests",
"deployConfig": {
"deployServicePack": true,
"deployScripts": []
},
"streamConfig": {
"streamWeb": true,
"streamWebFolder": "webv",
"webSourcePath": "build",
"streamServiceName": "adapter-tests",
"assetPaths": []
}
} }
] ]
} }

View File

@@ -29,11 +29,33 @@ describe('SASViyaApiClient', () => {
jest jest
.spyOn(requestClient, 'get') .spyOn(requestClient, 'get')
.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: any) => e) .catch((e: any) => e)
expect(error).toBeInstanceOf(RootFolderNotFoundError) expect(error).toBeInstanceOf(RootFolderNotFoundError)
}) })
it('should throw an error when ', async () => {
const testMessage1 = 'test message 1'
const testMessage2 = 'test message 2.'
jest.spyOn(requestClient, 'post').mockImplementation(() => {
return Promise.reject({
message: testMessage1,
response: { data: { message: testMessage2 }, status: 409 }
})
})
const error = await sasViyaApiClient
.createFolder('test', '/foo')
.catch((e: any) => e)
const expectedError = `${testMessage1}. ${testMessage2} To override, please set "isForced" to "true".`
expect(error).toEqual(expectedError)
})
}) })
const setupMocks = () => { const setupMocks = () => {

View File

@@ -29,6 +29,12 @@ import { executeScript } from './api/viya/executeScript'
import { getAccessTokenForViya } from './auth/getAccessTokenForViya' import { getAccessTokenForViya } from './auth/getAccessTokenForViya'
import { refreshTokensForViya } from './auth/refreshTokensForViya' import { refreshTokensForViya } from './auth/refreshTokensForViya'
interface JobExecutionResult {
result?: { result: object }
log?: string
error?: object
}
/** /**
* A client for interfacing with the SAS Viya REST API. * A client for interfacing with the SAS Viya REST API.
* *
@@ -270,7 +276,7 @@ export class SASViyaApiClient {
* @param debug - when set to true, the log will be returned. * @param debug - when set to true, the log will be returned.
* @param expectWebout - when set to true, the automatic _webout fileref will be checked for content, and that content returned. This fileref is used when the Job contains a SASjs web request (as opposed to executing arbitrary SAS code). * @param expectWebout - when set to true, the automatic _webout fileref will be checked for content, and that content returned. This fileref is used when the Job contains a SASjs web request (as opposed to executing arbitrary SAS code).
* @param waitForResult - when set to true, function will return the session * @param waitForResult - when set to true, function will return the session
* @param pollOptions - an object that represents poll interval(milliseconds) and maximum amount of attempts. Object example: { MAX_POLL_COUNT: 24 * 60 * 60, POLL_INTERVAL: 1000 }. * @param pollOptions - an object that represents poll interval(milliseconds) and maximum amount of attempts. Object example: { maxPollCount: 24 * 60 * 60, pollInterval: 1000 }. More information available at src/api/viya/pollJobState.ts.
* @param printPid - a boolean that indicates whether the function should print (PID) of the started job. * @param printPid - a boolean that indicates whether the function should print (PID) of the started job.
* @param variables - an object that represents macro variables. * @param variables - an object that represents macro variables.
*/ */
@@ -378,12 +384,14 @@ export class SASViyaApiClient {
isForced?: boolean isForced?: boolean
): Promise<Folder> { ): Promise<Folder> {
const logger = process.logger || console const logger = process.logger || console
if (!parentFolderPath && !parentFolderUri) { if (!parentFolderPath && !parentFolderUri) {
throw new Error('Path or URI of the parent folder is required.') throw new Error('Path or URI of the parent folder is required.')
} }
if (!parentFolderUri && parentFolderPath) { if (!parentFolderUri && parentFolderPath) {
parentFolderUri = await this.getFolderUri(parentFolderPath, accessToken) parentFolderUri = await this.getFolderUri(parentFolderPath, accessToken)
if (!parentFolderUri) { if (!parentFolderUri) {
logger.info( logger.info(
`Parent folder at path '${parentFolderPath}' is not present.` `Parent folder at path '${parentFolderPath}' is not present.`
@@ -394,6 +402,7 @@ export class SASViyaApiClient {
parentFolderPath.lastIndexOf('/') parentFolderPath.lastIndexOf('/')
) )
const newFolderName = `${parentFolderPath.split('/').pop()}` const newFolderName = `${parentFolderPath.split('/').pop()}`
if (newParentFolderPath === '') { if (newParentFolderPath === '') {
throw new RootFolderNotFoundError( throw new RootFolderNotFoundError(
parentFolderPath, parentFolderPath,
@@ -401,20 +410,24 @@ export class SASViyaApiClient {
accessToken accessToken
) )
} }
logger.info( logger.info(
`Creating parent folder:\n'${newFolderName}' in '${newParentFolderPath}'` `Creating parent folder:\n'${newFolderName}' in '${newParentFolderPath}'`
) )
const parentFolder = await this.createFolder( const parentFolder = await this.createFolder(
newFolderName, newFolderName,
newParentFolderPath, newParentFolderPath,
undefined, undefined,
accessToken accessToken
) )
logger.info( logger.info(
`Parent folder '${newFolderName}' has been successfully created.` `Parent folder '${newFolderName}' has been successfully created.`
) )
parentFolderUri = `/folders/folders/${parentFolder.id}` parentFolderUri = `/folders/folders/${parentFolder.id}`
} else if (isForced && accessToken) { } else if (isForced) {
const folderPath = parentFolderPath + '/' + folderName const folderPath = parentFolderPath + '/' + folderName
const folderUri = await this.getFolderUri(folderPath, accessToken) const folderUri = await this.getFolderUri(folderPath, accessToken)
@@ -427,8 +440,8 @@ export class SASViyaApiClient {
} }
} }
const { result: createFolderResponse } = const { result: createFolderResponse } = await this.requestClient
await this.requestClient.post<Folder>( .post<Folder>(
`/folders/folders?parentFolderUri=${parentFolderUri}`, `/folders/folders?parentFolderUri=${parentFolderUri}`,
{ {
name: folderName, name: folderName,
@@ -436,12 +449,34 @@ export class SASViyaApiClient {
}, },
accessToken accessToken
) )
.catch((err) => {
const { message, response } = err
if (message && response && response.data && response.data.message) {
const { status } = response
const { message: responseMessage } = response.data
const messages = [message, responseMessage].map((mes: string) =>
/\.$/.test(mes) ? mes : `${mes}.`
)
if (!isForced && status === 409) {
messages.push(`To override, please set "isForced" to "true".`)
}
const errMessage = messages.join(' ')
throw errMessage
}
throw err
})
// update folder map with newly created folder. // update folder map with newly created folder.
await this.populateFolderMap( await this.populateFolderMap(
`${parentFolderPath}/${folderName}`, `${parentFolderPath}/${folderName}`,
accessToken accessToken
) )
return createFolderResponse return createFolderResponse
} }
@@ -592,7 +627,7 @@ export class SASViyaApiClient {
* @param accessToken - an optional access token for an authorized user. * @param accessToken - an optional access token for an authorized user.
* @param waitForResult - a boolean indicating if the function should wait for a result. * @param waitForResult - a boolean indicating if the function should wait for a result.
* @param expectWebout - a boolean indicating whether to expect a _webout response. * @param expectWebout - a boolean indicating whether to expect a _webout response.
* @param pollOptions - an object that represents poll interval(milliseconds) and maximum amount of attempts. Object example: { MAX_POLL_COUNT: 24 * 60 * 60, POLL_INTERVAL: 1000 }. * @param pollOptions - an object that represents poll interval(milliseconds) and maximum amount of attempts. Object example: { maxPollCount: 24 * 60 * 60, pollInterval: 1000 }. More information available at src/api/viya/pollJobState.ts.
* @param printPid - a boolean that indicates whether the function should print (PID) of the started job. * @param printPid - a boolean that indicates whether the function should print (PID) of the started job.
* @param variables - an object that represents macro variables. * @param variables - an object that represents macro variables.
*/ */
@@ -703,11 +738,13 @@ export class SASViyaApiClient {
debug: boolean, debug: boolean,
data?: any, data?: any,
authConfig?: AuthConfig authConfig?: AuthConfig
) { ): Promise<JobExecutionResult> {
let access_token = (authConfig || {}).access_token let access_token = (authConfig || {}).access_token
if (authConfig) { if (authConfig) {
;({ access_token } = await getTokens(this.requestClient, authConfig)) ;({ access_token } = await getTokens(this.requestClient, authConfig))
} }
if (isRelativePath(sasJob) && !this.rootFolderName) { if (isRelativePath(sasJob) && !this.rootFolderName) {
throw new Error( throw new Error(
'Relative paths cannot be used without specifying a root folder name.' 'Relative paths cannot be used without specifying a root folder name.'
@@ -720,6 +757,7 @@ export class SASViyaApiClient {
const fullFolderPath = isRelativePath(sasJob) const fullFolderPath = isRelativePath(sasJob)
? `${this.rootFolderName}/${folderPath}` ? `${this.rootFolderName}/${folderPath}`
: folderPath : folderPath
await this.populateFolderMap(fullFolderPath, access_token) await this.populateFolderMap(fullFolderPath, access_token)
const jobFolder = this.folderMap.get(fullFolderPath) const jobFolder = this.folderMap.get(fullFolderPath)
@@ -736,9 +774,8 @@ export class SASViyaApiClient {
files = await this.uploadTables(data, access_token) files = await this.uploadTables(data, access_token)
} }
if (!jobToExecute) { if (!jobToExecute) throw new Error(`Job was not found.`)
throw new Error(`Job was not found.`)
}
const jobDefinitionLink = jobToExecute?.links.find( const jobDefinitionLink = jobToExecute?.links.find(
(l) => l.rel === 'getResource' (l) => l.rel === 'getResource'
)?.href )?.href
@@ -778,16 +815,19 @@ export class SASViyaApiClient {
jobDefinition, jobDefinition,
arguments: jobArguments arguments: jobArguments
} }
const { result: postedJob } = await this.requestClient.post<Job>( const { result: postedJob } = await this.requestClient.post<Job>(
`${this.serverUrl}/jobExecution/jobs?_action=wait`, `${this.serverUrl}/jobExecution/jobs?_action=wait`,
postJobRequestBody, postJobRequestBody,
access_token access_token
) )
const jobStatus = await this.pollJobState(postedJob, authConfig).catch( const jobStatus = await this.pollJobState(postedJob, authConfig).catch(
(err) => { (err) => {
throw prefixMessage(err, 'Error while polling job status. ') throw prefixMessage(err, 'Error while polling job status. ')
} }
) )
const { result: currentJob } = await this.requestClient.get<Job>( const { result: currentJob } = await this.requestClient.get<Job>(
`${this.serverUrl}/jobExecution/jobs/${postedJob.id}`, `${this.serverUrl}/jobExecution/jobs/${postedJob.id}`,
access_token access_token
@@ -798,6 +838,7 @@ export class SASViyaApiClient {
const resultLink = currentJob.results['_webout.json'] const resultLink = currentJob.results['_webout.json']
const logLink = currentJob.links.find((l) => l.rel === 'log') const logLink = currentJob.links.find((l) => l.rel === 'log')
if (resultLink) { if (resultLink) {
jobResult = await this.requestClient.get<any>( jobResult = await this.requestClient.get<any>(
`${this.serverUrl}${resultLink}/content`, `${this.serverUrl}${resultLink}/content`,
@@ -805,11 +846,13 @@ export class SASViyaApiClient {
'text/plain' 'text/plain'
) )
} }
if (debug && logLink) { if (debug && logLink) {
log = await this.requestClient log = await this.requestClient
.get<any>(`${this.serverUrl}${logLink.href}/content`, access_token) .get<any>(`${this.serverUrl}${logLink.href}/content`, access_token)
.then((res: any) => res.result.items.map((i: any) => i.line).join('\n')) .then((res: any) => res.result.items.map((i: any) => i.line).join('\n'))
} }
if (jobStatus === 'failed') { if (jobStatus === 'failed') {
throw new JobExecutionError( throw new JobExecutionError(
currentJob.error?.errorCode, currentJob.error?.errorCode,
@@ -817,7 +860,16 @@ export class SASViyaApiClient {
log log
) )
} }
return { result: jobResult?.result, log }
const executionResult: JobExecutionResult = {
result: jobResult?.result,
log
}
const { error } = currentJob
if (error) executionResult.error = error
return executionResult
} }
private async populateFolderMap(folderPath: string, accessToken?: string) { private async populateFolderMap(folderPath: string, accessToken?: string) {
@@ -900,7 +952,7 @@ export class SASViyaApiClient {
return `/folders/folders/${folderDetails.id}` return `/folders/folders/${folderDetails.id}`
} }
private async getRecycleBinUri(accessToken: string) { private async getRecycleBinUri(accessToken?: string) {
const url = '/folders/folders/@myRecycleBin' const url = '/folders/folders/@myRecycleBin'
const { result: folder } = await this.requestClient const { result: folder } = await this.requestClient
@@ -984,7 +1036,7 @@ export class SASViyaApiClient {
sourceFolder: string, sourceFolder: string,
targetParentFolder: string, targetParentFolder: string,
targetFolderName: string, targetFolderName: string,
accessToken: string accessToken?: string
) { ) {
// If target path is an existing folder, than keep source folder name, othervise rename it with given target folder name // If target path is an existing folder, than keep source folder name, othervise rename it with given target folder name
const sourceFolderName = sourceFolder.split('/').pop() as string const sourceFolderName = sourceFolder.split('/').pop() as string
@@ -1051,7 +1103,7 @@ export class SASViyaApiClient {
* @param folderPath - the full path (eg `/Public/example/deleteThis`) of the folder to be deleted. * @param folderPath - the full path (eg `/Public/example/deleteThis`) of the folder to be deleted.
* @param accessToken - an access token for authorizing the request. * @param accessToken - an access token for authorizing the request.
*/ */
public async deleteFolder(folderPath: string, accessToken: string) { public async deleteFolder(folderPath: string, accessToken?: string) {
const recycleBinUri = await this.getRecycleBinUri(accessToken) const recycleBinUri = await this.getRecycleBinUri(accessToken)
const folderName = folderPath.split('/').pop() || '' const folderName = folderPath.split('/').pop() || ''
const date = new Date() const date = new Date()

View File

@@ -337,13 +337,16 @@ export default class SASjs {
sasApiClient?: SASViyaApiClient, sasApiClient?: SASViyaApiClient,
isForced?: boolean isForced?: boolean
) { ) {
if (sasApiClient) if (sasApiClient) {
return await sasApiClient.createFolder( return await sasApiClient.createFolder(
folderName, folderName,
parentFolderPath, parentFolderPath,
parentFolderUri, parentFolderUri,
accessToken accessToken,
isForced
) )
}
return await this.sasViyaApiClient!.createFolder( return await this.sasViyaApiClient!.createFolder(
folderName, folderName,
parentFolderPath, parentFolderPath,
@@ -783,13 +786,11 @@ export default class SASjs {
this.isMethodSupported('deployServicePack', [ServerType.SasViya]) this.isMethodSupported('deployServicePack', [ServerType.SasViya])
let sasApiClient: any = null let sasApiClient: any = null
if (serverUrl || appLoc) { if (serverUrl || appLoc) {
if (!serverUrl) { if (!serverUrl) serverUrl = this.sasjsConfig.serverUrl
serverUrl = this.sasjsConfig.serverUrl if (!appLoc) appLoc = this.sasjsConfig.appLoc
}
if (!appLoc) {
appLoc = this.sasjsConfig.appLoc
}
if (this.sasjsConfig.serverType === ServerType.SasViya) { if (this.sasjsConfig.serverType === ServerType.SasViya) {
sasApiClient = new SASViyaApiClient( sasApiClient = new SASViyaApiClient(
serverUrl, serverUrl,
@@ -807,11 +808,13 @@ export default class SASjs {
} }
} else { } else {
let sasClientConfig: any = null let sasClientConfig: any = null
if (this.sasjsConfig.serverType === ServerType.SasViya) { if (this.sasjsConfig.serverType === ServerType.SasViya) {
sasClientConfig = this.sasViyaApiClient!.getConfig() sasClientConfig = this.sasViyaApiClient!.getConfig()
} else if (this.sasjsConfig.serverType === ServerType.Sas9) { } else if (this.sasjsConfig.serverType === ServerType.Sas9) {
sasClientConfig = this.sas9ApiClient!.getConfig() sasClientConfig = this.sas9ApiClient!.getConfig()
} }
serverUrl = sasClientConfig.serverUrl serverUrl = sasClientConfig.serverUrl
appLoc = sasClientConfig.rootFolderName as string appLoc = sasClientConfig.rootFolderName as string
} }
@@ -848,7 +851,7 @@ export default class SASjs {
* @param authConfig - a valid client, secret, refresh and access tokens that are authorised to execute compute jobs. * @param authConfig - a valid client, secret, refresh and access tokens that are authorised to execute compute jobs.
* The access token is not required when the user is authenticated via the browser. * The access token is not required when the user is authenticated via the browser.
* @param waitForResult - a boolean that indicates whether the function needs to wait for execution to complete. * @param waitForResult - a boolean that indicates whether the function needs to wait for execution to complete.
* @param pollOptions - an object that represents poll interval(milliseconds) and maximum amount of attempts. Object example: { MAX_POLL_COUNT: 24 * 60 * 60, POLL_INTERVAL: 1000 }. * @param pollOptions - an object that represents poll interval(milliseconds) and maximum amount of attempts. Object example: { maxPollCount: 24 * 60 * 60, pollInterval: 1000 }. More information available at src/api/viya/pollJobState.ts.
* @param printPid - a boolean that indicates whether the function should print (PID) of the started job. * @param printPid - a boolean that indicates whether the function should print (PID) of the started job.
* @param variables - an object that represents macro variables. * @param variables - an object that represents macro variables.
*/ */

View File

@@ -1,11 +1,24 @@
import * as NodeFormData from 'form-data' import * as NodeFormData from 'form-data'
import { AuthConfig, ServerType, ServicePackSASjs } from '@sasjs/utils/types' import { AuthConfig, ServerType, ServicePackSASjs } from '@sasjs/utils/types'
import { prefixMessage } from '@sasjs/utils/error'
import { ExecutionQuery } from './types' 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 { getTokens } from './auth/getTokens' import { getTokens } from './auth/getTokens'
// TODO: move to sasjs/utils
export interface SASjsAuthResponse {
access_token: string
refresh_token: string
}
export interface ScriptExecutionResult {
log: string
webout?: string
printOutput?: string
}
export class SASjsApiClient { export class SASjsApiClient {
constructor(private requestClient: RequestClient) {} constructor(private requestClient: RequestClient) {}
@@ -118,18 +131,28 @@ export class SASjsApiClient {
code: string, code: string,
runTime: string = 'sas', runTime: string = 'sas',
authConfig?: AuthConfig authConfig?: AuthConfig
) { ): Promise<ScriptExecutionResult> {
const access_token = await this.getAccessTokenForRequest(authConfig) const access_token = await this.getAccessTokenForRequest(authConfig)
const executionResult: ScriptExecutionResult = { log: '' }
let parsedSasjsServerLog = ''
await this.requestClient await this.requestClient
.post('SASjsApi/code/execute', { code, runTime }, access_token) .post('SASjsApi/code/execute', { code, runTime }, access_token)
.then((res: any) => { .then((res: any) => {
if (res.log) parsedSasjsServerLog = res.log const { log, printOutput, result: webout } = res
executionResult.log = log
if (printOutput) executionResult.printOutput = printOutput
if (webout) executionResult.webout = webout
})
.catch((err) => {
throw prefixMessage(
err,
'Error while sending POST request to execute code. '
)
}) })
return parsedSasjsServerLog return executionResult
} }
/** /**
@@ -152,9 +175,3 @@ export class SASjsApiClient {
return refreshTokensForSasjs(this.requestClient, refreshToken) return refreshTokensForSasjs(this.requestClient, refreshToken)
} }
} }
// todo move to sasjs/utils
export interface SASjsAuthResponse {
access_token: string
refresh_token: string
}

View File

@@ -12,7 +12,7 @@ import { RequestClient } from '../../request/RequestClient'
import { SessionManager } from '../../SessionManager' import { SessionManager } from '../../SessionManager'
import { isRelativePath, fetchLogByChunks } from '../../utils' import { isRelativePath, fetchLogByChunks } from '../../utils'
import { formatDataForRequest } from '../../utils/formatDataForRequest' import { formatDataForRequest } from '../../utils/formatDataForRequest'
import { pollJobState } from './pollJobState' import { pollJobState, JobState } from './pollJobState'
import { uploadTables } from './uploadTables' import { uploadTables } from './uploadTables'
/** /**
@@ -25,7 +25,7 @@ import { uploadTables } from './uploadTables'
* @param debug - when set to true, the log will be returned. * @param debug - when set to true, the log will be returned.
* @param expectWebout - when set to true, the automatic _webout fileref will be checked for content, and that content returned. This fileref is used when the Job contains a SASjs web request (as opposed to executing arbitrary SAS code). * @param expectWebout - when set to true, the automatic _webout fileref will be checked for content, and that content returned. This fileref is used when the Job contains a SASjs web request (as opposed to executing arbitrary SAS code).
* @param waitForResult - when set to true, function will return the session * @param waitForResult - when set to true, function will return the session
* @param pollOptions - an object that represents poll interval(milliseconds) and maximum amount of attempts. Object example: { MAX_POLL_COUNT: 24 * 60 * 60, POLL_INTERVAL: 1000 }. * @param pollOptions - an object that represents poll interval(milliseconds) and maximum amount of attempts. Object example: { maxPollCount: 24 * 60 * 60, pollInterval: 1000 }. More information available at src/api/viya/pollJobState.ts.
* @param printPid - a boolean that indicates whether the function should print (PID) of the started job. * @param printPid - a boolean that indicates whether the function should print (PID) of the started job.
* @param variables - an object that represents macro variables. * @param variables - an object that represents macro variables.
*/ */
@@ -228,7 +228,7 @@ export async function executeScript(
) )
} }
if (jobStatus === 'failed' || jobStatus === 'error') { if (jobStatus === JobState.Failed || jobStatus === JobState.Error) {
throw new ComputeJobExecutionError(currentJob, log) throw new ComputeJobExecutionError(currentJob, log)
} }

View File

@@ -1,29 +1,88 @@
import { AuthConfig } from '@sasjs/utils/types' import { AuthConfig } from '@sasjs/utils/types'
import { Job, PollOptions } from '../..' import { Job, PollOptions, PollStrategy } from '../..'
import { getTokens } from '../../auth/getTokens' import { getTokens } from '../../auth/getTokens'
import { RequestClient } from '../../request/RequestClient' import { RequestClient } from '../../request/RequestClient'
import { JobStatePollError } from '../../types/errors' import { JobStatePollError } from '../../types/errors'
import { Link, WriteStream } from '../../types' import { Link, WriteStream } from '../../types'
import { delay, isNode } from '../../utils' import { delay, isNode } from '../../utils'
export enum JobState {
Completed = 'completed',
Running = 'running',
Pending = 'pending',
Unavailable = 'unavailable',
NoState = '',
Failed = 'failed',
Error = 'error'
}
/**
* Polls job status using default or provided poll options.
* @param requestClient - the pre-configured HTTP request client.
* @param postedJob - the relative or absolute path to the job.
* @param debug - sets the _debug flag in the job arguments.
* @param authConfig - an access token, refresh token, client and secret for an authorized user.
* @param pollOptions - an object containing maxPollCount, pollInterval, streamLog and logFolderPath. It will override the first default poll options in poll strategy if provided.
* Example pollOptions:
* {
* maxPollCount: 200,
* pollInterval: 300,
* streamLog: true, // optional, equals to false by default.
* pollStrategy?: // optional array of poll options that should be applied after 'maxPollCount' of the provided poll options is reached. If not provided the default (see example below) poll strategy will be used.
* }
* Example pollStrategy (values used from default poll strategy):
* [
* { maxPollCount: 200, pollInterval: 300 }, // approximately ~2 mins (including time to get response (~300ms))
* { maxPollCount: 300, pollInterval: 3000 }, // approximately ~5.5 mins (including time to get response (~300ms))
* { maxPollCount: 500, pollInterval: 30000 }, // approximately ~50.5 mins (including time to get response (~300ms))
* { maxPollCount: 3400, pollInterval: 60000 } // approximately ~3015 mins (~125 hours) (including time to get response (~300ms))
* ]
* @returns - a promise which resolves with a job state
*/
export async function pollJobState( export async function pollJobState(
requestClient: RequestClient, requestClient: RequestClient,
postedJob: Job, postedJob: Job,
debug: boolean, debug: boolean,
authConfig?: AuthConfig, authConfig?: AuthConfig,
pollOptions?: PollOptions pollOptions?: PollOptions
) { ): Promise<JobState> {
const logger = process.logger || console const logger = process.logger || console
let pollInterval = 300 const streamLog = pollOptions?.streamLog || false
let maxPollCount = 1000
const defaultPollOptions: PollOptions = { const defaultPollStrategy: PollStrategy = [
maxPollCount, { maxPollCount: 200, pollInterval: 300 },
pollInterval, { maxPollCount: 300, pollInterval: 3000 },
streamLog: false { maxPollCount: 500, pollInterval: 30000 },
{ maxPollCount: 3400, pollInterval: 60000 }
]
let pollStrategy: PollStrategy
if (pollOptions !== undefined) {
pollStrategy = [pollOptions]
let { pollStrategy: providedPollStrategy } = pollOptions
if (providedPollStrategy !== undefined) {
validatePollStrategies(providedPollStrategy)
// INFO: sort by 'maxPollCount'
providedPollStrategy = providedPollStrategy.sort(
(strategyA: PollOptions, strategyB: PollOptions) =>
strategyA.maxPollCount - strategyB.maxPollCount
)
pollStrategy = [...pollStrategy, ...providedPollStrategy]
} else {
pollStrategy = [...pollStrategy, ...defaultPollStrategy]
}
} else {
pollStrategy = defaultPollStrategy
} }
let defaultPollOptions: PollOptions = pollStrategy.splice(0, 1)[0]
pollOptions = { ...defaultPollOptions, ...(pollOptions || {}) } pollOptions = { ...defaultPollOptions, ...(pollOptions || {}) }
const stateLink = postedJob.links.find((l: any) => l.rel === 'state') const stateLink = postedJob.links.find((l: any) => l.rel === 'state')
@@ -31,10 +90,10 @@ export async function pollJobState(
throw new Error(`Job state link was not found.`) throw new Error(`Job state link was not found.`)
} }
let currentState = await getJobState( let currentState: JobState = await getJobState(
requestClient, requestClient,
postedJob, postedJob,
'', JobState.NoState,
debug, debug,
authConfig authConfig
).catch((err) => { ).catch((err) => {
@@ -42,73 +101,71 @@ export async function pollJobState(
`Error fetching job state from ${stateLink.href}. Starting poll, assuming job to be running.`, `Error fetching job state from ${stateLink.href}. Starting poll, assuming job to be running.`,
err err
) )
return 'unavailable'
return JobState.Unavailable
}) })
let pollCount = 0 let pollCount = 0
if (currentState === 'completed') { if (currentState === JobState.Completed) {
return Promise.resolve(currentState) return Promise.resolve(currentState)
} }
let logFileStream let logFileStream
if (pollOptions.streamLog && isNode()) { if (streamLog && isNode()) {
const { getFileStream } = require('./getFileStream') const { getFileStream } = require('./getFileStream')
logFileStream = await getFileStream(postedJob, pollOptions.logFolderPath) logFileStream = await getFileStream(postedJob, pollOptions.logFolderPath)
} }
// Poll up to the first 100 times with the specified poll interval
let result = await doPoll( let result = await doPoll(
requestClient, requestClient,
postedJob, postedJob,
currentState, currentState,
debug, debug,
pollCount, pollCount,
pollOptions,
authConfig, authConfig,
{ streamLog,
...pollOptions,
maxPollCount:
pollOptions.maxPollCount <= 100 ? pollOptions.maxPollCount : 100
},
logFileStream logFileStream
) )
currentState = result.state currentState = result.state
pollCount = result.pollCount pollCount = result.pollCount
if (!needsRetry(currentState) || pollCount >= pollOptions.maxPollCount) { if (
!needsRetry(currentState) ||
(pollCount >= pollOptions.maxPollCount && !pollStrategy.length)
) {
return currentState return currentState
} }
// If we get to this point, this is a long-running job that needs longer polling. // INFO: If we get to this point, this is a long-running job that needs longer polling.
// We will resume polling with a bigger interval of 1 minute // We will resume polling with a bigger interval according to the next polling strategy
let longJobPollOptions: PollOptions = { while (pollStrategy.length && needsRetry(currentState)) {
maxPollCount: 24 * 60, defaultPollOptions = pollStrategy.splice(0, 1)[0]
pollInterval: 60000,
streamLog: false if (pollOptions) {
} defaultPollOptions.logFolderPath = pollOptions.logFolderPath
if (pollOptions) { }
longJobPollOptions.streamLog = pollOptions.streamLog
longJobPollOptions.logFolderPath = pollOptions.logFolderPath result = await doPoll(
requestClient,
postedJob,
currentState,
debug,
pollCount,
defaultPollOptions,
authConfig,
streamLog,
logFileStream
)
currentState = result.state
pollCount = result.pollCount
} }
result = await doPoll( if (logFileStream) logFileStream.end()
requestClient,
postedJob,
currentState,
debug,
pollCount,
authConfig,
longJobPollOptions,
logFileStream
)
currentState = result.state
pollCount = result.pollCount
if (logFileStream) {
logFileStream.end()
}
return currentState return currentState
} }
@@ -119,17 +176,13 @@ const getJobState = async (
currentState: string, currentState: string,
debug: boolean, debug: boolean,
authConfig?: AuthConfig authConfig?: AuthConfig
) => { ): Promise<JobState> => {
const stateLink = job.links.find((l: any) => l.rel === 'state') const stateLink = job.links.find((l: any) => l.rel === 'state')!
if (!stateLink) {
throw new Error(`Job state link was not found.`)
}
if (needsRetry(currentState)) { if (needsRetry(currentState)) {
let tokens let tokens
if (authConfig) {
tokens = await getTokens(requestClient, authConfig) if (authConfig) tokens = await getTokens(requestClient, authConfig)
}
const { result: jobState } = await requestClient const { result: jobState } = await requestClient
.get<string>( .get<string>(
@@ -143,48 +196,38 @@ const getJobState = async (
throw new JobStatePollError(job.id, err) throw new JobStatePollError(job.id, err)
}) })
return jobState.trim() return jobState.trim() as JobState
} else { } else {
return currentState return currentState as JobState
} }
} }
const needsRetry = (state: string) => const needsRetry = (state: string) =>
state === 'running' || state === JobState.Running ||
state === '' || state === JobState.NoState ||
state === 'pending' || state === JobState.Pending ||
state === 'unavailable' state === JobState.Unavailable
const doPoll = async ( const doPoll = async (
requestClient: RequestClient, requestClient: RequestClient,
postedJob: Job, postedJob: Job,
currentState: string, currentState: JobState,
debug: boolean, debug: boolean,
pollCount: number, pollCount: number,
pollOptions: PollOptions,
authConfig?: AuthConfig, authConfig?: AuthConfig,
pollOptions?: PollOptions, streamLog?: boolean,
logStream?: WriteStream logStream?: WriteStream
): Promise<{ state: string; pollCount: number }> => { ): Promise<{ state: JobState; pollCount: number }> => {
let pollInterval = 300 const { maxPollCount, pollInterval } = pollOptions
let maxPollCount = 1000 const logger = process.logger || console
const stateLink = postedJob.links.find((l: Link) => l.rel === 'state')!
let maxErrorCount = 5 let maxErrorCount = 5
let errorCount = 0 let errorCount = 0
let state = currentState let state = currentState
let printedState = '' let printedState = JobState.NoState
let startLogLine = 0 let startLogLine = 0
const logger = process.logger || console
if (pollOptions) {
pollInterval = pollOptions.pollInterval || pollInterval
maxPollCount = pollOptions.maxPollCount || maxPollCount
}
const stateLink = postedJob.links.find((l: Link) => l.rel === 'state')
if (!stateLink) {
throw new Error(`Job state link was not found.`)
}
while (needsRetry(state) && pollCount <= maxPollCount) { while (needsRetry(state) && pollCount <= maxPollCount) {
state = await getJobState( state = await getJobState(
requestClient, requestClient,
@@ -194,21 +237,24 @@ const doPoll = async (
authConfig authConfig
).catch((err) => { ).catch((err) => {
errorCount++ errorCount++
if (pollCount >= maxPollCount || errorCount >= maxErrorCount) { if (pollCount >= maxPollCount || errorCount >= maxErrorCount) {
throw err throw err
} }
logger.error( logger.error(
`Error fetching job state from ${stateLink.href}. Resuming poll, assuming job to be running.`, `Error fetching job state from ${stateLink.href}. Resuming poll, assuming job to be running.`,
err err
) )
return 'unavailable'
return JobState.Unavailable
}) })
pollCount++ pollCount++
const jobHref = postedJob.links.find((l: Link) => l.rel === 'self')!.href const jobHref = postedJob.links.find((l: Link) => l.rel === 'self')!.href
if (pollOptions?.streamLog) { if (streamLog) {
const { result: job } = await requestClient.get<Job>( const { result: job } = await requestClient.get<Job>(
jobHref, jobHref,
authConfig?.access_token authConfig?.access_token
@@ -238,12 +284,45 @@ const doPoll = async (
printedState = state printedState = state
} }
if (state != 'unavailable' && errorCount > 0) { if (state !== JobState.Unavailable && errorCount > 0) {
errorCount = 0 errorCount = 0
} }
await delay(pollInterval) if (state !== JobState.Completed) {
await delay(pollInterval)
}
} }
return { state, pollCount } return { state, pollCount }
} }
const validatePollStrategies = (strategy: PollStrategy) => {
const throwError = (message?: string, pollOptions?: PollOptions) => {
throw new Error(
`Poll strategies are not valid.${message ? ` ${message}` : ''}${
pollOptions
? ` Invalid poll strategy: \n${JSON.stringify(pollOptions, null, 2)}`
: ''
}`
)
}
strategy.forEach((pollOptions: PollOptions, i: number) => {
const { maxPollCount, pollInterval } = pollOptions
if (maxPollCount < 1) {
throwError(`'maxPollCount' has to be greater than 0.`, pollOptions)
} else if (i !== 0) {
const previousPollOptions = strategy[i - 1]
if (maxPollCount <= previousPollOptions.maxPollCount) {
throwError(
`'maxPollCount' has to be greater than 'maxPollCount' in previous poll strategy.`,
pollOptions
)
}
} else if (pollInterval < 1) {
throwError(`'pollInterval' has to be greater than 0.`, pollOptions)
}
})
}

View File

@@ -9,14 +9,13 @@ import * as formatDataModule from '../../../utils/formatDataForRequest'
import * as fetchLogsModule from '../../../utils/fetchLogByChunks' import * as fetchLogsModule from '../../../utils/fetchLogByChunks'
import { PollOptions } from '../../../types' import { PollOptions } from '../../../types'
import { ComputeJobExecutionError, NotFoundError } from '../../../types/errors' import { ComputeJobExecutionError, NotFoundError } from '../../../types/errors'
import { Logger, LogLevel } from '@sasjs/utils' import { Logger, LogLevel } from '@sasjs/utils/logger'
const sessionManager = new (<jest.Mock<SessionManager>>SessionManager)() const sessionManager = new (<jest.Mock<SessionManager>>SessionManager)()
const requestClient = new (<jest.Mock<RequestClient>>RequestClient)() const requestClient = new (<jest.Mock<RequestClient>>RequestClient)()
const defaultPollOptions: PollOptions = { const defaultPollOptions: PollOptions = {
maxPollCount: 100, maxPollCount: 100,
pollInterval: 500, pollInterval: 500
streamLog: false
} }
describe('executeScript', () => { describe('executeScript', () => {
@@ -452,7 +451,9 @@ describe('executeScript', () => {
it('should throw a ComputeJobExecutionError if the job has failed', async () => { it('should throw a ComputeJobExecutionError if the job has failed', async () => {
jest jest
.spyOn(pollJobStateModule, 'pollJobState') .spyOn(pollJobStateModule, 'pollJobState')
.mockImplementation(() => Promise.resolve('failed')) .mockImplementation(() =>
Promise.resolve(pollJobStateModule.JobState.Failed)
)
const error: ComputeJobExecutionError = await executeScript( const error: ComputeJobExecutionError = await executeScript(
requestClient, requestClient,
@@ -485,7 +486,9 @@ describe('executeScript', () => {
it('should throw a ComputeJobExecutionError if the job has errored out', async () => { it('should throw a ComputeJobExecutionError if the job has errored out', async () => {
jest jest
.spyOn(pollJobStateModule, 'pollJobState') .spyOn(pollJobStateModule, 'pollJobState')
.mockImplementation(() => Promise.resolve('error')) .mockImplementation(() =>
Promise.resolve(pollJobStateModule.JobState.Error)
)
const error: ComputeJobExecutionError = await executeScript( const error: ComputeJobExecutionError = await executeScript(
requestClient, requestClient,
@@ -654,7 +657,9 @@ const setupMocks = () => {
.mockImplementation(() => Promise.resolve(mockAuthConfig)) .mockImplementation(() => Promise.resolve(mockAuthConfig))
jest jest
.spyOn(pollJobStateModule, 'pollJobState') .spyOn(pollJobStateModule, 'pollJobState')
.mockImplementation(() => Promise.resolve('completed')) .mockImplementation(() =>
Promise.resolve(pollJobStateModule.JobState.Completed)
)
jest jest
.spyOn(sessionManager, 'getVariable') .spyOn(sessionManager, 'getVariable')
.mockImplementation(() => .mockImplementation(() =>

View File

@@ -1,4 +1,4 @@
import { Logger, LogLevel } from '@sasjs/utils' import { Logger, LogLevel } from '@sasjs/utils/logger'
import { RequestClient } from '../../../request/RequestClient' import { RequestClient } from '../../../request/RequestClient'
import { mockAuthConfig, mockJob } from './mockResponses' import { mockAuthConfig, mockJob } from './mockResponses'
import { pollJobState } from '../pollJobState' import { pollJobState } from '../pollJobState'
@@ -6,17 +6,18 @@ import * as getTokensModule from '../../../auth/getTokens'
import * as saveLogModule from '../saveLog' import * as saveLogModule from '../saveLog'
import * as getFileStreamModule from '../getFileStream' import * as getFileStreamModule from '../getFileStream'
import * as isNodeModule from '../../../utils/isNode' import * as isNodeModule from '../../../utils/isNode'
import { PollOptions } from '../../../types' import * as delayModule from '../../../utils/delay'
import { PollOptions, PollStrategy } from '../../../types'
import { WriteStream } from 'fs' import { WriteStream } from 'fs'
const baseUrl = 'http://localhost' const baseUrl = 'http://localhost'
const requestClient = new (<jest.Mock<RequestClient>>RequestClient)() const requestClient = new (<jest.Mock<RequestClient>>RequestClient)()
requestClient['httpClient'].defaults.baseURL = baseUrl requestClient['httpClient'].defaults.baseURL = baseUrl
const defaultPollOptions: PollOptions = { const defaultStreamLog = false
const defaultPollStrategy: PollOptions = {
maxPollCount: 100, maxPollCount: 100,
pollInterval: 500, pollInterval: 500
streamLog: false
} }
describe('pollJobState', () => { describe('pollJobState', () => {
@@ -26,13 +27,10 @@ describe('pollJobState', () => {
}) })
it('should get valid tokens if the authConfig has been provided', async () => { it('should get valid tokens if the authConfig has been provided', async () => {
await pollJobState( await pollJobState(requestClient, mockJob, false, mockAuthConfig, {
requestClient, ...defaultPollStrategy,
mockJob, streamLog: defaultStreamLog
false, })
mockAuthConfig,
defaultPollOptions
)
expect(getTokensModule.getTokens).toHaveBeenCalledWith( expect(getTokensModule.getTokens).toHaveBeenCalledWith(
requestClient, requestClient,
@@ -46,7 +44,7 @@ describe('pollJobState', () => {
mockJob, mockJob,
false, false,
undefined, undefined,
defaultPollOptions defaultPollStrategy
) )
expect(getTokensModule.getTokens).not.toHaveBeenCalled() expect(getTokensModule.getTokens).not.toHaveBeenCalled()
@@ -58,7 +56,7 @@ describe('pollJobState', () => {
{ ...mockJob, links: mockJob.links.filter((l) => l.rel !== 'state') }, { ...mockJob, links: mockJob.links.filter((l) => l.rel !== 'state') },
false, false,
undefined, undefined,
defaultPollOptions defaultPollStrategy
).catch((e: any) => 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.')
@@ -72,7 +70,7 @@ describe('pollJobState', () => {
mockJob, mockJob,
false, false,
mockAuthConfig, mockAuthConfig,
defaultPollOptions defaultPollStrategy
) )
expect(getTokensModule.getTokens).toHaveBeenCalledTimes(3) expect(getTokensModule.getTokens).toHaveBeenCalledTimes(3)
@@ -83,7 +81,7 @@ describe('pollJobState', () => {
const { saveLog } = require('../saveLog') const { saveLog } = require('../saveLog')
await pollJobState(requestClient, mockJob, false, mockAuthConfig, { await pollJobState(requestClient, mockJob, false, mockAuthConfig, {
...defaultPollOptions, ...defaultPollStrategy,
streamLog: true streamLog: true
}) })
@@ -96,7 +94,7 @@ describe('pollJobState', () => {
const { saveLog } = require('../saveLog') const { saveLog } = require('../saveLog')
await pollJobState(requestClient, mockJob, false, mockAuthConfig, { await pollJobState(requestClient, mockJob, false, mockAuthConfig, {
...defaultPollOptions, ...defaultPollStrategy,
streamLog: true streamLog: true
}) })
@@ -111,7 +109,7 @@ describe('pollJobState', () => {
const { getFileStream } = require('../getFileStream') const { getFileStream } = require('../getFileStream')
await pollJobState(requestClient, mockJob, false, mockAuthConfig, { await pollJobState(requestClient, mockJob, false, mockAuthConfig, {
...defaultPollOptions, ...defaultPollStrategy,
streamLog: true streamLog: true
}) })
@@ -127,7 +125,7 @@ describe('pollJobState', () => {
mockJob, mockJob,
false, false,
mockAuthConfig, mockAuthConfig,
defaultPollOptions defaultPollStrategy
) )
expect(saveLogModule.saveLog).not.toHaveBeenCalled() expect(saveLogModule.saveLog).not.toHaveBeenCalled()
@@ -136,15 +134,18 @@ describe('pollJobState', () => {
it('should return the current status when the max poll count is reached', async () => { it('should return the current status when the max poll count is reached', async () => {
mockRunningPoll() mockRunningPoll()
const pollOptions: PollOptions = {
...defaultPollStrategy,
maxPollCount: 1,
pollStrategy: []
}
const state = await pollJobState( const state = await pollJobState(
requestClient, requestClient,
mockJob, mockJob,
false, false,
mockAuthConfig, mockAuthConfig,
{ pollOptions
...defaultPollOptions,
maxPollCount: 1
}
) )
expect(state).toEqual('running') expect(state).toEqual('running')
@@ -159,7 +160,7 @@ describe('pollJobState', () => {
false, false,
mockAuthConfig, mockAuthConfig,
{ {
...defaultPollOptions, ...defaultPollStrategy,
maxPollCount: 200, maxPollCount: 200,
pollInterval: 10 pollInterval: 10
} }
@@ -176,7 +177,7 @@ describe('pollJobState', () => {
mockJob, mockJob,
false, false,
undefined, undefined,
defaultPollOptions defaultPollStrategy
) )
expect(requestClient.get).toHaveBeenCalledTimes(2) expect(requestClient.get).toHaveBeenCalledTimes(2)
@@ -192,7 +193,7 @@ describe('pollJobState', () => {
mockJob, mockJob,
true, true,
undefined, undefined,
defaultPollOptions defaultPollStrategy
) )
expect((process as any).logger.info).toHaveBeenCalledTimes(4) expect((process as any).logger.info).toHaveBeenCalledTimes(4)
@@ -222,7 +223,7 @@ describe('pollJobState', () => {
mockJob, mockJob,
false, false,
undefined, undefined,
defaultPollOptions defaultPollStrategy
) )
expect(requestClient.get).toHaveBeenCalledTimes(2) expect(requestClient.get).toHaveBeenCalledTimes(2)
@@ -237,13 +238,119 @@ describe('pollJobState', () => {
mockJob, mockJob,
false, false,
undefined, undefined,
defaultPollOptions defaultPollStrategy
).catch((e: any) => 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'
) )
}) })
it('should change poll strategies', async () => {
mockSimplePoll(6)
const delays: number[] = []
jest.spyOn(delayModule, 'delay').mockImplementation((ms: number) => {
delays.push(ms)
return Promise.resolve()
})
const pollIntervals = [3, 4, 5, 6]
const pollStrategy = [
{ maxPollCount: 2, pollInterval: pollIntervals[1] },
{ maxPollCount: 3, pollInterval: pollIntervals[2] },
{ maxPollCount: 4, pollInterval: pollIntervals[3] }
]
const pollOptions: PollOptions = {
maxPollCount: 1,
pollInterval: pollIntervals[0],
pollStrategy: pollStrategy
}
await pollJobState(requestClient, mockJob, false, undefined, pollOptions)
expect(delays).toEqual([pollIntervals[0], ...pollIntervals])
})
it('should throw an error if not valid poll strategies provided', async () => {
// INFO: 'maxPollCount' has to be > 0
let invalidPollStrategy = {
maxPollCount: 0,
pollInterval: 3
}
let pollStrategy: PollStrategy = [invalidPollStrategy]
let expectedError = new Error(
`Poll strategies are not valid. 'maxPollCount' has to be greater than 0. Invalid poll strategy: \n${JSON.stringify(
invalidPollStrategy,
null,
2
)}`
)
await expect(
pollJobState(requestClient, mockJob, false, undefined, {
...defaultPollStrategy,
pollStrategy: pollStrategy
})
).rejects.toThrow(expectedError)
// INFO: 'maxPollCount' has to be > than 'maxPollCount' of the previous strategy
const validPollStrategy = {
maxPollCount: 5,
pollInterval: 2
}
invalidPollStrategy = {
maxPollCount: validPollStrategy.maxPollCount,
pollInterval: 3
}
pollStrategy = [validPollStrategy, invalidPollStrategy]
expectedError = new Error(
`Poll strategies are not valid. 'maxPollCount' has to be greater than 'maxPollCount' in previous poll strategy. Invalid poll strategy: \n${JSON.stringify(
invalidPollStrategy,
null,
2
)}`
)
await expect(
pollJobState(requestClient, mockJob, false, undefined, {
...defaultPollStrategy,
pollStrategy: pollStrategy
})
).rejects.toThrow(expectedError)
// INFO: invalid 'pollInterval'
invalidPollStrategy = {
maxPollCount: 1,
pollInterval: 0
}
pollStrategy = [invalidPollStrategy]
expectedError = new Error(
`Poll strategies are not valid. 'pollInterval' has to be greater than 0. Invalid poll strategy: \n${JSON.stringify(
invalidPollStrategy,
null,
2
)}`
)
await expect(
pollJobState(requestClient, mockJob, false, undefined, {
...defaultPollStrategy,
pollStrategy: pollStrategy
})
).rejects.toThrow(expectedError)
})
}) })
const setupMocks = () => { const setupMocks = () => {
@@ -273,11 +380,14 @@ const setupMocks = () => {
const mockSimplePoll = (runningCount = 2) => { const mockSimplePoll = (runningCount = 2) => {
let count = 0 let count = 0
jest.spyOn(requestClient, 'get').mockImplementation((url) => { jest.spyOn(requestClient, 'get').mockImplementation((url) => {
count++ count++
if (url.includes('job')) { if (url.includes('job')) {
return Promise.resolve({ result: mockJob, etag: '', status: 200 }) return Promise.resolve({ result: mockJob, etag: '', status: 200 })
} }
return Promise.resolve({ return Promise.resolve({
result: result:
count === 0 count === 0
@@ -293,11 +403,14 @@ const mockSimplePoll = (runningCount = 2) => {
const mockRunningPoll = () => { const mockRunningPoll = () => {
let count = 0 let count = 0
jest.spyOn(requestClient, 'get').mockImplementation((url) => { jest.spyOn(requestClient, 'get').mockImplementation((url) => {
count++ count++
if (url.includes('job')) { if (url.includes('job')) {
return Promise.resolve({ result: mockJob, etag: '', status: 200 }) return Promise.resolve({ result: mockJob, etag: '', status: 200 })
} }
return Promise.resolve({ return Promise.resolve({
result: count === 0 ? 'pending' : 'running', result: count === 0 ? 'pending' : 'running',
etag: '', etag: '',
@@ -308,11 +421,14 @@ const mockRunningPoll = () => {
const mockLongPoll = () => { const mockLongPoll = () => {
let count = 0 let count = 0
jest.spyOn(requestClient, 'get').mockImplementation((url) => { jest.spyOn(requestClient, 'get').mockImplementation((url) => {
count++ count++
if (url.includes('job')) { if (url.includes('job')) {
return Promise.resolve({ result: mockJob, etag: '', status: 200 }) return Promise.resolve({ result: mockJob, etag: '', status: 200 })
} }
return Promise.resolve({ return Promise.resolve({
result: count <= 102 ? 'running' : 'completed', result: count <= 102 ? 'running' : 'completed',
etag: '', etag: '',
@@ -323,14 +439,18 @@ const mockLongPoll = () => {
const mockPollWithSingleError = () => { const mockPollWithSingleError = () => {
let count = 0 let count = 0
jest.spyOn(requestClient, 'get').mockImplementation((url) => { jest.spyOn(requestClient, 'get').mockImplementation((url) => {
count++ count++
if (url.includes('job')) { if (url.includes('job')) {
return Promise.resolve({ result: mockJob, etag: '', status: 200 }) return Promise.resolve({ result: mockJob, etag: '', status: 200 })
} }
if (count === 1) { if (count === 1) {
return Promise.reject('Status Error') return Promise.reject('Status Error')
} }
return Promise.resolve({ return Promise.resolve({
result: count === 0 ? 'pending' : 'completed', result: count === 0 ? 'pending' : 'completed',
etag: '', etag: '',
@@ -344,6 +464,7 @@ const mockErroredPoll = () => {
if (url.includes('job')) { if (url.includes('job')) {
return Promise.resolve({ result: mockJob, etag: '', status: 200 }) return Promise.resolve({ result: mockJob, etag: '', status: 200 })
} }
return Promise.reject('Status Error') return Promise.reject('Status Error')
}) })
} }

View File

@@ -1,4 +1,4 @@
import { Logger, LogLevel } from '@sasjs/utils' import { Logger, LogLevel } from '@sasjs/utils/logger'
import { RequestClient } from '../../../request/RequestClient' import { RequestClient } from '../../../request/RequestClient'
import * as fetchLogsModule from '../../../utils/fetchLogByChunks' import * as fetchLogsModule from '../../../utils/fetchLogByChunks'
import * as writeStreamModule from '../writeStream' import * as writeStreamModule from '../writeStream'

View File

@@ -5,7 +5,7 @@ import {
fileExists, fileExists,
readFile, readFile,
deleteFile deleteFile
} from '@sasjs/utils' } from '@sasjs/utils/file'
describe('writeStream', () => { describe('writeStream', () => {
const filename = 'test.txt' const filename = 'test.txt'

View File

@@ -1,5 +1,7 @@
import { prefixMessage } from '@sasjs/utils/error' import { prefixMessage } from '@sasjs/utils/error'
import { RequestClient } from '../request/RequestClient' import { RequestClient } from '../request/RequestClient'
import { getTokenRequestErrorPrefix } from './getTokenRequestErrorPrefix'
import { ServerType } from '@sasjs/utils/types'
/** /**
* Exchanges the auth code for an access token for the given client. * Exchanges the auth code for an access token for the given client.
@@ -31,6 +33,16 @@ export async function getAccessTokenForSasjs(
} }
}) })
.catch((err) => { .catch((err) => {
throw prefixMessage(err, 'Error while getting access token. ') throw prefixMessage(
err,
getTokenRequestErrorPrefix(
'fetching access token',
'getAccessTokenForSasjs',
ServerType.Sasjs,
url,
data,
clientId
)
)
}) })
} }

View File

@@ -1,11 +1,12 @@
import { SasAuthResponse } from '@sasjs/utils/types' import { SasAuthResponse, ServerType } from '@sasjs/utils/types'
import { prefixMessage } from '@sasjs/utils/error' import { prefixMessage } from '@sasjs/utils/error'
import { RequestClient } from '../request/RequestClient' import { RequestClient } from '../request/RequestClient'
import { CertificateError } from '../types/errors' import { CertificateError } from '../types/errors'
import { getTokenRequestErrorPrefix } from './getTokenRequestErrorPrefix'
/** /**
* Exchanges the auth code for an access token for the given client. * Exchange the auth code for access / refresh tokens for the given client / secret pair.
* @param requestClient - the pre-configured HTTP request client * @param requestClient - the pre-configured HTTP request client.
* @param clientId - the client ID to authenticate with. * @param clientId - the client ID to authenticate with.
* @param clientSecret - the client secret to authenticate with. * @param clientSecret - the client secret to authenticate with.
* @param authCode - the auth code received from the server. * @param authCode - the auth code received from the server.
@@ -16,29 +17,44 @@ export async function getAccessTokenForViya(
clientSecret: string, clientSecret: string,
authCode: string authCode: string
): Promise<SasAuthResponse> { ): Promise<SasAuthResponse> {
const url = '/SASLogon/oauth/token'
let token let token
if (typeof Buffer === 'undefined') { if (typeof Buffer === 'undefined') {
token = btoa(clientId + ':' + clientSecret) token = btoa(clientId + ':' + clientSecret)
} else { } else {
token = Buffer.from(clientId + ':' + clientSecret).toString('base64') token = Buffer.from(clientId + ':' + clientSecret).toString('base64')
} }
const url = '/SASLogon/oauth/token'
const headers = { const headers = {
Authorization: 'Basic ' + token, Authorization: 'Basic ' + token,
Accept: 'application/json' Accept: 'application/json'
} }
const data = new URLSearchParams({ const dataJson = new URLSearchParams({
grant_type: 'authorization_code', grant_type: 'authorization_code',
code: authCode code: authCode
}) })
const data = new URLSearchParams(dataJson)
const authResponse = await requestClient const authResponse = await requestClient
.post(url, data, undefined, 'application/x-www-form-urlencoded', headers) .post(url, data, undefined, 'application/x-www-form-urlencoded', headers)
.then((res) => res.result as SasAuthResponse) .then((res) => res.result as SasAuthResponse)
.catch((err) => { .catch((err) => {
if (err instanceof CertificateError) throw err if (err instanceof CertificateError) throw err
throw prefixMessage(err, 'Error while getting access token. ') throw prefixMessage(
err,
getTokenRequestErrorPrefix(
'fetching access token',
'getAccessTokenForViya',
ServerType.SasViya,
url,
dataJson,
headers,
clientId,
clientSecret
)
)
}) })
return authResponse return authResponse

View File

@@ -0,0 +1,88 @@
import { ServerType } from '@sasjs/utils/types'
type Server = ServerType.SasViya | ServerType.Sasjs
type Operation = 'fetching access token' | 'refreshing tokens'
const getServerName = (server: Server) =>
server === ServerType.SasViya ? 'Viya' : 'Sasjs'
const getResponseTitle = (server: Server) =>
`Response from ${getServerName(server)} is below.`
/**
* Forms error prefix for requests related to token operations.
* @param operation - string describing operation ('fetching access token' or 'refreshing tokens').
* @param funcName - name of the function sent the request.
* @param server - server type (SASVIYA or SASJS).
* @param url - endpoint used to send the request.
* @param data - request payload.
* @param headers - request headers.
* @param clientId - client ID to authenticate with.
* @param clientSecret - client secret to authenticate with.
* @returns - string containing request information. Example:
* Error while fetching access token from /SASLogon/oauth/token
* Thrown by the @sasjs/adapter getAccessTokenForViya function.
* Payload:
* {
* "grant_type": "authorization_code",
* "code": "example_code"
* }
* Headers:
* {
* "Authorization": "Basic NEdMQXBwOjRHTEFwcDE=",
* "Accept": "application/json"
* }
* ClientId: exampleClientId
* ClientSecret: exampleClientSecret
*
* Response from Viya is below.
* Auth error: {
* "error": "invalid_token",
* "error_description": "No scopes were granted"
* }
*/
export const getTokenRequestErrorPrefix = (
operation: Operation,
funcName: string,
server: Server,
url: string,
data?: {},
headers?: {},
clientId?: string,
clientSecret?: string
) => {
const stringify = (obj: {}) => JSON.stringify(obj, null, 2)
const lines = [
`Error while ${operation} from ${url}`,
`Thrown by the @sasjs/adapter ${funcName} function.`
]
if (data) {
lines.push('Payload:')
lines.push(stringify(data))
}
if (headers) {
lines.push('Headers:')
lines.push(stringify(headers))
}
if (clientId) lines.push(`ClientId: ${clientId}`)
if (clientSecret) lines.push(`ClientSecret: ${clientSecret}`)
lines.push('')
lines.push(`${getResponseTitle(server)}`)
lines.push('')
return lines.join(`\n`)
}
/**
* Parse error prefix to get response payload.
* @param prefix - error prefix generated by getTokenRequestErrorPrefix function.
* @param server - server type (SASVIYA or SASJS).
* @returns - response payload.
*/
export const getTokenRequestErrorPrefixResponse = (
prefix: string,
server: ServerType.SasViya | ServerType.Sasjs
) => prefix.split(`${getResponseTitle(server)}\n`).pop() as string

View File

@@ -22,6 +22,7 @@ export async function getTokens(
): Promise<AuthConfig> { ): Promise<AuthConfig> {
const logger = process.logger || console const logger = process.logger || console
let { access_token, refresh_token, client, secret } = authConfig let { access_token, refresh_token, client, secret } = authConfig
if ( if (
isAccessTokenExpiring(access_token) || isAccessTokenExpiring(access_token) ||
isRefreshTokenExpiring(refresh_token) isRefreshTokenExpiring(refresh_token)
@@ -29,6 +30,7 @@ export async function getTokens(
if (hasTokenExpired(refresh_token)) { if (hasTokenExpired(refresh_token)) {
const error = const error =
'Unable to obtain new access token. Your refresh token has expired.' 'Unable to obtain new access token. Your refresh token has expired.'
logger.error(error) logger.error(error)
throw new Error(error) throw new Error(error)
@@ -47,5 +49,6 @@ export async function getTokens(
: await refreshTokensForSasjs(requestClient, refresh_token) : await refreshTokensForSasjs(requestClient, refresh_token)
;({ access_token, refresh_token } = tokens) ;({ access_token, refresh_token } = tokens)
} }
return { access_token, refresh_token, client, secret } return { access_token, refresh_token, client, secret }
} }

View File

@@ -1,5 +1,7 @@
import { prefixMessage } from '@sasjs/utils/error' import { prefixMessage } from '@sasjs/utils/error'
import { RequestClient } from '../request/RequestClient' import { RequestClient } from '../request/RequestClient'
import { getTokenRequestErrorPrefix } from './getTokenRequestErrorPrefix'
import { ServerType } from '@sasjs/utils/types'
/** /**
* Exchanges the refresh token for an access token for the given client. * Exchanges the refresh token for an access token for the given client.
@@ -28,7 +30,15 @@ export async function refreshTokensForSasjs(
} }
}) })
.catch((err) => { .catch((err) => {
throw prefixMessage(err, 'Error while refreshing tokens: ') throw prefixMessage(
err,
getTokenRequestErrorPrefix(
'refreshing tokens',
'refreshTokensForSasjs',
ServerType.Sasjs,
url
)
)
}) })
return authResponse return authResponse

View File

@@ -1,8 +1,9 @@
import { SasAuthResponse } from '@sasjs/utils/types' import { SasAuthResponse, ServerType } from '@sasjs/utils/types'
import { prefixMessage } from '@sasjs/utils/error' import { prefixMessage } from '@sasjs/utils/error'
import * as NodeFormData from 'form-data' import * as NodeFormData from 'form-data'
import { RequestClient } from '../request/RequestClient' import { RequestClient } from '../request/RequestClient'
import { isNode } from '../utils' import { isNode } from '../utils'
import { getTokenRequestErrorPrefix } from './getTokenRequestErrorPrefix'
/** /**
* Exchanges the refresh token for an access token for the given client. * Exchanges the refresh token for an access token for the given client.
@@ -46,7 +47,19 @@ 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,
getTokenRequestErrorPrefix(
'refreshing tokens',
'refreshTokensForViya',
ServerType.SasViya,
url,
formData,
headers,
clientId,
clientSecret
)
)
}) })
return authResponse return authResponse

View File

@@ -1,4 +1,4 @@
import { AuthConfig } from '@sasjs/utils' import { AuthConfig } from '@sasjs/utils/types'
import { generateToken, mockSasjsAuthResponse } from './mockResponses' import { generateToken, mockSasjsAuthResponse } from './mockResponses'
import { RequestClient } from '../../request/RequestClient' import { RequestClient } from '../../request/RequestClient'
import { getAccessTokenForSasjs } from '../getAccessTokenForSasjs' import { getAccessTokenForSasjs } from '../getAccessTokenForSasjs'
@@ -55,7 +55,7 @@ describe('getAccessTokenForSasjs', () => {
authConfig.refresh_token authConfig.refresh_token
).catch((e: any) => e) ).catch((e: any) => e)
expect(error).toContain('Error while getting access token') expect(error).toContain('Error while fetching access token')
}) })
}) })

View File

@@ -1,4 +1,4 @@
import { AuthConfig } from '@sasjs/utils' import { AuthConfig } from '@sasjs/utils/types'
import * as NodeFormData from 'form-data' import * as NodeFormData from 'form-data'
import { generateToken, mockAuthResponse } from './mockResponses' import { generateToken, mockAuthResponse } from './mockResponses'
import { RequestClient } from '../../request/RequestClient' import { RequestClient } from '../../request/RequestClient'
@@ -66,7 +66,7 @@ describe('getAccessTokenForViya', () => {
authConfig.refresh_token authConfig.refresh_token
).catch((e: any) => e) ).catch((e: any) => e)
expect(error).toContain('Error while getting access token') expect(error).toContain('Error while fetching access token')
}) })
}) })

View File

@@ -0,0 +1,81 @@
import { ServerType } from '@sasjs/utils/types'
import { getTokenRequestErrorPrefix } from '../getTokenRequestErrorPrefix'
describe('getTokenRequestErrorPrefix', () => {
it('should return error prefix', () => {
// INFO: Viya with only required attributes
let operation: 'fetching access token' = 'fetching access token'
const funcName = 'testFunc'
const url = '/SASjsApi/auth/token'
let expectedPrefix = `Error while ${operation} from ${url}
Thrown by the @sasjs/adapter ${funcName} function.
Response from Viya is below.
`
expect(
getTokenRequestErrorPrefix(operation, funcName, ServerType.SasViya, url)
).toEqual(expectedPrefix)
// INFO: Sasjs with data and headers
const data = {
grant_type: 'authorization_code',
code: 'testCode'
}
const headers = {
Authorization: 'Basic test=',
Accept: 'application/json'
}
expectedPrefix = `Error while ${operation} from ${url}
Thrown by the @sasjs/adapter ${funcName} function.
Payload:
${JSON.stringify(data, null, 2)}
Headers:
${JSON.stringify(headers, null, 2)}
Response from Sasjs is below.
`
expect(
getTokenRequestErrorPrefix(
operation,
funcName,
ServerType.Sasjs,
url,
data,
headers
)
).toEqual(expectedPrefix)
// INFO: Viya with all attributes
const clientId = 'testId'
const clientSecret = 'testSecret'
expectedPrefix = `Error while ${operation} from ${url}
Thrown by the @sasjs/adapter ${funcName} function.
Payload:
${JSON.stringify(data, null, 2)}
Headers:
${JSON.stringify(headers, null, 2)}
ClientId: ${clientId}
ClientSecret: ${clientSecret}
Response from Viya is below.
`
expect(
getTokenRequestErrorPrefix(
operation,
funcName,
ServerType.SasViya,
url,
data,
headers,
clientId,
clientSecret
)
).toEqual(expectedPrefix)
})
})

View File

@@ -1,4 +1,4 @@
import { AuthConfig } from '@sasjs/utils' import { AuthConfig } from '@sasjs/utils/types'
import * as refreshTokensModule from '../refreshTokensForViya' import * as refreshTokensModule from '../refreshTokensForViya'
import { generateToken, mockAuthResponse } from './mockResponses' import { generateToken, mockAuthResponse } from './mockResponses'
import { getTokens } from '../getTokens' import { getTokens } from '../getTokens'

View File

@@ -1,6 +1,8 @@
import { ServerType } from '@sasjs/utils/types'
import { generateToken, mockAuthResponse } from './mockResponses' import { generateToken, mockAuthResponse } from './mockResponses'
import { RequestClient } from '../../request/RequestClient' import { RequestClient } from '../../request/RequestClient'
import { refreshTokensForSasjs } from '../refreshTokensForSasjs' import { refreshTokensForSasjs } from '../refreshTokensForSasjs'
import { getTokenRequestErrorPrefixResponse } from '../getTokenRequestErrorPrefix'
const requestClient = new (<jest.Mock<RequestClient>>RequestClient)() const requestClient = new (<jest.Mock<RequestClient>>RequestClient)()
@@ -38,9 +40,9 @@ describe('refreshTokensForSasjs', () => {
const error = await refreshTokensForSasjs( const error = await refreshTokensForSasjs(
requestClient, requestClient,
refresh_token refresh_token
).catch((e: any) => e) ).catch((e: any) => getTokenRequestErrorPrefixResponse(e, ServerType.Sasjs))
expect(error).toEqual(`Error while refreshing tokens: ${tokenError}`) expect(error).toEqual(tokenError)
}) })
}) })

View File

@@ -1,9 +1,10 @@
import { AuthConfig } from '@sasjs/utils' import { AuthConfig, ServerType } from '@sasjs/utils/types'
import * as NodeFormData from 'form-data' import * as NodeFormData from 'form-data'
import { generateToken, mockAuthResponse } from './mockResponses' import { generateToken, mockAuthResponse } from './mockResponses'
import { RequestClient } from '../../request/RequestClient' import { RequestClient } from '../../request/RequestClient'
import { refreshTokensForViya } from '../refreshTokensForViya' import { refreshTokensForViya } from '../refreshTokensForViya'
import * as IsNodeModule from '../../utils/isNode' import * as IsNodeModule from '../../utils/isNode'
import { getTokenRequestErrorPrefixResponse } from '../getTokenRequestErrorPrefix'
const requestClient = new (<jest.Mock<RequestClient>>RequestClient)() const requestClient = new (<jest.Mock<RequestClient>>RequestClient)()
@@ -67,9 +68,11 @@ describe('refreshTokensForViya', () => {
authConfig.client, authConfig.client,
authConfig.secret, authConfig.secret,
authConfig.refresh_token authConfig.refresh_token
).catch((e: any) => e) ).catch((e: any) =>
getTokenRequestErrorPrefixResponse(e, ServerType.SasViya)
)
expect(error).toEqual(`Error while refreshing tokens: ${tokenError}`) expect(error).toEqual(tokenError)
}) })
it('should throw an error if environment is not Node', async () => { it('should throw an error if environment is not Node', async () => {

View File

@@ -93,15 +93,24 @@ export class FileUploader extends BaseJobExecutor {
this.requestClient, this.requestClient,
config.serverUrl config.serverUrl
) )
break break
case ServerType.Sas9: case ServerType.Sas9:
jsonResponse = jsonResponse =
typeof res.result === 'string' typeof res.result === 'string'
? parseWeboutResponse(res.result, uploadUrl) ? parseWeboutResponse(res.result, uploadUrl)
: res.result : res.result
break
case ServerType.Sasjs:
jsonResponse =
typeof res.result === 'string'
? getValidJson(res.result)
: res.result
break break
} }
} else if (this.serverType !== ServerType.Sasjs) { } else {
jsonResponse = jsonResponse =
typeof res.result === 'string' typeof res.result === 'string'
? getValidJson(res.result) ? getValidJson(res.result)

View File

@@ -13,7 +13,11 @@ import { generateFileUploadForm } from '../file/generateFileUploadForm'
import { RequestClient } from '../request/RequestClient' import { RequestClient } from '../request/RequestClient'
import { isRelativePath, appendExtraResponseAttributes } from '../utils' import {
isRelativePath,
appendExtraResponseAttributes,
getValidJson
} from '../utils'
import { BaseJobExecutor } from './JobExecutor' import { BaseJobExecutor } from './JobExecutor'
export class SasjsJobExecutor extends BaseJobExecutor { export class SasjsJobExecutor extends BaseJobExecutor {
@@ -89,12 +93,18 @@ export class SasjsJobExecutor extends BaseJobExecutor {
) )
} }
const { result } = res
if (result && typeof result === 'string' && result.trim())
res.result = getValidJson(result)
this.requestClient!.appendRequest(res, sasJob, config.debug) this.requestClient!.appendRequest(res, sasJob, config.debug)
const responseObject = appendExtraResponseAttributes( const responseObject = appendExtraResponseAttributes(
res, res,
extraResponseAttributes extraResponseAttributes
) )
resolve(responseObject) resolve(responseObject)
}) })
.catch(async (e: Error) => { .catch(async (e: Error) => {

View File

@@ -1,6 +1,14 @@
import { RequestClient } from './RequestClient' import { RequestClient } from './RequestClient'
import { AxiosResponse } from 'axios' import { AxiosResponse } from 'axios'
import { SASJS_LOGS_SEPARATOR, getValidJson } from '../utils' import { SASJS_LOGS_SEPARATOR } from '../utils'
interface SasjsParsedResponse<T> {
result: T
log: string
etag: string
status: number
printOutput?: string
}
/** /**
* Specific request client for SASJS. * Specific request client for SASJS.
@@ -27,7 +35,7 @@ export class SasjsRequestClient extends RequestClient {
protected parseResponse<T>(response: AxiosResponse<any>) { protected parseResponse<T>(response: AxiosResponse<any>) {
const etag = response?.headers ? response.headers['etag'] : '' const etag = response?.headers ? response.headers['etag'] : ''
let parsedResponse = {} let parsedResponse = {}
let log let webout, log, printOutput
try { try {
if (typeof response.data === 'string') { if (typeof response.data === 'string') {
@@ -38,17 +46,26 @@ export class SasjsRequestClient extends RequestClient {
} catch { } catch {
if (response.data.includes(SASJS_LOGS_SEPARATOR)) { if (response.data.includes(SASJS_LOGS_SEPARATOR)) {
const splittedResponse = response.data.split(SASJS_LOGS_SEPARATOR) const splittedResponse = response.data.split(SASJS_LOGS_SEPARATOR)
webout = splittedResponse[0]
if (webout !== undefined) parsedResponse = webout
log = splittedResponse[1] log = splittedResponse[1]
if (splittedResponse[0].trim()) printOutput = splittedResponse[2]
parsedResponse = getValidJson(splittedResponse[0]) } else {
} else parsedResponse = response.data parsedResponse = response.data
}
} }
return { const returnResult: SasjsParsedResponse<T> = {
result: parsedResponse as T, result: parsedResponse as T,
log, log,
etag, etag,
status: response.status status: response.status
} }
if (printOutput) returnResult.printOutput = printOutput
return returnResult
} }
} }

View File

@@ -2,7 +2,7 @@ import * as pem from 'pem'
import * as http from 'http' import * as http from 'http'
import * as https from 'https' import * as https from 'https'
import { app, mockedAuthResponse } from './SAS_server_app' import { app, mockedAuthResponse } from './SAS_server_app'
import { ServerType } from '@sasjs/utils' import { ServerType } from '@sasjs/utils/types'
import SASjs from '../SASjs' import SASjs from '../SASjs'
import * as axiosModules from '../utils/createAxiosInstance' import * as axiosModules from '../utils/createAxiosInstance'
import { import {
@@ -11,8 +11,8 @@ import {
NotFoundError, NotFoundError,
InternalServerError InternalServerError
} from '../types/errors' } from '../types/errors'
import { prefixMessage } from '@sasjs/utils/error'
import { RequestClient } from '../request/RequestClient' import { RequestClient } from '../request/RequestClient'
import { getTokenRequestErrorPrefixResponse } from '../auth/getTokenRequestErrorPrefix'
const axiosActual = jest.requireActual('axios') const axiosActual = jest.requireActual('axios')
@@ -66,14 +66,18 @@ describe('RequestClient', () => {
}) })
it('should response the POST method with Unauthorized', async () => { it('should response the POST method with Unauthorized', async () => {
await expect( const expectedError = new LoginRequiredError({
adapter.getAccessToken('clientId', 'clientSecret', 'incorrect') error: 'unauthorized',
).rejects.toEqual( error_description: 'Bad credentials'
prefixMessage( })
new LoginRequiredError(incorrectAuthCodeErr),
'Error while getting access token. ' const rejectionErrorMessage = await adapter
.getAccessToken('clientId', 'clientSecret', 'incorrect')
.catch((err) =>
getTokenRequestErrorPrefixResponse(err.message, ServerType.SasViya)
) )
)
expect(rejectionErrorMessage).toEqual(expectedError.message)
}) })
describe('handleError', () => { describe('handleError', () => {
@@ -209,15 +213,15 @@ describe('RequestClient - Self Signed Server', () => {
serverType: ServerType.SasViya serverType: ServerType.SasViya
}) })
await expect( const expectedError = 'self-signed certificate'
adapterWithoutCertificate.getAccessToken(
'clientId', const rejectionErrorMessage = await adapterWithoutCertificate
'clientSecret', .getAccessToken('clientId', 'clientSecret', 'authCode')
'authCode' .catch((err) =>
getTokenRequestErrorPrefixResponse(err.message, ServerType.SasViya)
) )
).rejects.toThrow(
`Error while getting access token. ${ERROR_MESSAGES.selfSigned}` expect(rejectionErrorMessage).toEqual(expectedError)
)
}) })
it('should response the POST method using insecure flag', async () => { it('should response the POST method using insecure flag', async () => {
@@ -247,14 +251,18 @@ describe('RequestClient - Self Signed Server', () => {
}) })
it('should response the POST method with Unauthorized', async () => { it('should response the POST method with Unauthorized', async () => {
await expect( const expectedError = new LoginRequiredError({
adapter.getAccessToken('clientId', 'clientSecret', 'incorrect') error: 'unauthorized',
).rejects.toEqual( error_description: 'Bad credentials'
prefixMessage( })
new LoginRequiredError(incorrectAuthCodeErr),
'Error while getting access token. ' const rejectionErrorMessage = await adapter
.getAccessToken('clientId', 'clientSecret', 'incorrect')
.catch((err) =>
getTokenRequestErrorPrefixResponse(err.message, ServerType.SasViya)
) )
)
expect(rejectionErrorMessage).toEqual(expectedError.message)
}) })
}) })

View File

@@ -2,7 +2,7 @@ import { SessionManager } from '../SessionManager'
import { RequestClient } from '../request/RequestClient' import { RequestClient } from '../request/RequestClient'
import * as dotenv from 'dotenv' import * as dotenv from 'dotenv'
import axios from 'axios' import axios from 'axios'
import { Logger, LogLevel } from '@sasjs/utils' import { Logger, LogLevel } from '@sasjs/utils/logger'
import { Session, Context } from '../types' import { Session, Context } from '../types'
jest.mock('axios') jest.mock('axios')

View File

@@ -1,6 +1,9 @@
export interface PollOptions { export interface PollOptions {
maxPollCount: number maxPollCount: number
pollInterval: number pollInterval: number // milliseconds
streamLog: boolean pollStrategy?: PollStrategy
streamLog?: boolean
logFolderPath?: string logFolderPath?: string
} }
export type PollStrategy = PollOptions[]

View File

@@ -7,7 +7,7 @@ describe('RootFolderNotFoundError', () => {
const error = new RootFolderNotFoundError( const error = new RootFolderNotFoundError(
'/myProject', '/myProject',
'https://analytium.co.uk', 'https://sas.4gl.io',
token token
) )
@@ -19,7 +19,7 @@ describe('RootFolderNotFoundError', () => {
it('when access token is not provided, error message should not contain scopes', () => { it('when access token is not provided, error message should not contain scopes', () => {
const error = new RootFolderNotFoundError( const error = new RootFolderNotFoundError(
'/myProject', '/myProject',
'https://analytium.co.uk' 'https://sas.4gl.io'
) )
expect(error).toBeInstanceOf(RootFolderNotFoundError) expect(error).toBeInstanceOf(RootFolderNotFoundError)
@@ -30,7 +30,7 @@ describe('RootFolderNotFoundError', () => {
it('should include the folder path and SASDrive URL in the message', () => { it('should include the folder path and SASDrive URL in the message', () => {
const folderPath = '/myProject' const folderPath = '/myProject'
const serverUrl = 'https://analytium.co.uk' const serverUrl = 'https://sas.4gl.io'
const error = new RootFolderNotFoundError(folderPath, serverUrl) const error = new RootFolderNotFoundError(folderPath, serverUrl)
expect(error).toBeInstanceOf(RootFolderNotFoundError) expect(error).toBeInstanceOf(RootFolderNotFoundError)

View File

@@ -17,6 +17,7 @@ export const getValidJson = (str: string | object): object => {
return JSON.parse(str) return JSON.parse(str)
} catch (e: any) { } catch (e: any) {
if (e instanceof JsonParseArrayError) throw e if (e instanceof JsonParseArrayError) throw e
throw new InvalidJsonError() throw new InvalidJsonError()
} }
} }

View File

@@ -86,7 +86,8 @@ const nodeConfig = {
entry: './node/index.ts', entry: './node/index.ts',
output: { output: {
...browserConfig.output, ...browserConfig.output,
path: path.resolve(__dirname, 'build', 'node') path: path.resolve(__dirname, 'build', 'node'),
filename: 'index.js'
} }
} }