mirror of
https://github.com/sasjs/adapter.git
synced 2026-01-05 03:30:05 +00:00
Compare commits
77 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca18fcecf0 | ||
|
|
009069169f | ||
|
|
6d166efd11 | ||
|
|
1b117a67aa | ||
|
|
505d85c256 | ||
|
|
71a3fe04a0 | ||
|
|
79bb27524c | ||
|
|
9651b7adb4 | ||
|
|
59e5bec731 | ||
|
|
182e66216f | ||
|
|
2408fd091e | ||
|
|
0e38a24664 | ||
|
|
aa643d1782 | ||
|
|
cdc91e9cda | ||
|
|
3f0590e0fe | ||
|
|
5efb294ff2 | ||
|
|
011e2d83dc | ||
|
|
e36b511530 | ||
|
|
b6a2a85d1d | ||
|
|
f1cceeb5e6 | ||
|
|
6fee2548fd | ||
|
|
91005066cf | ||
|
|
e1f17ef47d | ||
|
|
8a40071c35 | ||
|
|
430957eb3d | ||
|
|
25874be679 | ||
|
|
ed8440434f | ||
|
|
0f9884c1b6 | ||
|
|
d126a05347 | ||
|
|
3e26bbbbba | ||
|
|
982cc8f7a0 | ||
|
|
d1770698e0 | ||
|
|
b78e8617c4 | ||
|
|
3ce9ca0986 | ||
|
|
04d17c3680 | ||
|
|
d26e15f91c | ||
|
|
83c46091b3 | ||
|
|
d640d7c040 | ||
|
|
c934eb2b08 | ||
|
|
24dd5e32ad | ||
|
|
a23103b2c3 | ||
|
|
35aa4235e4 | ||
|
|
e9be1cf99a | ||
|
|
c7b0821081 | ||
|
|
4a4618dd32 | ||
|
|
d223e83c60 | ||
|
|
d1f1a20126 | ||
|
|
4b89e3762f | ||
|
|
bc110288de | ||
|
|
e94e16b52c | ||
|
|
76aacee016 | ||
|
|
1a3bd5d1f5 | ||
|
|
3f6e89d716 | ||
|
|
361ec84638 | ||
|
|
35cc1e4f62 | ||
|
|
64a976e888 | ||
|
|
7e2cb8491f | ||
|
|
2cdab7522d | ||
|
|
a07eabc408 | ||
|
|
7279c23fe2 | ||
|
|
80707d77d9 | ||
|
|
d5920c5885 | ||
|
|
6a3a6b4485 | ||
|
|
2b1df0c61a | ||
|
|
216725f306 | ||
|
|
3183f89a62 | ||
|
|
f5cc16c3bd | ||
|
|
e78dc76e56 | ||
|
|
bfdb5ef0a6 | ||
|
|
35353d3fce | ||
|
|
7a02c8ad34 | ||
|
|
331d9b0010 | ||
|
|
ef5686cce7 | ||
|
|
3f796b300d | ||
|
|
a07c16fb52 | ||
|
|
fd6905ea9f | ||
|
|
08f58b5f4f |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,2 +1,2 @@
|
|||||||
node_modules
|
node_modules
|
||||||
build
|
build
|
||||||
@@ -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-cli` unit tests are passing (`npm test`).
|
||||||
- [ ] All `sasjs-tests` are passing (instructions available [here](https://github.com/sasjs/adapter/blob/master/sasjs-tests/README.md)).
|
- [ ] 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
231
docs/classes/reflection-787.reflection-214.fileuploader.html
Normal file
231
docs/classes/reflection-787.reflection-214.fileuploader.html
Normal file
File diff suppressed because one or more lines are too long
312
docs/classes/reflection-787.reflection-214.sas9apiclient.html
Normal file
312
docs/classes/reflection-787.reflection-214.sas9apiclient.html
Normal file
File diff suppressed because one or more lines are too long
1590
docs/classes/reflection-787.reflection-214.sasjs.html
Normal file
1590
docs/classes/reflection-787.reflection-214.sasjs.html
Normal file
File diff suppressed because one or more lines are too long
1435
docs/classes/reflection-787.reflection-214.sasviyaapiclient.html
Normal file
1435
docs/classes/reflection-787.reflection-214.sasviyaapiclient.html
Normal file
File diff suppressed because one or more lines are too long
323
docs/classes/reflection-787.reflection-214.sessionmanager.html
Normal file
323
docs/classes/reflection-787.reflection-214.sessionmanager.html
Normal file
File diff suppressed because one or more lines are too long
231
docs/classes/reflection-790.reflection-214.fileuploader.html
Normal file
231
docs/classes/reflection-790.reflection-214.fileuploader.html
Normal file
File diff suppressed because one or more lines are too long
312
docs/classes/reflection-790.reflection-214.sas9apiclient.html
Normal file
312
docs/classes/reflection-790.reflection-214.sas9apiclient.html
Normal file
File diff suppressed because one or more lines are too long
1641
docs/classes/reflection-790.reflection-214.sasjs.html
Normal file
1641
docs/classes/reflection-790.reflection-214.sasjs.html
Normal file
File diff suppressed because one or more lines are too long
1444
docs/classes/reflection-790.reflection-214.sasviyaapiclient.html
Normal file
1444
docs/classes/reflection-790.reflection-214.sasviyaapiclient.html
Normal file
File diff suppressed because one or more lines are too long
323
docs/classes/reflection-790.reflection-214.sessionmanager.html
Normal file
323
docs/classes/reflection-790.reflection-214.sessionmanager.html
Normal file
File diff suppressed because one or more lines are too long
@@ -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>
|
||||||
|
|||||||
106
docs/modules/reflection-787.html
Normal file
106
docs/modules/reflection-787.html
Normal file
File diff suppressed because one or more lines are too long
128
docs/modules/reflection-787.reflection-214.html
Normal file
128
docs/modules/reflection-787.reflection-214.html
Normal file
File diff suppressed because one or more lines are too long
106
docs/modules/reflection-790.html
Normal file
106
docs/modules/reflection-790.html
Normal file
File diff suppressed because one or more lines are too long
128
docs/modules/reflection-790.reflection-214.html
Normal file
128
docs/modules/reflection-790.reflection-214.html
Normal file
File diff suppressed because one or more lines are too long
2660
package-lock.json
generated
2660
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
@@ -2,7 +2,7 @@
|
|||||||
"name": "@sasjs/adapter",
|
"name": "@sasjs/adapter",
|
||||||
"description": "JavaScript adapter for SAS",
|
"description": "JavaScript adapter for SAS",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "rimraf build && webpack",
|
"build": "rimraf build && rimraf node && mkdir node && cp -r src/* node && webpack && rimraf build/src && rimraf node",
|
||||||
"package:lib": "npm run build && cp ./package.json build && cd build && npm version \"5.0.0\" && npm pack",
|
"package:lib": "npm run build && cp ./package.json build && cd build && npm version \"5.0.0\" && npm pack",
|
||||||
"publish:lib": "npm run build && cd build && npm publish",
|
"publish:lib": "npm run build && cd build && npm publish",
|
||||||
"lint:fix": "npx prettier --write 'src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}'",
|
"lint:fix": "npx prettier --write 'src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}'",
|
||||||
@@ -37,15 +37,15 @@
|
|||||||
"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",
|
||||||
"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.3.0",
|
||||||
"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 +53,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": {
|
||||||
|
|||||||
6
sasjs-tests/package-lock.json
generated
6
sasjs-tests/package-lock.json
generated
@@ -1357,9 +1357,9 @@
|
|||||||
"integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw=="
|
"integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw=="
|
||||||
},
|
},
|
||||||
"@sasjs/adapter": {
|
"@sasjs/adapter": {
|
||||||
"version": "1.12.0",
|
"version": "1.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/@sasjs/adapter/-/adapter-1.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sasjs/adapter/-/adapter-1.18.3.tgz",
|
||||||
"integrity": "sha512-0uGQH9ynomWzdBaEujEtcR38q6V7LCgG0mrb1Wellv6cC/IHD3j6WfeZZAgtiMPeOSJjbCDBOlVnzC2TlBqJFw==",
|
"integrity": "sha512-wzDFJRyt2dXFeQP+JzqRGunYUbujrAbU/Jc4IWg5URsCkGAzwsNl/4G0xJVbqOTy1MvoZ431rfCnvhkUlg7D3Q==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"es6-promise": "^4.2.8",
|
"es6-promise": "^4.2.8",
|
||||||
"form-data": "^3.0.0",
|
"form-data": "^3.0.0",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"homepage": ".",
|
"homepage": ".",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sasjs/adapter": "^1.12.0",
|
"@sasjs/adapter": "^1.18.2",
|
||||||
"@sasjs/test-framework": "^1.4.0",
|
"@sasjs/test-framework": "^1.4.0",
|
||||||
"@testing-library/jest-dom": "^4.2.4",
|
"@testing-library/jest-dom": "^4.2.4",
|
||||||
"@testing-library/react": "^9.5.0",
|
"@testing-library/react": "^9.5.0",
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { render } from "@testing-library/react";
|
|
||||||
import App from "./App";
|
|
||||||
|
|
||||||
test("renders learn react link", () => {
|
|
||||||
const { getByText } = render(<App />);
|
|
||||||
const linkElement = getByText(/learn react/i);
|
|
||||||
expect(linkElement).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
@@ -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]);
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ import { TestSuite } from "@sasjs/test-framework";
|
|||||||
|
|
||||||
const defaultConfig: SASjsConfig = {
|
const defaultConfig: SASjsConfig = {
|
||||||
serverUrl: window.location.origin,
|
serverUrl: window.location.origin,
|
||||||
pathSAS9: "/SASStoredProcess/do",
|
pathSAS9: '/SASStoredProcess/do',
|
||||||
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
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -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:
|
||||||
@@ -46,6 +57,7 @@ export const basicTests = (
|
|||||||
},
|
},
|
||||||
assertion: (sasjsInstance: SASjs) => {
|
assertion: (sasjsInstance: SASjs) => {
|
||||||
const sasjsConfig = sasjsInstance.getSasjsConfig();
|
const sasjsConfig = sasjsInstance.getSasjsConfig();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
sasjsConfig.serverUrl === defaultConfig.serverUrl &&
|
sasjsConfig.serverUrl === defaultConfig.serverUrl &&
|
||||||
sasjsConfig.pathSAS9 === defaultConfig.pathSAS9 &&
|
sasjsConfig.pathSAS9 === defaultConfig.pathSAS9 &&
|
||||||
|
|||||||
41
sasjs-tests/src/testSuites/Compute.ts
Normal file
41
sasjs-tests/src/testSuites/Compute.ts
Normal 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
|
||||||
|
}
|
||||||
@@ -88,7 +88,7 @@ export const sendArrTests = (adapter: SASjs): TestSuite => ({
|
|||||||
return adapter.request("common/sendArr", data).catch((e) => e);
|
return adapter.request("common/sendArr", data).catch((e) => e);
|
||||||
},
|
},
|
||||||
assertion: (error: any) => {
|
assertion: (error: any) => {
|
||||||
return !!error && !!error.body && !!error.body.message;
|
return !!error && !!error.error && !!error.error.message;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -185,7 +185,7 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
|||||||
};
|
};
|
||||||
return adapter.request("common/sendObj", invalidData).catch((e) => e);
|
return adapter.request("common/sendObj", invalidData).catch((e) => e);
|
||||||
},
|
},
|
||||||
assertion: (error: any) => !!error && !!error.body && !!error.body.message
|
assertion: (error: any) => !!error && !!error.error && !!error.error.message
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Single string value",
|
title: "Single string value",
|
||||||
@@ -219,7 +219,7 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
|||||||
.catch((e) => e);
|
.catch((e) => e);
|
||||||
},
|
},
|
||||||
assertion: (error: any) => {
|
assertion: (error: any) => {
|
||||||
return !!error && !!error.body && !!error.body.message;
|
return !!error && !!error.error && !!error.error.message;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -23,22 +23,24 @@ export const sasjsRequestTests = (adapter: SASjs): TestSuite => ({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Make error and capture log",
|
title: "Make error and capture log",
|
||||||
description: "Should make an error and capture log",
|
description: "Should make an error and capture log, in the same time it is testing if debug override is working",
|
||||||
test: async () => {
|
test: async () => {
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
adapter
|
adapter
|
||||||
.request("common/makeErr", data)
|
.request("common/makeErr", data, {debug: true})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
//no action here, this request must throw error
|
//no action here, this request must throw error
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
let sasRequests = adapter.getSasRequests();
|
let sasRequests = adapter.getSasRequests();
|
||||||
let makeErrRequest =
|
let makeErrRequest: any =
|
||||||
sasRequests.find((req) =>
|
sasRequests.find((req) =>
|
||||||
req.serviceLink.includes("makeErr")
|
req.serviceLink.includes("makeErr")
|
||||||
) || null;
|
) || null;
|
||||||
|
|
||||||
resolve(!!makeErrRequest);
|
if (!makeErrRequest) resolve(false)
|
||||||
|
|
||||||
|
resolve(!!(makeErrRequest.logFile && makeErrRequest.logFile.length > 0));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ import {
|
|||||||
CsrfToken,
|
CsrfToken,
|
||||||
EditContextInput,
|
EditContextInput,
|
||||||
ErrorResponse,
|
ErrorResponse,
|
||||||
JobDefinition
|
JobDefinition,
|
||||||
|
PollOptions
|
||||||
} from './types'
|
} from './types'
|
||||||
import { formatDataForRequest } from './utils/formatDataForRequest'
|
import { formatDataForRequest } from './utils/formatDataForRequest'
|
||||||
import { SessionManager } from './SessionManager'
|
import { SessionManager } from './SessionManager'
|
||||||
@@ -52,7 +53,9 @@ export class SASViyaApiClient {
|
|||||||
|
|
||||||
public set debug(value: boolean) {
|
public set debug(value: boolean) {
|
||||||
this._debug = value
|
this._debug = value
|
||||||
this.sessionManager.debug = value
|
if (this.sessionManager) {
|
||||||
|
this.sessionManager.debug = value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -145,43 +148,45 @@ 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,
|
||||||
null,
|
accessToken,
|
||||||
true
|
null,
|
||||||
).catch((err) => err)
|
false,
|
||||||
|
true,
|
||||||
|
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 && result.body && result.body.details) {
|
if (result && result.log) {
|
||||||
try {
|
try {
|
||||||
const resultParsed = JSON.parse(result.body.details)
|
const resultParsed = result.log
|
||||||
|
let sysUserId = ''
|
||||||
|
|
||||||
if (resultParsed && resultParsed.body) {
|
const sysUserIdLog = resultParsed
|
||||||
let sysUserId = ''
|
.split('\n')
|
||||||
|
.find((line: string) => line.startsWith('SYSUSERID='))
|
||||||
|
|
||||||
const sysUserIdLog = resultParsed.body
|
if (sysUserIdLog) {
|
||||||
.split('\n')
|
sysUserId = sysUserIdLog.replace('SYSUSERID=', '')
|
||||||
.find((line: string) => line.startsWith('SYSUSERID='))
|
|
||||||
|
|
||||||
if (sysUserIdLog) {
|
executableContexts.push({
|
||||||
sysUserId = sysUserIdLog.replace('SYSUSERID=', '')
|
createdBy: contextsList[index].createdBy,
|
||||||
|
id: contextsList[index].id,
|
||||||
executableContexts.push({
|
name: contextsList[index].name,
|
||||||
createdBy: contextsList[index].createdBy,
|
version: contextsList[index].version,
|
||||||
id: contextsList[index].id,
|
attributes: {
|
||||||
name: contextsList[index].name,
|
sysUserId
|
||||||
version: contextsList[index].version,
|
}
|
||||||
attributes: {
|
})
|
||||||
sysUserId
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error
|
throw error
|
||||||
@@ -333,7 +338,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) {
|
||||||
@@ -418,10 +425,11 @@ export class SASViyaApiClient {
|
|||||||
* @param linesOfCode - an array of code lines to execute.
|
* @param linesOfCode - an array of code lines to execute.
|
||||||
* @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 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).
|
||||||
|
* @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 }.
|
||||||
*/
|
*/
|
||||||
public async executeScript(
|
public async executeScript(
|
||||||
jobPath: string,
|
jobPath: string,
|
||||||
@@ -429,7 +437,10 @@ export class SASViyaApiClient {
|
|||||||
contextName: string,
|
contextName: string,
|
||||||
accessToken?: string,
|
accessToken?: string,
|
||||||
data = null,
|
data = null,
|
||||||
expectWebout = false
|
debug: boolean = false,
|
||||||
|
expectWebout = false,
|
||||||
|
waitForResult = true,
|
||||||
|
pollOptions?: PollOptions
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const headers: any = {
|
const headers: any = {
|
||||||
@@ -441,7 +452,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 } = {
|
||||||
@@ -453,7 +469,7 @@ export class SASViyaApiClient {
|
|||||||
_OMITTEXTLOG: true
|
_OMITTEXTLOG: true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.debug) {
|
if (debug) {
|
||||||
jobArguments['_OMITTEXTLOG'] = false
|
jobArguments['_OMITTEXTLOG'] = false
|
||||||
jobArguments['_OMITSESSIONRESULTS'] = false
|
jobArguments['_OMITSESSIONRESULTS'] = false
|
||||||
jobArguments['_DEBUG'] = 131
|
jobArguments['_DEBUG'] = 131
|
||||||
@@ -480,7 +496,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
|
||||||
|
|
||||||
@@ -511,9 +529,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 (this.debug) {
|
if (!waitForResult) {
|
||||||
|
return session
|
||||||
|
}
|
||||||
|
|
||||||
|
if (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}${
|
||||||
@@ -522,37 +546,50 @@ export class SASViyaApiClient {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const jobStatus = await this.pollJobState(postedJob, etag, accessToken)
|
const jobStatus = await this.pollJobState(
|
||||||
|
postedJob,
|
||||||
|
etag,
|
||||||
|
accessToken,
|
||||||
|
pollOptions
|
||||||
|
)
|
||||||
|
|
||||||
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 (this.debug && logLink) {
|
if (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') {
|
||||||
return Promise.reject({ error: currentJob.error, log })
|
return Promise.reject({ job: currentJob, log })
|
||||||
}
|
}
|
||||||
|
|
||||||
let resultLink
|
let resultLink
|
||||||
|
|
||||||
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) {
|
||||||
@@ -568,16 +605,18 @@ export class SASViyaApiClient {
|
|||||||
{
|
{
|
||||||
headers
|
headers
|
||||||
}
|
}
|
||||||
).then((res: any) =>
|
|
||||||
res.result.items.map((i: any) => i.line).join('\n')
|
|
||||||
)
|
)
|
||||||
|
.then((res: any) =>
|
||||||
return Promise.reject(
|
res.result.items.map((i: any) => i.line).join('\n')
|
||||||
new ErrorResponse('Job execution failed', {
|
)
|
||||||
status: 500,
|
.catch((err) => {
|
||||||
body: log
|
throw err
|
||||||
})
|
})
|
||||||
)
|
|
||||||
|
return Promise.reject({
|
||||||
|
status: 500,
|
||||||
|
log: log
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@@ -586,7 +625,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) {
|
||||||
@@ -596,7 +639,10 @@ export class SASViyaApiClient {
|
|||||||
linesOfCode,
|
linesOfCode,
|
||||||
contextName,
|
contextName,
|
||||||
accessToken,
|
accessToken,
|
||||||
data
|
data,
|
||||||
|
debug,
|
||||||
|
false,
|
||||||
|
true
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
throw e
|
throw e
|
||||||
@@ -909,13 +955,19 @@ 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.
|
||||||
|
* @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 }.
|
||||||
*/
|
*/
|
||||||
public async executeComputeJob(
|
public async executeComputeJob(
|
||||||
sasJob: string,
|
sasJob: string,
|
||||||
contextName: string,
|
contextName: string,
|
||||||
debug: boolean,
|
debug?: boolean,
|
||||||
data?: any,
|
data?: any,
|
||||||
accessToken?: string
|
accessToken?: string,
|
||||||
|
waitForResult = true,
|
||||||
|
expectWebout = false,
|
||||||
|
pollOptions?: PollOptions
|
||||||
) {
|
) {
|
||||||
if (isRelativePath(sasJob) && !this.rootFolderName) {
|
if (isRelativePath(sasJob) && !this.rootFolderName) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -989,6 +1041,8 @@ 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,
|
||||||
@@ -996,7 +1050,10 @@ export class SASViyaApiClient {
|
|||||||
contextName,
|
contextName,
|
||||||
accessToken,
|
accessToken,
|
||||||
data,
|
data,
|
||||||
true
|
debug,
|
||||||
|
expectWebout,
|
||||||
|
waitForResult,
|
||||||
|
pollOptions
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1068,7 +1125,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'
|
||||||
@@ -1189,13 +1246,21 @@ export class SASViyaApiClient {
|
|||||||
this.folderMap.set(path, itemsAtRoot)
|
this.folderMap.set(path, itemsAtRoot)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// REFACTOR: set default value for 'pollOptions' attribute
|
||||||
private async pollJobState(
|
private async pollJobState(
|
||||||
postedJob: any,
|
postedJob: any,
|
||||||
etag: string | null,
|
etag: string | null,
|
||||||
accessToken?: string
|
accessToken?: string,
|
||||||
|
pollOptions?: PollOptions
|
||||||
) {
|
) {
|
||||||
const MAX_POLL_COUNT = 1000
|
let POLL_INTERVAL = 100
|
||||||
const POLL_INTERVAL = 100
|
let MAX_POLL_COUNT = 1000
|
||||||
|
|
||||||
|
if (pollOptions) {
|
||||||
|
POLL_INTERVAL = pollOptions.POLL_INTERVAL || POLL_INTERVAL
|
||||||
|
MAX_POLL_COUNT = pollOptions.MAX_POLL_COUNT || MAX_POLL_COUNT
|
||||||
|
}
|
||||||
|
|
||||||
let postedJobState = ''
|
let postedJobState = ''
|
||||||
let pollCount = 0
|
let pollCount = 0
|
||||||
const headers: any = {
|
const headers: any = {
|
||||||
|
|||||||
207
src/SASjs.ts
207
src/SASjs.ts
@@ -32,7 +32,8 @@ import {
|
|||||||
CsrfToken,
|
CsrfToken,
|
||||||
UploadFile,
|
UploadFile,
|
||||||
EditContextInput,
|
EditContextInput,
|
||||||
ErrorResponse
|
ErrorResponse,
|
||||||
|
PollOptions
|
||||||
} from './types'
|
} from './types'
|
||||||
import { SASViyaApiClient } from './SASViyaApiClient'
|
import { SASViyaApiClient } from './SASViyaApiClient'
|
||||||
import { SAS9ApiClient } from './SAS9ApiClient'
|
import { SAS9ApiClient } from './SAS9ApiClient'
|
||||||
@@ -44,7 +45,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
|
||||||
}
|
}
|
||||||
@@ -411,6 +412,29 @@ export default class SASjs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether a session is active, or login is required.
|
* Checks whether a session is active, or login is required.
|
||||||
* @returns - a promise which resolves with an object containing two values - a boolean `isLoggedIn`, and a string `userName`.
|
* @returns - a promise which resolves with an object containing two values - a boolean `isLoggedIn`, and a string `userName`.
|
||||||
@@ -419,10 +443,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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -440,7 +470,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()
|
||||||
|
|
||||||
@@ -450,15 +480,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,
|
||||||
@@ -670,6 +698,54 @@ 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.
|
||||||
|
* @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 }.
|
||||||
|
*/
|
||||||
|
public async startComputeJob(
|
||||||
|
sasJob: string,
|
||||||
|
data: any,
|
||||||
|
config: any = {},
|
||||||
|
accessToken?: string,
|
||||||
|
waitForResult?: boolean,
|
||||||
|
pollOptions?: PollOptions
|
||||||
|
) {
|
||||||
|
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,
|
||||||
|
config.debug,
|
||||||
|
data,
|
||||||
|
accessToken,
|
||||||
|
!!waitForResult,
|
||||||
|
false,
|
||||||
|
pollOptions
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private async executeJobViaComputeApi(
|
private async executeJobViaComputeApi(
|
||||||
sasJob: string,
|
sasJob: string,
|
||||||
data: any,
|
data: any,
|
||||||
@@ -689,13 +765,17 @@ 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,
|
config.debug,
|
||||||
data,
|
data,
|
||||||
accessToken
|
accessToken,
|
||||||
|
waitForResult,
|
||||||
|
expectWebout
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (!config.debug) {
|
if (!config.debug) {
|
||||||
@@ -733,11 +813,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
|
||||||
@@ -745,10 +837,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)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -811,8 +901,8 @@ export default class SASjs {
|
|||||||
|
|
||||||
return responseJson
|
return responseJson
|
||||||
})
|
})
|
||||||
.catch(async (e) => {
|
.catch(async (response) => {
|
||||||
if (needsRetry(JSON.stringify(e))) {
|
if (needsRetry(JSON.stringify(response))) {
|
||||||
if (this.retryCountJeseApi < requestRetryLimit) {
|
if (this.retryCountJeseApi < requestRetryLimit) {
|
||||||
let retryResponse = await this.executeJobViaJesApi(
|
let retryResponse = await this.executeJobViaJesApi(
|
||||||
sasJob,
|
sasJob,
|
||||||
@@ -828,12 +918,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 (response?.log) {
|
||||||
|
this.appendSasjsRequest(response.log, sasJob, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.toString().includes('Job was not found')) {
|
||||||
|
reject(
|
||||||
|
new ErrorResponse('Service not found on the server.', {
|
||||||
|
sasJob: sasJob
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
reject(new ErrorResponse('Job execution failed.', response))
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1017,7 +1119,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)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -1035,7 +1137,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 }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -1044,7 +1146,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
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -1053,19 +1155,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
|
||||||
})
|
})
|
||||||
@@ -1076,7 +1193,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))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -1217,10 +1334,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))
|
||||||
@@ -1318,11 +1445,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) === '/') {
|
||||||
@@ -1387,26 +1518,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[],
|
||||||
|
|||||||
@@ -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(
|
||||||
@@ -27,19 +33,22 @@ export class SessionManager {
|
|||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,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 =
|
||||||
@@ -107,6 +138,8 @@ export class SessionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.currentContext = currentContext
|
this.currentContext = currentContext
|
||||||
|
|
||||||
|
Promise.resolve()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,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}`
|
||||||
}
|
}
|
||||||
@@ -132,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 (this.debug) {
|
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 (this.debug) {
|
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 {
|
||||||
@@ -169,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,
|
||||||
@@ -177,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
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
137
src/test/FileUploader.spec.ts
Normal file
137
src/test/FileUploader.spec.ts
Normal 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()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -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
|
||||||
|
|||||||
4
src/types/PollOptions.ts
Normal file
4
src/types/PollOptions.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export interface PollOptions {
|
||||||
|
MAX_POLL_COUNT?: number
|
||||||
|
POLL_INTERVAL?: number
|
||||||
|
}
|
||||||
@@ -12,3 +12,4 @@ export * from './SASjsWaitingRequest'
|
|||||||
export * from './ServerType'
|
export * from './ServerType'
|
||||||
export * from './Session'
|
export * from './Session'
|
||||||
export * from './UploadFile'
|
export * from './UploadFile'
|
||||||
|
export * from './PollOptions'
|
||||||
|
|||||||
@@ -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, and also the scopes granted when registering your CLIENT_ID. ${
|
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 }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ const browserConfig = {
|
|||||||
const nodeConfig = {
|
const nodeConfig = {
|
||||||
...browserConfig,
|
...browserConfig,
|
||||||
target: 'node',
|
target: 'node',
|
||||||
|
entry: './node/index.ts',
|
||||||
output: {
|
output: {
|
||||||
...browserConfig.output,
|
...browserConfig.output,
|
||||||
path: path.resolve(__dirname, 'build', 'node')
|
path: path.resolve(__dirname, 'build', 'node')
|
||||||
|
|||||||
Reference in New Issue
Block a user