1
0
mirror of https://github.com/sasjs/adapter.git synced 2025-12-13 10:14:36 +00:00

Compare commits

...

70 Commits

Author SHA1 Message Date
Krishna Acondy
232f4ec3fb chore(*): add tests for SessionManager 2020-11-24 07:42:18 +00:00
Krishna Acondy
e1f17ef47d Merge pull request #127 from sasjs/dependabot/npm_and_yarn/terser-webpack-plugin-4.2.3
chore(deps-dev): bump terser-webpack-plugin from 4.2.2 to 4.2.3
2020-11-20 09:07:14 +00:00
dependabot-preview[bot]
8a40071c35 chore(deps-dev): bump terser-webpack-plugin from 4.2.2 to 4.2.3
Bumps [terser-webpack-plugin](https://github.com/webpack-contrib/terser-webpack-plugin) from 4.2.2 to 4.2.3.
- [Release notes](https://github.com/webpack-contrib/terser-webpack-plugin/releases)
- [Changelog](https://github.com/webpack-contrib/terser-webpack-plugin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/terser-webpack-plugin/compare/v4.2.2...v4.2.3)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-11-20 09:01:25 +00:00
Krishna Acondy
430957eb3d Merge pull request #132 from sasjs/dependabot/npm_and_yarn/npm-user-validate-1.0.1
chore(deps): [security] bump npm-user-validate from 1.0.0 to 1.0.1
2020-11-20 08:58:57 +00:00
dependabot-preview[bot]
25874be679 chore(deps): [security] bump npm-user-validate from 1.0.0 to 1.0.1
Bumps [npm-user-validate](https://github.com/npm/npm-user-validate) from 1.0.0 to 1.0.1. **This update includes a security fix.**
- [Release notes](https://github.com/npm/npm-user-validate/releases)
- [Commits](https://github.com/npm/npm-user-validate/compare/v1.0.0...v1.0.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-11-20 08:57:20 +00:00
Krishna Acondy
ed8440434f Merge pull request #135 from sasjs/dependabot/npm_and_yarn/types/jest-26.0.15
chore(deps-dev): bump @types/jest from 26.0.14 to 26.0.15
2020-11-20 08:55:42 +00:00
dependabot-preview[bot]
0f9884c1b6 chore(deps-dev): bump @types/jest from 26.0.14 to 26.0.15
Bumps [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest) from 26.0.14 to 26.0.15.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jest)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-11-20 08:46:00 +00:00
Krishna Acondy
d126a05347 Merge pull request #146 from sasjs/dependabot/npm_and_yarn/webpack-cli-4.2.0
chore(deps-dev): bump webpack-cli from 3.3.12 to 4.2.0
2020-11-20 08:43:50 +00:00
dependabot-preview[bot]
3e26bbbbba chore(deps-dev): bump webpack-cli from 3.3.12 to 4.2.0
Bumps [webpack-cli](https://github.com/webpack/webpack-cli) from 3.3.12 to 4.2.0.
- [Release notes](https://github.com/webpack/webpack-cli/releases)
- [Changelog](https://github.com/webpack/webpack-cli/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-cli/compare/v3.3.12...webpack-cli@4.2.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-11-20 08:34:49 +00:00
Krishna Acondy
982cc8f7a0 Merge pull request #149 from sasjs/dependabot/npm_and_yarn/ts-loader-8.0.11
chore(deps-dev): bump ts-loader from 8.0.4 to 8.0.11
2020-11-20 08:32:39 +00:00
dependabot-preview[bot]
d1770698e0 chore(deps-dev): bump ts-loader from 8.0.4 to 8.0.11
Bumps [ts-loader](https://github.com/TypeStrong/ts-loader) from 8.0.4 to 8.0.11.
- [Release notes](https://github.com/TypeStrong/ts-loader/releases)
- [Changelog](https://github.com/TypeStrong/ts-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/TypeStrong/ts-loader/compare/8.0.4...v8.0.11)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-11-20 08:29:41 +00:00
Krishna Acondy
b78e8617c4 Merge pull request #151 from sasjs/dependabot/npm_and_yarn/semantic-release-17.2.3
[security] chore(deps-dev): bump semantic-release from 17.1.2 to 17.2.3
2020-11-20 08:27:32 +00:00
dependabot-preview[bot]
3ce9ca0986 chore(deps-dev): bump semantic-release from 17.1.2 to 17.2.3
Bumps [semantic-release](https://github.com/semantic-release/semantic-release) from 17.1.2 to 17.2.3.
- [Release notes](https://github.com/semantic-release/semantic-release/releases)
- [Commits](https://github.com/semantic-release/semantic-release/compare/v17.1.2...v17.2.3)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-11-20 08:19:07 +00:00
Yury Shkoda
04d17c3680 Merge pull request #153 from sasjs/context-issue
fix(context): fixed log parsing
2020-11-19 16:53:48 +03:00
Yury Shkoda
d26e15f91c Merge branch 'master' into context-issue 2020-11-19 16:43:30 +03:00
Yury Shkoda
83c46091b3 fix(context): fixed log parsing 2020-11-19 16:39:47 +03:00
Krishna Acondy
d640d7c040 Merge pull request #152 from sasjs/issue117
fix: viya login issue
2020-11-18 17:16:26 +00:00
Mihajlo Medjedovic
c934eb2b08 test: added test for multiple login attempts 2020-11-18 14:13:13 +01:00
Mihajlo Medjedovic
24dd5e32ad style: lint 2020-11-18 13:10:29 +01:00
Mihajlo Medjedovic
a23103b2c3 fix: viya login issue 2020-11-18 13:09:49 +01:00
Yury Shkoda
35aa4235e4 Merge pull request #148 from sasjs/issue113
feat: service not found error handling
2020-11-16 09:54:03 +03:00
Mihajlo Medjedovic
e9be1cf99a fix: service not found error handling 2020-11-12 16:03:32 +01:00
Mihajlo Medjedovic
c7b0821081 style: lint 2020-11-09 18:24:10 +01:00
Mihajlo Medjedovic
4a4618dd32 feat: service not found error handling for SAS9 2020-11-09 18:19:39 +01:00
Krishna Acondy
d223e83c60 Merge pull request #142 from sasjs/issue138
fix(file-uploader): handle errors during file upload
2020-11-02 14:54:58 +00:00
Krishna Acondy
d1f1a20126 chore(file-uploader): move uploader to describe scope 2020-11-02 09:29:33 +00:00
Krishna Acondy
4b89e3762f chore(file-uploader): remove duplication 2020-11-02 08:54:26 +00:00
Krishna Acondy
bc110288de chore(file-uploader): improve mocking of fetch, add tests for all error scenarios 2020-11-02 08:51:27 +00:00
Krishna Acondy
e94e16b52c chore(*): fix linting errors 2020-11-02 07:55:48 +00:00
Krishna Acondy
76aacee016 Merge branch 'master' into issue138 2020-11-02 07:42:39 +00:00
Mihajlo Medjedovic
1a3bd5d1f5 chore: lint 2020-10-30 16:13:03 +01:00
Mihajlo Medjedovic
3f6e89d716 fix: file uploader error handling and tests 2020-10-30 16:11:50 +01:00
Yury Shkoda
361ec84638 Merge pull request #141 from sasjs/fetch-log-feat
chore(log): made 'fetchLogFileContent' method public
2020-10-30 11:28:07 +03:00
Yury Shkoda
35cc1e4f62 chore(fetchLogFileContent): made accessToken optional 2020-10-30 11:26:43 +03:00
Yury Shkoda
64a976e888 doc: updated docs 2020-10-30 10:36:53 +03:00
Yury Shkoda
7e2cb8491f feat(log): made 'fetchLogFileContent' method public 2020-10-30 10:36:04 +03:00
Krishna Acondy
2cdab7522d Merge pull request #139 from sasjs/location-issue
fix(location): added handle cases when 'location' is not defined
2020-10-29 08:07:11 +00:00
Yury Shkoda
a07eabc408 fix(location): added handle cases when 'location' is not defined 2020-10-29 10:07:30 +03:00
Mihajlo Medjedovic
7279c23fe2 fix: FIleUploader added catch 2020-10-27 14:50:05 +01:00
Mihajlo Medjedovic
80707d77d9 gitfe Merge branches 'errorResponse' and 'master' of github.com:sasjs/adapter 2020-10-27 14:40:41 +01:00
Yury Shkoda
d5920c5885 Merge pull request #134 from sasjs/executeComputeJob
fix(executeComputeJob): added fix for cases when code was not provided
2020-10-21 11:55:43 +03:00
Yury Shkoda
6a3a6b4485 fix(executeComputeJob): added fix for cases when code was not provided 2020-10-21 11:45:21 +03:00
Krishna Acondy
2b1df0c61a Merge pull request #123 from sasjs/sasjs-job
feat(start-compute-job): Add API that returns immediately after job is started
2020-10-16 11:27:02 +01:00
Krishna Acondy
216725f306 chore(doc): update documentation 2020-10-16 11:04:03 +01:00
Krishna Acondy
3183f89a62 chore(*): fix lint warning 2020-10-16 10:58:04 +01:00
Krishna Acondy
f5cc16c3bd chore(create-job): add tests 2020-10-16 10:56:10 +01:00
Krishna Acondy
e78dc76e56 fix(config): set debug to false by default
feat(create-job): add the ability to wait for result
2020-10-16 10:55:56 +01:00
Krishna Acondy
bfdb5ef0a6 chore(*): regenerate documentation 2020-10-16 09:13:48 +01:00
Krishna Acondy
35353d3fce Merge branch 'master' into sasjs-job 2020-10-15 09:11:50 +01:00
Yury Shkoda
7a02c8ad34 Merge pull request #131 from sasjs/issue-124
fix(session): add internal SAS error handler
2020-10-14 14:03:58 +03:00
Yury Shkoda
331d9b0010 fix(session): add internal SAS error handler 2020-10-14 12:53:59 +03:00
Yury Shkoda
ef5686cce7 Merge branch 'master' into sasjs-job 2020-10-12 09:21:00 +03:00
Yury Shkoda
fa87111f4a Merge pull request #126 from sasjs/issue-124
fix(context): fixed 'getExecutableContexts' method
2020-10-07 17:53:31 +03:00
Yury Shkoda
94967b0f6c fix(context): fixed 'getExecutableContexts' method 2020-10-07 17:25:47 +03:00
Mihajlo Medjedovic
3f796b300d fix: ErrorResponse body changed to error 2020-10-07 11:15:00 +02:00
Krishna Acondy
a07c16fb52 chore(start-compute-job): add test 2020-10-06 09:21:58 +01:00
Krishna Acondy
fd6905ea9f feat(start-compute-job): add API that starts a compute job and immediately returns the session 2020-10-06 09:21:15 +01:00
Krishna Acondy
08f58b5f4f fix(debug): only set session manager debug if it is defined 2020-10-06 08:17:02 +01:00
Krishna Acondy
bd8012fe3e fix(*): revert to older version of isomorphic-fetch 2020-10-03 18:19:06 +01:00
Krishna Acondy
fa531b34fd Merge pull request #120 from sasjs/session-manager-debug
fix(debug): propagate debug value from SASjs config
2020-10-03 17:41:35 +01:00
Krishna Acondy
354443c98b fix(debug): propagate debug value from SASjs config 2020-10-03 16:53:00 +01:00
Krishna Acondy
ee30ab195f Merge pull request #115 from sasjs/issue-114
chore(error-message): updated error message for forbidden request
2020-10-01 09:10:35 +01:00
Yury Shkoda
02c1712d22 chore(error-message): updated error message for forbidden request 2020-10-01 09:49:24 +03:00
Krishna Acondy
37def7a956 Merge pull request #111 from sasjs/dependabot/npm_and_yarn/isomorphic-fetch-3.0.0
chore(deps): bump isomorphic-fetch from 2.2.1 to 3.0.0
2020-09-29 20:03:06 +01:00
Krishna Acondy
653e3d05e0 Merge branch 'master' into dependabot/npm_and_yarn/isomorphic-fetch-3.0.0 2020-09-29 19:55:08 +01:00
Yury Shkoda
d8467c24b1 Merge pull request #112 from sasjs/cli-issue-73
feat(folder-management): made folder related methods public
2020-09-28 15:16:33 +03:00
Yury Shkoda
fc9056c1ac chore(folder-management): made 'moveFolder' method public and fixed 'createFolder' method 2020-09-28 14:59:27 +03:00
Yury Shkoda
9b1d295b82 feat(folder): made 'deleteFolder' method public 2020-09-26 11:41:18 +03:00
dependabot-preview[bot]
e2ea3f4ddc chore(deps): bump isomorphic-fetch from 2.2.1 to 3.0.0
Bumps [isomorphic-fetch](https://github.com/matthew-andrews/isomorphic-fetch) from 2.2.1 to 3.0.0.
- [Release notes](https://github.com/matthew-andrews/isomorphic-fetch/releases)
- [Commits](https://github.com/matthew-andrews/isomorphic-fetch/compare/v2.2.1...v3.0.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-09-25 23:51:17 +00:00
Allan Bowe
99d0b01a24 Update example.html 2020-09-24 23:00:37 +02:00
34 changed files with 10201 additions and 1889 deletions

View File

@@ -27,6 +27,16 @@ jobs:
run: npm run lint run: npm run lint
- name: Run unit tests - name: Run unit tests
run: npm test run: npm test
env:
CI: true
CLIENT: ${{secrets.CLIENT}}
SECRET: ${{secrets.SECRET}}
SAS_USERNAME: ${{secrets.SAS_USERNAME}}
SAS_PASSWORD: ${{secrets.SAS_PASSWORD}}
SERVER_URL: ${{secrets.SERVER_URL}}
SERVER_TYPE: ${{secrets.SERVER_TYPE}}
ACCESS_TOKEN: ${{secrets.ACCESS_TOKEN}}
REFRESH_TOKEN: ${{secrets.REFRESH_TOKEN}}
- name: Build Package - name: Build Package
run: npm run package:lib run: npm run package:lib
env: env:

View File

@@ -14,4 +14,5 @@ What code changes have been made to achieve the intent.
- [ ] Code is formatted correctly (`npm run lint:fix`). - [ ] Code is formatted correctly (`npm run lint:fix`).
- [ ] All unit tests are passing (`npm test`). - [ ] All unit tests are passing (`npm test`).
- [ ] All `sasjs-tests` unit tests are passing (`npm test`).
- [ ] All `sasjs-tests` are passing (instructions available [here](https://github.com/sasjs/adapter/blob/master/sasjs-tests/README.md)). - [ ] All `sasjs-tests` are passing (instructions available [here](https://github.com/sasjs/adapter/blob/master/sasjs-tests/README.md)).

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -76,7 +76,7 @@
<section class="tsd-index-section "> <section class="tsd-index-section ">
<h3>Modules</h3> <h3>Modules</h3>
<ul class="tsd-index-list"> <ul class="tsd-index-list">
<li class="tsd-kind-module tsd-is-not-exported"><a href="modules/reflection-762.html" class="tsd-kind-icon"><em>Module</em></a></li> <li class="tsd-kind-module tsd-is-not-exported"><a href="modules/reflection-790.html" class="tsd-kind-icon"><em>Module</em></a></li>
<li class="tsd-kind-module"><a href="modules/types.html" class="tsd-kind-icon">types</a></li> <li class="tsd-kind-module"><a href="modules/types.html" class="tsd-kind-icon">types</a></li>
<li class="tsd-kind-module"><a href="modules/utils.html" class="tsd-kind-icon">utils</a></li> <li class="tsd-kind-module"><a href="modules/utils.html" class="tsd-kind-icon">utils</a></li>
</ul> </ul>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -30,8 +30,8 @@
$('#chart-container').append('<canvas id="myChart" style="display: none;"></canvas>') $('#chart-container').append('<canvas id="myChart" style="display: none;"></canvas>')
// make a request to a SAS service // make a request to a SAS service
var type = $("#cars")[0].options[$("#cars")[0].selectedIndex].value; var type = $("#cars")[0].options[$("#cars")[0].selectedIndex].value;
// request data from an endpoint under your appLoc // request data from an endpoint under your appLoc (missing opening slash implies relative path)
sasJs.request("/common/getdata", { sasJs.request("common/getdata", {
// send data as an array of objects - each object is one row // send data as an array of objects - each object is one row
fromjs: [{ type: type }] fromjs: [{ type: type }]
}).then((response) => { }).then((response) => {

2610
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -37,15 +37,16 @@
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"@types/isomorphic-fetch": "0.0.35", "@types/isomorphic-fetch": "0.0.35",
"@types/jest": "^26.0.14", "@types/jest": "^26.0.15",
"cp": "^0.2.0", "cp": "^0.2.0",
"dotenv": "^8.2.0",
"jest": "^25.5.4", "jest": "^25.5.4",
"path": "^0.12.7", "path": "^0.12.7",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"semantic-release": "^17.1.2", "semantic-release": "^17.2.3",
"terser-webpack-plugin": "^4.2.2", "terser-webpack-plugin": "^4.2.3",
"ts-jest": "^25.5.1", "ts-jest": "^25.5.1",
"ts-loader": "^8.0.4", "ts-loader": "^8.0.11",
"tslint": "^6.1.3", "tslint": "^6.1.3",
"tslint-config-prettier": "^1.18.0", "tslint-config-prettier": "^1.18.0",
"typedoc": "^0.17.8", "typedoc": "^0.17.8",
@@ -53,7 +54,7 @@
"typedoc-plugin-external-module-name": "^4.0.3", "typedoc-plugin-external-module-name": "^4.0.3",
"typescript": "^3.9.7", "typescript": "^3.9.7",
"webpack": "^4.44.2", "webpack": "^4.44.2",
"webpack-cli": "^3.3.12" "webpack-cli": "^4.2.0"
}, },
"main": "index.js", "main": "index.js",
"dependencies": { "dependencies": {

View File

@@ -5,6 +5,7 @@ import { sendArrTests, sendObjTests } from "./testSuites/RequestData";
import { specialCaseTests } from "./testSuites/SpecialCases"; import { specialCaseTests } from "./testSuites/SpecialCases";
import { sasjsRequestTests } from "./testSuites/SasjsRequests"; import { sasjsRequestTests } from "./testSuites/SasjsRequests";
import "@sasjs/test-framework/dist/index.css"; import "@sasjs/test-framework/dist/index.css";
import { computeTests } from "./testSuites/Compute";
const App = (): ReactElement<{}> => { const App = (): ReactElement<{}> => {
const { adapter, config } = useContext(AppContext); const { adapter, config } = useContext(AppContext);
@@ -17,7 +18,8 @@ const App = (): ReactElement<{}> => {
sendArrTests(adapter), sendArrTests(adapter),
sendObjTests(adapter), sendObjTests(adapter),
specialCaseTests(adapter), specialCaseTests(adapter),
sasjsRequestTests(adapter) sasjsRequestTests(adapter),
computeTests(adapter)
]); ]);
} }
}, [adapter, config]); }, [adapter, config]);

View File

@@ -37,6 +37,17 @@ export const basicTests = (
assertion: (response: any) => assertion: (response: any) =>
response && response.isLoggedIn && response.userName === userName response && response.isLoggedIn && response.userName === userName
}, },
{
title: "Multiple Log in attempts",
description: "Should fail on first attempt and should log the user in on second attempt",
test: async () => {
await adapter.logOut()
await adapter.logIn('invalid', 'invalid')
return adapter.logIn(userName, password)
},
assertion: (response: any) =>
response && response.isLoggedIn && response.userName === userName
},
{ {
title: "Default config", title: "Default config",
description: description:

View File

@@ -0,0 +1,41 @@
import SASjs from "@sasjs/adapter";
import { TestSuite } from "@sasjs/test-framework";
export const computeTests = (adapter: SASjs): TestSuite => ({
name: "Compute",
tests: [
{
title: "Start Compute Job - not waiting for result",
description: "Should start a compute job and return the session",
test: () => {
const data: any = { table1: [{ col1: "first col value" }] };
return adapter.startComputeJob("/Public/app/common/sendArr", data);
},
assertion: (res: any) => {
const expectedProperties = ["id", "applicationName", "attributes"]
return validate(expectedProperties, res);
}
},
{
title: "Start Compute Job - waiting for result",
description: "Should start a compute job and return the job",
test: () => {
const data: any = { table1: [{ col1: "first col value" }] };
return adapter.startComputeJob("/Public/app/common/sendArr", data, {}, "", true);
},
assertion: (res: any) => {
const expectedProperties = ["id", "state", "creationTimeStamp", "jobConditionCode"]
return validate(expectedProperties, res);
}
}
]
});
const validate = (expectedProperties: string[], data: any): boolean => {
const actualProperties = Object.keys(data);
const isValid = expectedProperties.every(
(property) => actualProperties.includes(property)
);
return isValid
}

View File

@@ -1,6 +1,7 @@
import { isLogInRequired, needsRetry, isUrl } from './utils' import { isLogInRequired, needsRetry, isUrl } from './utils'
import { CsrfToken } from './types/CsrfToken' import { CsrfToken } from './types/CsrfToken'
import { UploadFile } from './types/UploadFile' import { UploadFile } from './types/UploadFile'
import { ErrorResponse } from './types'
const requestRetryLimit = 5 const requestRetryLimit = 5
@@ -18,29 +19,31 @@ export class FileUploader {
private retryCount = 0 private retryCount = 0
public uploadFile(sasJob: string, files: UploadFile[], params: any) { public uploadFile(sasJob: string, files: UploadFile[], params: any) {
if (files?.length < 1)
throw new Error('At least one file must be provided.')
let paramsString = ''
for (let param in params) {
if (params.hasOwnProperty(param)) {
paramsString += `&${param}=${params[param]}`
}
}
const program = this.appLoc
? this.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
: sasJob
const uploadUrl = `${this.serverUrl}${this.jobsPath}/?${
'_program=' + program
}${paramsString}`
const headers = {
'cache-control': 'no-cache'
}
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (files?.length < 1)
reject(new ErrorResponse('At least one file must be provided.'))
if (!sasJob || sasJob === '')
reject(new ErrorResponse('sasJob must be provided.'))
let paramsString = ''
for (let param in params) {
if (params.hasOwnProperty(param)) {
paramsString += `&${param}=${params[param]}`
}
}
const program = this.appLoc
? this.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
: sasJob
const uploadUrl = `${this.serverUrl}${this.jobsPath}/?${
'_program=' + program
}${paramsString}`
const headers = {
'cache-control': 'no-cache'
}
const formData = new FormData() const formData = new FormData()
for (let file of files) { for (let file of files) {
@@ -76,7 +79,7 @@ export class FileUploader {
}) })
.then((responseText) => { .then((responseText) => {
if (isLogInRequired(responseText)) if (isLogInRequired(responseText))
reject('You must be logged in to upload a file') reject(new ErrorResponse('You must be logged in to upload a file.'))
if (needsRetry(responseText)) { if (needsRetry(responseText)) {
if (this.retryCount < requestRetryLimit) { if (this.retryCount < requestRetryLimit) {
@@ -95,10 +98,18 @@ export class FileUploader {
try { try {
resolve(JSON.parse(responseText)) resolve(JSON.parse(responseText))
} catch (e) { } catch (e) {
reject(e) reject(
new ErrorResponse(
'Error while parsing json from upload response.',
e
)
)
} }
} }
}) })
.catch((err: any) => {
reject(new ErrorResponse('Upload request failed.', err))
})
}) })
} }
} }

View File

@@ -38,14 +38,25 @@ export class SASViyaApiClient {
private csrfToken: CsrfToken | null = null private csrfToken: CsrfToken | null = null
private fileUploadCsrfToken: CsrfToken | null = null private fileUploadCsrfToken: CsrfToken | null = null
private _debug = false
private sessionManager = new SessionManager( private sessionManager = new SessionManager(
this.serverUrl, this.serverUrl,
this.contextName, this.contextName,
this.setCsrfToken this.setCsrfToken
) )
private isForceDeploy: boolean = false
private folderMap = new Map<string, Job[]>() private folderMap = new Map<string, Job[]>()
public get debug() {
return this._debug
}
public set debug(value: boolean) {
this._debug = value
if (this.sessionManager) {
this.sessionManager.debug = value
}
}
/** /**
* Returns a list of jobs in the currently set root folder. * Returns a list of jobs in the currently set root folder.
*/ */
@@ -136,42 +147,51 @@ export class SASViyaApiClient {
const promises = contextsList.map((context: any) => { const promises = contextsList.map((context: any) => {
const linesOfCode = ['%put &=sysuserid;'] const linesOfCode = ['%put &=sysuserid;']
return this.executeScript( return () =>
`test-${context.name}`, this.executeScript(
linesOfCode, `test-${context.name}`,
context.name, linesOfCode,
accessToken, context.name,
false, accessToken,
null, null,
true true,
).catch(() => null) true
).catch((err) => err)
}) })
const results = await Promise.all(promises) let results: any[] = []
for (const promise of promises) results.push(await promise())
results.forEach((result: any, index: number) => { results.forEach((result: any, index: number) => {
if (result) { if (result && result.error && result.error.details) {
let sysUserId = '' try {
const resultParsed = result.error.details
if (result.log) { if (resultParsed && resultParsed.body) {
const sysUserIdLog = result.log let sysUserId = ''
.split('\n')
.find((line: string) => line.startsWith('SYSUSERID='))
if (sysUserIdLog) { const sysUserIdLog = resultParsed.body
sysUserId = sysUserIdLog.replace('SYSUSERID=', '') .split('\n')
.find((line: string) => line.startsWith('SYSUSERID='))
if (sysUserIdLog) {
sysUserId = sysUserIdLog.replace('SYSUSERID=', '')
executableContexts.push({
createdBy: contextsList[index].createdBy,
id: contextsList[index].id,
name: contextsList[index].name,
version: contextsList[index].version,
attributes: {
sysUserId
}
})
}
} }
} catch (error) {
throw error
} }
executableContexts.push({
createdBy: contextsList[index].createdBy,
id: contextsList[index].id,
name: contextsList[index].name,
version: contextsList[index].version,
attributes: {
sysUserId
}
})
} }
}) })
@@ -319,7 +339,9 @@ export class SASViyaApiClient {
originalContext = await this.getComputeContextByName( originalContext = await this.getComputeContextByName(
contextName, contextName,
accessToken accessToken
).catch((_) => {}) ).catch((err) => {
throw err
})
// Try to find context by id, when context name has been changed. // Try to find context by id, when context name has been changed.
if (!originalContext) { if (!originalContext) {
@@ -405,7 +427,6 @@ export class SASViyaApiClient {
* @param contextName - the context to execute the code in. * @param contextName - the context to execute the code in.
* @param accessToken - an access token for an authorized user. * @param accessToken - an access token for an authorized user.
* @param sessionId - optional session ID to reuse. * @param sessionId - optional session ID to reuse.
* @param silent - optional flag to disable logging.
* @param data - execution data. * @param data - execution data.
* @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).
@@ -415,12 +436,10 @@ export class SASViyaApiClient {
linesOfCode: string[], linesOfCode: string[],
contextName: string, contextName: string,
accessToken?: string, accessToken?: string,
silent = false,
data = null, data = null,
debug = false, expectWebout = false,
expectWebout = false waitForResult = true
): Promise<any> { ): Promise<any> {
silent = !debug
try { try {
const headers: any = { const headers: any = {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@@ -431,7 +450,12 @@ export class SASViyaApiClient {
} }
let executionSessionId: string let executionSessionId: string
const session = await this.sessionManager.getSession(accessToken) const session = await this.sessionManager
.getSession(accessToken)
.catch((err) => {
throw err
})
executionSessionId = session!.id executionSessionId = session!.id
const jobArguments: { [key: string]: any } = { const jobArguments: { [key: string]: any } = {
@@ -443,7 +467,7 @@ export class SASViyaApiClient {
_OMITTEXTLOG: true _OMITTEXTLOG: true
} }
if (debug) { if (this.debug) {
jobArguments['_OMITTEXTLOG'] = false jobArguments['_OMITTEXTLOG'] = false
jobArguments['_OMITSESSIONRESULTS'] = false jobArguments['_OMITSESSIONRESULTS'] = false
jobArguments['_DEBUG'] = 131 jobArguments['_DEBUG'] = 131
@@ -470,7 +494,9 @@ export class SASViyaApiClient {
if (data) { if (data) {
if (JSON.stringify(data).includes(';')) { if (JSON.stringify(data).includes(';')) {
files = await this.uploadTables(data, accessToken) files = await this.uploadTables(data, accessToken).catch((err) => {
throw err
})
jobVariables['_webin_file_count'] = files.length jobVariables['_webin_file_count'] = files.length
@@ -501,9 +527,15 @@ export class SASViyaApiClient {
const { result: postedJob, etag } = await this.request<Job>( const { result: postedJob, etag } = await this.request<Job>(
`${this.serverUrl}/compute/sessions/${executionSessionId}/jobs`, `${this.serverUrl}/compute/sessions/${executionSessionId}/jobs`,
postJobRequest postJobRequest
) ).catch((err) => {
throw err
})
if (!silent) { if (!waitForResult) {
return session
}
if (this.debug) {
console.log(`Job has been submitted for '${fileName}'.`) console.log(`Job has been submitted for '${fileName}'.`)
console.log( console.log(
`You can monitor the job progress at '${this.serverUrl}${ `You can monitor the job progress at '${this.serverUrl}${
@@ -512,32 +544,33 @@ export class SASViyaApiClient {
) )
} }
const jobStatus = await this.pollJobState( const jobStatus = await this.pollJobState(postedJob, etag, accessToken)
postedJob,
etag,
accessToken,
silent
)
const { result: currentJob } = await this.request<Job>( const { result: currentJob } = await this.request<Job>(
`${this.serverUrl}/compute/sessions/${executionSessionId}/jobs/${postedJob.id}`, `${this.serverUrl}/compute/sessions/${executionSessionId}/jobs/${postedJob.id}`,
{ headers } { headers }
) ).catch((err) => {
throw err
})
let jobResult let jobResult
let log let log
const logLink = currentJob.links.find((l) => l.rel === 'log') const logLink = currentJob.links.find((l) => l.rel === 'log')
if (debug && logLink) { if (this.debug && logLink) {
log = await this.request<any>( log = await this.request<any>(
`${this.serverUrl}${logLink.href}/content?limit=10000`, `${this.serverUrl}${logLink.href}/content?limit=10000`,
{ {
headers headers
} }
).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')
)
.catch((err) => {
throw err
})
} }
if (jobStatus === 'failed' || jobStatus === 'error') { if (jobStatus === 'failed' || jobStatus === 'error') {
@@ -548,6 +581,8 @@ export class SASViyaApiClient {
if (expectWebout) { if (expectWebout) {
resultLink = `/compute/sessions/${executionSessionId}/filerefs/_webout/content` resultLink = `/compute/sessions/${executionSessionId}/filerefs/_webout/content`
} else {
return currentJob
} }
if (resultLink) { if (resultLink) {
@@ -563,12 +598,16 @@ export class SASViyaApiClient {
{ {
headers headers
} }
).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')
)
.catch((err) => {
throw err
})
return Promise.reject( return Promise.reject(
new ErrorResponse('Job execution failed', { new ErrorResponse('Job execution failed.', {
status: 500, status: 500,
body: log body: log
}) })
@@ -581,7 +620,11 @@ export class SASViyaApiClient {
}) })
} }
await this.sessionManager.clearSession(executionSessionId, accessToken) await this.sessionManager
.clearSession(executionSessionId, accessToken)
.catch((err) => {
throw err
})
return { result: jobResult?.result, log } return { result: jobResult?.result, log }
} catch (e) { } catch (e) {
@@ -591,9 +634,9 @@ export class SASViyaApiClient {
linesOfCode, linesOfCode,
contextName, contextName,
accessToken, accessToken,
silent,
data, data,
debug false,
true
) )
} else { } else {
throw e throw e
@@ -625,8 +668,6 @@ export class SASViyaApiClient {
if (!parentFolderUri && parentFolderPath) { if (!parentFolderUri && parentFolderPath) {
parentFolderUri = await this.getFolderUri(parentFolderPath, accessToken) parentFolderUri = await this.getFolderUri(parentFolderPath, accessToken)
if (!parentFolderUri) { if (!parentFolderUri) {
if (isForced) this.isForceDeploy = true
console.log( console.log(
`Parent folder at path '${parentFolderPath}' is not present.` `Parent folder at path '${parentFolderPath}' is not present.`
) )
@@ -652,37 +693,16 @@ export class SASViyaApiClient {
`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 && !this.isForceDeploy) { } else if (isForced && accessToken) {
this.isForceDeploy = true const folderPath = parentFolderPath + '/' + folderName
const folderUri = await this.getFolderUri(folderPath, accessToken)
await this.deleteFolder(parentFolderPath, accessToken) if (folderUri) {
await this.deleteFolder(
const newParentFolderPath = parentFolderPath.substring( parentFolderPath + '/' + folderName,
0, accessToken
parentFolderPath.lastIndexOf('/') )
)
const newFolderName = `${parentFolderPath.split('/').pop()}`
if (newParentFolderPath === '') {
throw new Error(`Root folder has to be present on the server.`)
} }
console.log(
`Creating parent folder:\n'${newFolderName}' in '${newParentFolderPath}'`
)
const parentFolder = await this.createFolder(
newFolderName,
newParentFolderPath,
undefined,
accessToken
)
console.log(
`Parent folder '${newFolderName}' has been successfully created.`
)
parentFolderUri = `/folders/folders/${parentFolder.id}`
} }
} }
@@ -929,13 +949,16 @@ export class SASViyaApiClient {
* @param debug - sets the _debug flag in the job arguments. * @param debug - sets the _debug flag in the job arguments.
* @param data - any data to be passed in as input to the job. * @param data - any data to be passed in as input to the job.
* @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 expectWebout - a boolean indicating whether to expect a _webout response.
*/ */
public async executeComputeJob( public async executeComputeJob(
sasJob: string, sasJob: string,
contextName: string, contextName: string,
debug: boolean,
data?: any, data?: any,
accessToken?: string accessToken?: string,
waitForResult = true,
expectWebout = false
) { ) {
if (isRelativePath(sasJob) && !this.rootFolderName) { if (isRelativePath(sasJob) && !this.rootFolderName) {
throw new Error( throw new Error(
@@ -1009,16 +1032,17 @@ export class SASViyaApiClient {
jobToExecute.code = code jobToExecute.code = code
} }
if (!code) code = ''
const linesToExecute = code.replace(/\r\n/g, '\n').split('\n') const linesToExecute = code.replace(/\r\n/g, '\n').split('\n')
return await this.executeScript( return await this.executeScript(
sasJob, sasJob,
linesToExecute, linesToExecute,
contextName, contextName,
accessToken, accessToken,
true,
data, data,
debug, expectWebout,
true waitForResult
) )
} }
@@ -1090,7 +1114,7 @@ export class SASViyaApiClient {
} }
if (!jobToExecute) { if (!jobToExecute) {
throw new Error(`The job ${sasJob} 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'
@@ -1149,12 +1173,7 @@ export class SASViyaApiClient {
`${this.serverUrl}/jobExecution/jobs?_action=wait`, `${this.serverUrl}/jobExecution/jobs?_action=wait`,
postJobRequest postJobRequest
) )
const jobStatus = await this.pollJobState( const jobStatus = await this.pollJobState(postedJob, etag, accessToken)
postedJob,
etag,
accessToken,
true
)
const { result: currentJob } = await this.request<Job>( const { result: currentJob } = await this.request<Job>(
`${this.serverUrl}/jobExecution/jobs/${postedJob.id}`, `${this.serverUrl}/jobExecution/jobs/${postedJob.id}`,
{ headers } { headers }
@@ -1219,8 +1238,7 @@ export class SASViyaApiClient {
private async pollJobState( private async pollJobState(
postedJob: any, postedJob: any,
etag: string | null, etag: string | null,
accessToken?: string, accessToken?: string
silent = false
) { ) {
const MAX_POLL_COUNT = 1000 const MAX_POLL_COUNT = 1000
const POLL_INTERVAL = 100 const POLL_INTERVAL = 100
@@ -1259,7 +1277,7 @@ export class SASViyaApiClient {
postedJobState === 'pending' postedJobState === 'pending'
) { ) {
if (stateLink) { if (stateLink) {
if (!silent) { if (this.debug) {
console.log('Polling job status... \n') console.log('Polling job status... \n')
} }
const { result: jobState } = await this.request<string>( const { result: jobState } = await this.request<string>(
@@ -1271,7 +1289,7 @@ export class SASViyaApiClient {
) )
postedJobState = jobState.trim() postedJobState = jobState.trim()
if (!silent) { if (this.debug) {
console.log(`Current state: ${postedJobState}\n`) console.log(`Current state: ${postedJobState}\n`)
} }
pollCount++ pollCount++
@@ -1287,49 +1305,6 @@ export class SASViyaApiClient {
}) })
} }
private async waitForSession(
session: Session,
etag: string | null,
accessToken?: string,
silent = false
) {
let sessionState = session.state
let pollCount = 0
const headers: any = {
'Content-Type': 'application/json',
'If-None-Match': etag
}
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`
}
const stateLink = session.links.find((l: any) => l.rel === 'state')
return new Promise(async (resolve, _) => {
if (sessionState === 'pending') {
if (stateLink) {
if (!silent) {
console.log('Polling session status... \n')
}
const { result: state } = await this.request<string>(
`${this.serverUrl}${stateLink.href}?wait=30`,
{
headers
},
'text'
)
sessionState = state.trim()
if (!silent) {
console.log(`Current state: ${sessionState}\n`)
}
pollCount++
resolve(sessionState)
}
} else {
resolve(sessionState)
}
})
}
private async uploadTables(data: any, accessToken?: string) { private async uploadTables(data: any, accessToken?: string) {
const uploadedFiles = [] const uploadedFiles = []
const headers: any = { const headers: any = {
@@ -1508,6 +1483,16 @@ export class SASViyaApiClient {
`${this.serverUrl}${url}`, `${this.serverUrl}${url}`,
requestInfo requestInfo
).catch((err) => { ).catch((err) => {
if (err.code && err.code === 'ENOTFOUND') {
const notFoundError = {
body: JSON.stringify({
message: `Folder '${sourceFolder.split('/').pop()}' was not found.`
})
}
throw notFoundError
}
throw err throw err
}) })

View File

@@ -44,7 +44,7 @@ const defaultConfig: SASjsConfig = {
pathSASViya: '/SASJobExecution', pathSASViya: '/SASJobExecution',
appLoc: '/Public/seedapp', appLoc: '/Public/seedapp',
serverType: ServerType.SASViya, serverType: ServerType.SASViya,
debug: true, debug: false,
contextName: 'SAS Job Execution compute context', contextName: 'SAS Job Execution compute context',
useComputeApi: false useComputeApi: false
} }
@@ -209,9 +209,7 @@ export default class SASjs {
fileName: string, fileName: string,
linesOfCode: string[], linesOfCode: string[],
contextName: string, contextName: string,
accessToken?: string, accessToken?: string
sessionId = '',
silent = false
) { ) {
this.isMethodSupported('executeScriptSASViya', ServerType.SASViya) this.isMethodSupported('executeScriptSASViya', ServerType.SASViya)
@@ -220,9 +218,7 @@ export default class SASjs {
linesOfCode, linesOfCode,
contextName, contextName,
accessToken, accessToken,
silent, null
null,
this.sasjsConfig.debug
) )
} }
@@ -243,8 +239,6 @@ export default class SASjs {
sasApiClient?: SASViyaApiClient, sasApiClient?: SASViyaApiClient,
isForced?: boolean isForced?: boolean
) { ) {
this.isMethodSupported('createFolder', ServerType.SASViya)
if (sasApiClient) if (sasApiClient)
return await sasApiClient.createFolder( return await sasApiClient.createFolder(
folderName, folderName,
@@ -261,6 +255,40 @@ export default class SASjs {
) )
} }
/**
* For performance (and in case of accidental error) the `deleteFolder` function does not actually delete the folder (and all its content and subfolder content). Instead the folder is simply moved to the recycle bin. Deletion time will be added to the folder name.
* @param folderPath - the full path (eg `/Public/example/deleteThis`) of the folder to be deleted.
* @param accessToken - an access token for authorizing the request.
*/
public async deleteFolder(folderPath: string, accessToken: string) {
this.isMethodSupported('deleteFolder', ServerType.SASViya)
return await this.sasViyaApiClient?.deleteFolder(folderPath, accessToken)
}
/**
* Moves folder to a new location. The folder may be renamed at the same time.
* @param sourceFolder - the full path (eg `/Public/example/myFolder`) or URI of the source folder to be moved. Providing URI instead of path will save one extra request.
* @param targetParentFolder - the full path or URI of the _parent_ folder to which the `sourceFolder` will be moved (eg `/Public/newDestination`). To move a folder, a user has to have write permissions in targetParentFolder. Providing URI instead of path will save one extra request.
* @param targetFolderName - the name of the "moved" folder. If left blank, the original folder name will be used (eg `myFolder` in `/Public/newDestination/myFolder` for the example above). Optional field.
* @param accessToken - an access token for authorizing the request.
*/
public async moveFolder(
sourceFolder: string,
targetParentFolder: string,
targetFolderName: string,
accessToken: string
) {
this.isMethodSupported('moveFolder', ServerType.SASViya)
return await this.sasViyaApiClient?.moveFolder(
sourceFolder,
targetParentFolder,
targetFolderName,
accessToken
)
}
public async createJobDefinition( public async createJobDefinition(
jobName: string, jobName: string,
code: string, code: string,
@@ -378,6 +406,32 @@ export default class SASjs {
*/ */
public setDebugState(value: boolean) { public setDebugState(value: boolean) {
this.sasjsConfig.debug = value this.sasjsConfig.debug = value
if (this.sasViyaApiClient) {
this.sasViyaApiClient.debug = value
}
}
private async getLoginForm(response: any) {
const pattern: RegExp = /<form.+action="(.*Logon[^"]*).*>/
const matches = pattern.exec(response)
const formInputs: any = {}
if (matches && matches.length) {
this.setLoginUrl(matches)
const inputs = response.match(/<input.*"hidden"[^>]*>/g)
if (inputs) {
inputs.forEach((inputStr: string) => {
const valueMatch = inputStr.match(/name="([^"]*)"\svalue="([^"]*)/)
if (valueMatch && valueMatch.length) {
formInputs[valueMatch[1]] = valueMatch[2]
}
})
}
}
return Object.keys(formInputs).length ? formInputs : null
} }
/** /**
@@ -388,10 +442,16 @@ export default class SASjs {
const loginResponse = await fetch(this.loginUrl.replace('.do', '')) const loginResponse = await fetch(this.loginUrl.replace('.do', ''))
const responseText = await loginResponse.text() const responseText = await loginResponse.text()
const isLoggedIn = /<button.+onClick.+logout/gm.test(responseText) const isLoggedIn = /<button.+onClick.+logout/gm.test(responseText)
let loginForm: any = null
if (!isLoggedIn) {
loginForm = await this.getLoginForm(responseText)
}
return Promise.resolve({ return Promise.resolve({
isLoggedIn, isLoggedIn,
userName: this.userName userName: this.userName,
loginForm
}) })
} }
@@ -409,7 +469,7 @@ export default class SASjs {
this.userName = loginParams.username this.userName = loginParams.username
const { isLoggedIn } = await this.checkSession() const { isLoggedIn, loginForm } = await this.checkSession()
if (isLoggedIn) { if (isLoggedIn) {
this.resendWaitingRequests() this.resendWaitingRequests()
@@ -419,15 +479,13 @@ export default class SASjs {
}) })
} }
const loginForm = await this.getLoginForm()
for (const key in loginForm) { for (const key in loginForm) {
loginParams[key] = loginForm[key] loginParams[key] = loginForm[key]
} }
const loginParamsStr = serialize(loginParams) const loginParamsStr = serialize(loginParams)
return fetch(this.loginUrl, { return fetch(this.loginUrl, {
method: 'post', method: 'POST',
credentials: 'include', credentials: 'include',
referrerPolicy: 'same-origin', referrerPolicy: 'same-origin',
body: loginParamsStr, body: loginParamsStr,
@@ -603,6 +661,7 @@ export default class SASjs {
this.sasjsConfig.contextName, this.sasjsConfig.contextName,
this.setCsrfTokenApi this.setCsrfTokenApi
) )
sasApiClient.debug = this.sasjsConfig.debug
} else if (this.sasjsConfig.serverType === ServerType.SAS9) { } else if (this.sasjsConfig.serverType === ServerType.SAS9) {
sasApiClient = new SAS9ApiClient(serverUrl) sasApiClient = new SAS9ApiClient(serverUrl)
} }
@@ -638,6 +697,50 @@ export default class SASjs {
) )
} }
/**
* Kicks off execution of the given job via the compute API.
* @returns an object representing the compute session created for the given job.
* @param sasJob - the path to the SAS program (ultimately resolves to
* the SAS `_program` parameter to run a Job Definition or SAS 9 Stored
* Process). Is prepended at runtime with the value of `appLoc`.
* @param data - a JSON object containing one or more tables to be sent to
* SAS. Can be `null` if no inputs required.
* @param config - provide any changes to the config here, for instance to
* enable/disable `debug`. Any change provided will override the global config,
* for that particular function call.
* @param accessToken - a valid access token that is authorised to execute compute jobs.
* 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.
*/
public async startComputeJob(
sasJob: string,
data: any,
config: any = {},
accessToken?: string,
waitForResult?: boolean
) {
config = {
...this.sasjsConfig,
...config
}
this.isMethodSupported('startComputeJob', ServerType.SASViya)
if (!config.contextName) {
throw new Error(
'Context name is undefined. Please set a `contextName` in your SASjs or override config.'
)
}
return this.sasViyaApiClient?.executeComputeJob(
sasJob,
config.contextName,
data,
accessToken,
!!waitForResult,
false
)
}
private async executeJobViaComputeApi( private async executeJobViaComputeApi(
sasJob: string, sasJob: string,
data: any, data: any,
@@ -657,13 +760,16 @@ export default class SASjs {
sasjsWaitingRequest.requestPromise.promise = new Promise( sasjsWaitingRequest.requestPromise.promise = new Promise(
async (resolve, reject) => { async (resolve, reject) => {
const waitForResult = true
const expectWebout = true
this.sasViyaApiClient this.sasViyaApiClient
?.executeComputeJob( ?.executeComputeJob(
sasJob, sasJob,
config.contextName, config.contextName,
config.debug,
data, data,
accessToken accessToken,
waitForResult,
expectWebout
) )
.then((response) => { .then((response) => {
if (!config.debug) { if (!config.debug) {
@@ -701,11 +807,23 @@ export default class SASjs {
} else { } else {
this.retryCountComputeApi = 0 this.retryCountComputeApi = 0
reject( reject(
new ErrorResponse('Compute API retry requests limit reached') new ErrorResponse('Compute API retry requests limit reached.')
) )
} }
} }
if (response?.log) {
this.appendSasjsRequest(response.log, sasJob, null)
}
if (error.toString().includes('Job was not found')) {
reject(
new ErrorResponse('Service not found on the server.', {
sasJob: sasJob
})
)
}
if (error && error.status === 401) { if (error && error.status === 401) {
if (loginRequiredCallback) loginRequiredCallback(true) if (loginRequiredCallback) loginRequiredCallback(true)
sasjsWaitingRequest.requestPromise.resolve = resolve sasjsWaitingRequest.requestPromise.resolve = resolve
@@ -713,10 +831,8 @@ export default class SASjs {
sasjsWaitingRequest.config = config sasjsWaitingRequest.config = config
this.sasjsWaitingRequests.push(sasjsWaitingRequest) this.sasjsWaitingRequests.push(sasjsWaitingRequest)
} else { } else {
reject(new ErrorResponse('Job execution failed', error)) reject(new ErrorResponse('Job execution failed.', error))
} }
this.appendSasjsRequest(response.log, sasJob, null)
}) })
} }
) )
@@ -796,12 +912,24 @@ export default class SASjs {
} else { } else {
this.retryCountJeseApi = 0 this.retryCountJeseApi = 0
reject( reject(
new ErrorResponse('Jes API retry requests limit reached') new ErrorResponse('Jes API retry requests limit reached.')
) )
} }
} }
reject(new ErrorResponse('Job execution failed', e)) if (e?.log) {
this.appendSasjsRequest(e.log, sasJob, null)
}
if (e.toString().includes('Job was not found')) {
reject(
new ErrorResponse('Service not found on the server.', {
sasJob: sasJob
})
)
}
reject(new ErrorResponse('Job execution failed.', e))
}) })
) )
} }
@@ -985,7 +1113,7 @@ export default class SASjs {
} else { } else {
reject( reject(
new ErrorResponse( new ErrorResponse(
'Job WEB execution failed', 'Job WEB execution failed.',
this.parseSAS9ErrorResponse(responseText) this.parseSAS9ErrorResponse(responseText)
) )
) )
@@ -1003,7 +1131,7 @@ export default class SASjs {
} catch (e) { } catch (e) {
reject( reject(
new ErrorResponse( new ErrorResponse(
'Job WEB debug response parsing failed', 'Job WEB debug response parsing failed.',
{ response: resText, exception: e } { response: resText, exception: e }
) )
) )
@@ -1012,7 +1140,7 @@ export default class SASjs {
(err: any) => { (err: any) => {
reject( reject(
new ErrorResponse( new ErrorResponse(
'Job WEB debug response parsing failed', 'Job WEB debug response parsing failed.',
err err
) )
) )
@@ -1021,19 +1149,34 @@ export default class SASjs {
} catch (e) { } catch (e) {
reject( reject(
new ErrorResponse( new ErrorResponse(
'Job WEB debug response parsing failed', 'Job WEB debug response parsing failed.',
{ response: responseText, exception: e } { response: responseText, exception: e }
) )
) )
} }
} else { } else {
this.updateUsername(responseText) this.updateUsername(responseText)
if (
responseText.includes(
'The requested URL /SASStoredProcess/do/ was not found on this server.'
) ||
responseText.includes('Stored process not found')
) {
reject(
new ErrorResponse(
'Service not found on the server.',
{ service: sasJob },
responseText
)
)
}
try { try {
const parsedJson = JSON.parse(responseText) const parsedJson = JSON.parse(responseText)
resolve(parsedJson) resolve(parsedJson)
} catch (e) { } catch (e) {
reject( reject(
new ErrorResponse('Job WEB response parsing failed', { new ErrorResponse('Job WEB response parsing failed.', {
response: responseText, response: responseText,
exception: e exception: e
}) })
@@ -1044,7 +1187,7 @@ export default class SASjs {
} }
}) })
.catch((e: Error) => { .catch((e: Error) => {
reject(new ErrorResponse('Job WEB request failed', e)) reject(new ErrorResponse('Job WEB request failed.', e))
}) })
} }
) )
@@ -1185,10 +1328,20 @@ export default class SASjs {
} }
} }
private fetchLogFileContent(logLink: string) { /**
* Fetches content of the log file
* @param logLink - url of the log file.
* @param accessToken - an access token for an authorized user.
*/
public fetchLogFileContent(logLink: string, accessToken?: string) {
const headers: any = { 'Content-Type': 'application/json' }
if (accessToken) headers.Authorization = 'Bearer ' + accessToken
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fetch(logLink, { fetch(logLink, {
method: 'GET' method: 'GET',
headers
}) })
.then((response: any) => response.text()) .then((response: any) => response.text())
.then((response: any) => resolve(response)) .then((response: any) => resolve(response))
@@ -1286,11 +1439,15 @@ export default class SASjs {
this.sasjsConfig.serverUrl === undefined || this.sasjsConfig.serverUrl === undefined ||
this.sasjsConfig.serverUrl === '' this.sasjsConfig.serverUrl === ''
) { ) {
let url = `${location.protocol}//${location.hostname}` if (typeof location !== 'undefined') {
if (location.port) { let url = `${location.protocol}//${location.hostname}`
url = `${url}:${location.port}`
if (location.port) url = `${url}:${location.port}`
this.sasjsConfig.serverUrl = url
} else {
this.sasjsConfig.serverUrl = ''
} }
this.sasjsConfig.serverUrl = url
} }
if (this.sasjsConfig.serverUrl.slice(-1) === '/') { if (this.sasjsConfig.serverUrl.slice(-1) === '/') {
@@ -1320,6 +1477,8 @@ export default class SASjs {
this.sasjsConfig.contextName, this.sasjsConfig.contextName,
this.setCsrfTokenApi this.setCsrfTokenApi
) )
this.sasViyaApiClient.debug = this.sasjsConfig.debug
} }
if (this.sasjsConfig.serverType === ServerType.SAS9) { if (this.sasjsConfig.serverType === ServerType.SAS9) {
if (this.sas9ApiClient) if (this.sas9ApiClient)
@@ -1353,26 +1512,6 @@ export default class SASjs {
} }
} }
private async getLoginForm() {
const pattern: RegExp = /<form.+action="(.*Logon[^"]*).*>/
const response = await fetch(this.loginUrl).then((r) => r.text())
const matches = pattern.exec(response)
const formInputs: any = {}
if (matches && matches.length) {
this.setLoginUrl(matches)
const inputs = response.match(/<input.*"hidden"[^>]*>/g)
if (inputs) {
inputs.forEach((inputStr: string) => {
const valueMatch = inputStr.match(/name="([^"]*)"\svalue="([^"]*)/)
if (valueMatch && valueMatch.length) {
formInputs[valueMatch[1]] = valueMatch[2]
}
})
}
}
return Object.keys(formInputs).length ? formInputs : null
}
private async createFoldersAndServices( private async createFoldersAndServices(
parentFolder: string, parentFolder: string,
membersJson: any[], membersJson: any[],

View File

@@ -2,6 +2,12 @@ import { Session, Context, CsrfToken } from './types'
import { asyncForEach, makeRequest, isUrl } from './utils' import { asyncForEach, makeRequest, isUrl } from './utils'
const MAX_SESSION_COUNT = 1 const MAX_SESSION_COUNT = 1
const RETRY_LIMIT: number = 3
let RETRY_COUNT: number = 0
const INTERNAL_SAS_ERROR = {
status: 304,
message: 'Not Modified'
}
export class SessionManager { export class SessionManager {
constructor( constructor(
@@ -15,22 +21,34 @@ export class SessionManager {
private sessions: Session[] = [] private sessions: Session[] = []
private currentContext: Context | null = null private currentContext: Context | null = null
private csrfToken: CsrfToken | null = null private csrfToken: CsrfToken | null = null
private _debug: boolean = false
public get debug() {
return this._debug
}
public set debug(value: boolean) {
this._debug = value
}
async getSession(accessToken?: string) { async getSession(accessToken?: string) {
await this.createSessions(accessToken) await this.createSessions(accessToken)
this.createAndWaitForSession(accessToken) await this.createAndWaitForSession(accessToken)
const session = this.sessions.pop() const session = this.sessions.pop()
const secondsSinceSessionCreation = const secondsSinceSessionCreation =
(new Date().getTime() - new Date(session!.creationTimeStamp).getTime()) / (new Date().getTime() - new Date(session!.creationTimeStamp).getTime()) /
1000 1000
if ( if (
!session!.attributes || !session!.attributes ||
secondsSinceSessionCreation >= session!.attributes.sessionInactiveTimeout secondsSinceSessionCreation >= session!.attributes.sessionInactiveTimeout
) { ) {
await this.createSessions(accessToken) await this.createSessions(accessToken)
const freshSession = this.sessions.pop() const freshSession = this.sessions.pop()
return freshSession return freshSession
} }
return session return session
} }
@@ -39,22 +57,37 @@ export class SessionManager {
method: 'DELETE', method: 'DELETE',
headers: this.getHeaders(accessToken) headers: this.getHeaders(accessToken)
} }
return await this.request<Session>( return await this.request<Session>(
`${this.serverUrl}/compute/sessions/${id}`, `${this.serverUrl}/compute/sessions/${id}`,
deleteSessionRequest deleteSessionRequest
).then(() => { )
this.sessions = this.sessions.filter((s) => s.id !== id) .then(() => {
}) this.sessions = this.sessions.filter((s) => s.id !== id)
})
.catch((err) => {
throw err
})
} }
private async createSessions(accessToken?: string) { private async createSessions(accessToken?: string) {
if (!this.sessions.length) { if (!this.sessions.length) {
if (!this.currentContext) { if (!this.currentContext) {
await this.setCurrentContext(accessToken) await this.setCurrentContext(accessToken).catch((err) => {
throw err
})
} }
await asyncForEach(new Array(MAX_SESSION_COUNT), async () => { await asyncForEach(new Array(MAX_SESSION_COUNT), async () => {
const createdSession = await this.createAndWaitForSession(accessToken) const createdSession = await this.createAndWaitForSession(
accessToken
).catch((err) => {
throw err
})
this.sessions.push(createdSession) this.sessions.push(createdSession)
}).catch((err) => {
throw err
}) })
} }
} }
@@ -64,13 +97,18 @@ export class SessionManager {
method: 'POST', method: 'POST',
headers: this.getHeaders(accessToken) headers: this.getHeaders(accessToken)
} }
const { result: createdSession, etag } = await this.request<Session>( const { result: createdSession, etag } = await this.request<Session>(
`${this.serverUrl}/compute/contexts/${this.currentContext!.id}/sessions`, `${this.serverUrl}/compute/contexts/${this.currentContext!.id}/sessions`,
createSessionRequest createSessionRequest
) ).catch((err) => {
throw err
})
await this.waitForSession(createdSession, etag, accessToken) await this.waitForSession(createdSession, etag, accessToken)
this.sessions.push(createdSession) this.sessions.push(createdSession)
return createdSession return createdSession
} }
@@ -80,6 +118,8 @@ export class SessionManager {
items: Context[] items: Context[]
}>(`${this.serverUrl}/compute/contexts?limit=10000`, { }>(`${this.serverUrl}/compute/contexts?limit=10000`, {
headers: this.getHeaders(accessToken) headers: this.getHeaders(accessToken)
}).catch((err) => {
throw err
}) })
const contextsList = const contextsList =
@@ -98,6 +138,8 @@ export class SessionManager {
} }
this.currentContext = currentContext this.currentContext = currentContext
Promise.resolve()
} }
} }
@@ -105,6 +147,7 @@ export class SessionManager {
const headers: any = { const headers: any = {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} }
if (accessToken) { if (accessToken) {
headers.Authorization = `Bearer ${accessToken}` headers.Authorization = `Bearer ${accessToken}`
} }
@@ -115,8 +158,7 @@ export class SessionManager {
private async waitForSession( private async waitForSession(
session: Session, session: Session,
etag: string | null, etag: string | null,
accessToken?: string, accessToken?: string
silent = false
) { ) {
let sessionState = session.state let sessionState = session.state
const headers: any = { const headers: any = {
@@ -124,24 +166,41 @@ export class SessionManager {
'If-None-Match': etag 'If-None-Match': etag
} }
const stateLink = session.links.find((l: any) => l.rel === 'state') const stateLink = session.links.find((l: any) => l.rel === 'state')
return new Promise(async (resolve, _) => { return new Promise(async (resolve, _) => {
if (sessionState === 'pending') { if (sessionState === 'pending') {
if (stateLink) { if (stateLink) {
if (!silent) { if (this.debug) {
console.log('Polling session status... \n') // ? console.log('Polling session status... \n') // ?
} }
const { result: state } = await this.request<string>(
const { result: state } = await this.requestSessionStatus<string>(
`${this.serverUrl}${stateLink.href}?wait=30`, `${this.serverUrl}${stateLink.href}?wait=30`,
{ {
headers headers
}, },
'text' 'text'
) ).catch((err) => {
throw err
})
sessionState = state.trim() sessionState = state.trim()
if (!silent) {
if (this.debug) {
console.log(`Current state is '${sessionState}'\n`) console.log(`Current state is '${sessionState}'\n`)
} }
// There is an internal error present in SAS Viya 3.5
// Retry to wait for a session status in such case of SAS internal error
if (
sessionState === INTERNAL_SAS_ERROR.message &&
RETRY_COUNT < RETRY_LIMIT
) {
RETRY_COUNT++
resolve(this.waitForSession(session, etag, accessToken))
}
resolve(sessionState) resolve(sessionState)
} }
} else { } else {
@@ -161,6 +220,7 @@ export class SessionManager {
[this.csrfToken.headerName]: this.csrfToken.value [this.csrfToken.headerName]: this.csrfToken.value
} }
} }
return await makeRequest<T>( return await makeRequest<T>(
url, url,
options, options,
@@ -169,6 +229,36 @@ export class SessionManager {
this.setCsrfToken(token) this.setCsrfToken(token)
}, },
contentType contentType
) ).catch((err) => {
throw err
})
}
private async requestSessionStatus<T>(
url: string,
options: RequestInit,
contentType: 'text' | 'json' = 'json'
) {
if (this.csrfToken) {
options.headers = {
...options.headers,
[this.csrfToken.headerName]: this.csrfToken.value
}
}
return await makeRequest<T>(
url,
options,
(token) => {
this.csrfToken = token
this.setCsrfToken(token)
},
contentType
).catch((err) => {
if (err.status === INTERNAL_SAS_ERROR.status)
return { result: INTERNAL_SAS_ERROR.message }
throw err
})
} }
} }

View File

@@ -0,0 +1,137 @@
import { FileUploader } from '../FileUploader'
import { UploadFile } from '../types'
const sampleResponse = `{
"SYSUSERID": "cas",
"_DEBUG":" ",
"SYS_JES_JOB_URI": "/jobExecution/jobs/000-000-000-000",
"_PROGRAM" : "/Public/app/editors/loadfile",
"SYSCC" : "0",
"SYSJOBID" : "117382",
"SYSWARNINGTEXT" : ""
}`
const prepareFilesAndParams = () => {
const files: UploadFile[] = [
{
file: new File([''], 'testfile'),
fileName: 'testfile'
}
]
const params = { table: 'libtable' }
return { files, params }
}
describe('FileUploader', () => {
let originalFetch: any
const fileUploader = new FileUploader(
'/sample/apploc',
'https://sample.server.com',
'/jobs/path',
null,
null
)
beforeAll(() => {
originalFetch = (global as any).fetch
})
beforeEach(() => {
;(global as any).fetch = jest.fn().mockImplementation(() =>
Promise.resolve({
text: () => Promise.resolve(sampleResponse)
})
)
})
afterAll(() => {
;(global as any).fetch = originalFetch
})
it('should upload successfully', async (done) => {
const sasJob = 'test/upload'
const { files, params } = prepareFilesAndParams()
fileUploader.uploadFile(sasJob, files, params).then((res: any) => {
expect(JSON.stringify(res)).toEqual(
JSON.stringify(JSON.parse(sampleResponse))
)
done()
})
})
it('should an error when no files are provided', async (done) => {
const sasJob = 'test/upload'
const files: UploadFile[] = []
const params = { table: 'libtable' }
fileUploader.uploadFile(sasJob, files, params).catch((err: any) => {
expect(err.error.message).toEqual('At least one file must be provided.')
done()
})
})
it('should throw an error when no sasJob is provided', async (done) => {
const sasJob = ''
const { files, params } = prepareFilesAndParams()
fileUploader.uploadFile(sasJob, files, params).catch((err: any) => {
expect(err.error.message).toEqual('sasJob must be provided.')
done()
})
})
it('should throw an error when login is required', async (done) => {
;(global as any).fetch = jest.fn().mockImplementation(() =>
Promise.resolve({
text: () => Promise.resolve('<form action="Logon">')
})
)
const sasJob = 'test'
const { files, params } = prepareFilesAndParams()
fileUploader.uploadFile(sasJob, files, params).catch((err: any) => {
expect(err.error.message).toEqual(
'You must be logged in to upload a file.'
)
done()
})
})
it('should throw an error when invalid JSON is returned by the server', async (done) => {
;(global as any).fetch = jest.fn().mockImplementation(() =>
Promise.resolve({
text: () => Promise.resolve('{invalid: "json"')
})
)
const sasJob = 'test'
const { files, params } = prepareFilesAndParams()
fileUploader.uploadFile(sasJob, files, params).catch((err: any) => {
expect(err.error.message).toEqual(
'Error while parsing json from upload response.'
)
done()
})
})
it('should throw an error when the server request fails', async (done) => {
;(global as any).fetch = jest.fn().mockImplementation(() =>
Promise.resolve({
text: () => Promise.reject('{message: "Server error"}')
})
)
const sasJob = 'test'
const { files, params } = prepareFilesAndParams()
fileUploader.uploadFile(sasJob, files, params).catch((err: any) => {
expect(err.error.message).toEqual('Upload request failed.')
done()
})
})
})

View File

@@ -0,0 +1,38 @@
import dotenv from 'dotenv'
import { SessionManager } from '../SessionManager'
import { CsrfToken } from '../types'
describe('SessionManager', () => {
const setCsrfToken = jest
.fn()
.mockImplementation((csrfToken: CsrfToken) => console.log(csrfToken))
beforeAll(() => {
dotenv.config()
})
it('should instantiate', () => {
const sessionManager = new SessionManager(
'http://test-server.com',
'test context',
setCsrfToken
)
expect(sessionManager).toBeInstanceOf(SessionManager)
expect(sessionManager.debug).toBeFalsy()
expect((sessionManager as any).serverUrl).toEqual('http://test-server.com')
expect((sessionManager as any).contextName).toEqual('test context')
})
it('should set the debug flag', () => {
const sessionManager = new SessionManager(
'http://test-server.com',
'test context',
setCsrfToken
)
sessionManager.debug = true
expect(sessionManager.debug).toBeTruthy()
})
})

View File

@@ -1,4 +1,4 @@
import { parseGeneratedCode } from './index' import { parseGeneratedCode } from '../../utils/index'
it('should parse generated code', async (done) => { it('should parse generated code', async (done) => {
expect(sampleResponse).toBeTruthy() expect(sampleResponse).toBeTruthy()

View File

@@ -1,4 +1,4 @@
import { parseSourceCode } from './index' import { parseSourceCode } from '../../utils/index'
it('should parse SAS9 source code', async (done) => { it('should parse SAS9 source code', async (done) => {
expect(sampleResponse).toBeTruthy() expect(sampleResponse).toBeTruthy()

View File

@@ -1,17 +1,19 @@
export class ErrorResponse { export class ErrorResponse {
body: ErrorBody error: ErrorBody
constructor(message: string, details?: any) { constructor(message: string, details?: any, raw?: any) {
let detailsString = '' let detailsString = details
let raw
try { if (typeof details !== 'object') {
detailsString = JSON.stringify(details) try {
} catch { detailsString = JSON.parse(details)
raw = details } catch {
raw = details
detailsString = ''
}
} }
this.body = { this.error = {
message, message,
details: detailsString, details: detailsString,
raw raw

View File

@@ -2,7 +2,7 @@ import { CsrfToken } from '../types'
import { needsRetry } from './needsRetry' import { needsRetry } from './needsRetry'
let retryCount: number = 0 let retryCount: number = 0
let retryLimit: number = 5 const retryLimit: number = 5
export async function makeRequest<T>( export async function makeRequest<T>(
url: string, url: string,
@@ -18,57 +18,118 @@ export async function makeRequest<T>(
: (res: Response) => res.text() : (res: Response) => res.text()
let etag = null let etag = null
const result = await fetch(url, request).then(async (response) => { const result = await fetch(url, request)
if (response.redirected && response.url.includes('SASLogon/login')) { .then(async (response) => {
return Promise.reject({ status: 401 }) if (response.redirected && response.url.includes('SASLogon/login')) {
} return Promise.reject({ status: 401 })
if (!response.ok) { }
if (response.status === 403) {
const tokenHeader = response.headers.get('X-CSRF-HEADER')
if (tokenHeader) { if (!response.ok) {
const token = response.headers.get(tokenHeader) if (response.status === 403) {
callback({ const tokenHeader = response.headers.get('X-CSRF-HEADER')
headerName: tokenHeader,
value: token || '' if (tokenHeader) {
const token = response.headers.get(tokenHeader)
callback({
headerName: tokenHeader,
value: token || ''
})
retryRequest = {
...request,
headers: { ...request.headers, [tokenHeader]: token }
}
return await fetch(url, retryRequest).then((res) => {
etag = res.headers.get('ETag')
return responseTransform(res)
})
} else {
let body: any = await response.text().catch((err) => {
throw err
})
try {
body = JSON.parse(body)
body.message = `Forbidden. Check your permissions and user groups, and also the scopes granted when registering your CLIENT_ID. ${
body.message || ''
}`
body = JSON.stringify(body)
} catch (_) {}
return Promise.reject({ status: response.status, body })
}
} else {
let body: any = await response.text().catch((err) => {
throw err
}) })
retryRequest = { if (needsRetry(body)) {
...request, if (retryCount < retryLimit) {
headers: { ...request.headers, [tokenHeader]: token } retryCount++
let retryResponse = await makeRequest(
url,
retryRequest || request,
callback,
contentType
).catch((err) => {
throw err
})
retryCount = 0
etag = retryResponse.etag
return retryResponse.result
} else {
retryCount = 0
throw new Error('Request retry limit exceeded')
}
} }
return fetch(url, retryRequest).then((res) => { if (response.status === 401) {
etag = res.headers.get('ETag') try {
return responseTransform(res) body = JSON.parse(body)
})
} else {
let body: any = await response.text()
try { body.message = `Unauthorized request. Check your credentials(client, secret, access token). ${
body = JSON.parse(body) body.message || ''
}`
body.message = `Forbidden. Check your permissions and user groups. ${ body = JSON.stringify(body)
body.message || '' } catch (_) {}
}` }
body = JSON.stringify(body)
} catch (_) {}
return Promise.reject({ status: response.status, body }) return Promise.reject({ status: response.status, body })
} }
} else { } else {
let body: any = await response.text() if (response.status === 204) {
return Promise.resolve()
}
const responseTransformed = await responseTransform(response).catch(
(err) => {
throw err
}
)
let responseText = ''
if (needsRetry(body)) { if (typeof responseTransformed === 'string') {
responseText = responseTransformed
} else {
responseText = JSON.stringify(responseTransformed)
}
if (needsRetry(responseText)) {
if (retryCount < retryLimit) { if (retryCount < retryLimit) {
retryCount++ retryCount++
let retryResponse = await makeRequest( const retryResponse = await makeRequest(
url, url,
retryRequest || request, retryRequest || request,
callback, callback,
contentType contentType
) ).catch((err) => {
throw err
})
retryCount = 0 retryCount = 0
etag = retryResponse.etag etag = retryResponse.etag
@@ -80,57 +141,14 @@ export async function makeRequest<T>(
} }
} }
if (response.status === 401) { etag = response.headers.get('ETag')
try {
body = JSON.parse(body)
body.message = `Unauthorized request. Check your credentials(client, secret, access token). ${ return responseTransformed
body.message || ''
}`
body = JSON.stringify(body)
} catch (_) {}
}
return Promise.reject({ status: response.status, body })
} }
} else { })
if (response.status === 204) { .catch((err) => {
return Promise.resolve() throw err
} })
const responseTransformed = await responseTransform(response)
let responseText = ''
if (typeof responseTransformed === 'string') {
responseText = responseTransformed
} else {
responseText = JSON.stringify(responseTransformed)
}
if (needsRetry(responseText)) {
if (retryCount < retryLimit) {
retryCount++
const retryResponse = await makeRequest(
url,
retryRequest || request,
callback,
contentType
)
retryCount = 0
etag = retryResponse.etag
return retryResponse.result
} else {
retryCount = 0
throw new Error('Request retry limit exceeded')
}
}
etag = response.headers.get('ETag')
return responseTransformed
}
})
return { result, etag } return { result, etag }
} }