mirror of
https://github.com/sasjs/adapter.git
synced 2026-01-04 03:00:05 +00:00
Compare commits
70 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7bf53858f0 | ||
|
|
02780d0bcd | ||
|
|
6356aed06b | ||
|
|
69fd7b2cb5 | ||
|
|
5d1eed1494 | ||
|
|
e2e2824f37 | ||
|
|
d461135980 | ||
|
|
65fbae7610 | ||
|
|
761428502a | ||
|
|
6eb2ceaf53 | ||
|
|
66813b9824 | ||
|
|
140d8e4eac | ||
|
|
0d730e0576 | ||
|
|
ca18fcecf0 | ||
|
|
009069169f | ||
|
|
6d166efd11 | ||
|
|
1b117a67aa | ||
|
|
9037160362 | ||
|
|
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 | ||
|
|
7279c23fe2 | ||
|
|
80707d77d9 | ||
|
|
3f796b300d |
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-804.reflection-219.fileuploader.html
Normal file
231
docs/classes/reflection-804.reflection-219.fileuploader.html
Normal file
File diff suppressed because one or more lines are too long
312
docs/classes/reflection-804.reflection-219.sas9apiclient.html
Normal file
312
docs/classes/reflection-804.reflection-219.sas9apiclient.html
Normal file
File diff suppressed because one or more lines are too long
1670
docs/classes/reflection-804.reflection-219.sasjs.html
Normal file
1670
docs/classes/reflection-804.reflection-219.sasjs.html
Normal file
File diff suppressed because one or more lines are too long
1471
docs/classes/reflection-804.reflection-219.sasviyaapiclient.html
Normal file
1471
docs/classes/reflection-804.reflection-219.sasviyaapiclient.html
Normal file
File diff suppressed because one or more lines are too long
323
docs/classes/reflection-804.reflection-219.sessionmanager.html
Normal file
323
docs/classes/reflection-804.reflection-219.sessionmanager.html
Normal file
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
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
212
docs/interfaces/types.polloptions.html
Normal file
212
docs/interfaces/types.polloptions.html
Normal file
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
106
docs/modules/reflection-804.html
Normal file
106
docs/modules/reflection-804.html
Normal file
File diff suppressed because one or more lines are too long
128
docs/modules/reflection-804.reflection-219.html
Normal file
128
docs/modules/reflection-804.reflection-219.html
Normal file
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
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();
|
|
||||||
});
|
|
||||||
@@ -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 &&
|
||||||
|
|||||||
@@ -25,17 +25,71 @@ export const computeTests = (adapter: SASjs): TestSuite => ({
|
|||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => {
|
||||||
const expectedProperties = ["id", "state", "creationTimeStamp", "jobConditionCode"]
|
const expectedProperties = ["id", "state", "creationTimeStamp", "jobConditionCode"]
|
||||||
return validate(expectedProperties, res);
|
return validate(expectedProperties, res.result);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Execute Script Viya - complete job",
|
||||||
|
description: "Should execute sas file and return log",
|
||||||
|
test: () => {
|
||||||
|
const fileLines = [
|
||||||
|
`data;`,
|
||||||
|
`do x=1 to 100;`,
|
||||||
|
`output;`,
|
||||||
|
`end;`,
|
||||||
|
`run;`
|
||||||
|
]
|
||||||
|
|
||||||
|
return adapter.executeScriptSASViya(
|
||||||
|
'sasCode.sas',
|
||||||
|
fileLines,
|
||||||
|
'SAS Studio compute context',
|
||||||
|
undefined,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
},
|
||||||
|
assertion: (res: any) => {
|
||||||
|
const expectedLogContent = `1 data;\\n2 do x=1 to 100;\\n3 output;\\n4 end;\\n5 run;\\n\\n`
|
||||||
|
|
||||||
|
return validateLog(expectedLogContent, res.log);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Execute Script Viya - failed job",
|
||||||
|
description: "Should execute sas file and return log",
|
||||||
|
test: () => {
|
||||||
|
const fileLines = [
|
||||||
|
`%abort;`
|
||||||
|
]
|
||||||
|
|
||||||
|
return adapter.executeScriptSASViya(
|
||||||
|
'sasCode.sas',
|
||||||
|
fileLines,
|
||||||
|
'SAS Studio compute context',
|
||||||
|
undefined,
|
||||||
|
true
|
||||||
|
).catch((err: any) => err )
|
||||||
|
},
|
||||||
|
assertion: (res: any) => {
|
||||||
|
const expectedLogContent = `1 %abort;\\nERROR: The %ABORT statement is not valid in open code.\\n`
|
||||||
|
|
||||||
|
return validateLog(expectedLogContent, res.log);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const validateLog = (text: string, log: string): boolean => {
|
||||||
|
const isValid = JSON.stringify(log).includes(text)
|
||||||
|
|
||||||
|
return isValid
|
||||||
|
}
|
||||||
|
|
||||||
const validate = (expectedProperties: string[], data: any): boolean => {
|
const validate = (expectedProperties: string[], data: any): boolean => {
|
||||||
const actualProperties = Object.keys(data);
|
const actualProperties = Object.keys(data);
|
||||||
|
|
||||||
const isValid = expectedProperties.every(
|
const isValid = expectedProperties.every(
|
||||||
(property) => actualProperties.includes(property)
|
(property) => actualProperties.includes(property)
|
||||||
);
|
);
|
||||||
return isValid
|
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'
|
||||||
@@ -154,6 +155,7 @@ export class SASViyaApiClient {
|
|||||||
context.name,
|
context.name,
|
||||||
accessToken,
|
accessToken,
|
||||||
null,
|
null,
|
||||||
|
false,
|
||||||
true,
|
true,
|
||||||
true
|
true
|
||||||
).catch((err) => err)
|
).catch((err) => err)
|
||||||
@@ -164,30 +166,27 @@ export class SASViyaApiClient {
|
|||||||
for (const promise of promises) results.push(await promise())
|
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
|
||||||
@@ -426,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,
|
||||||
@@ -437,8 +437,10 @@ export class SASViyaApiClient {
|
|||||||
contextName: string,
|
contextName: string,
|
||||||
accessToken?: string,
|
accessToken?: string,
|
||||||
data = null,
|
data = null,
|
||||||
|
debug: boolean = false,
|
||||||
expectWebout = false,
|
expectWebout = false,
|
||||||
waitForResult = true
|
waitForResult = true,
|
||||||
|
pollOptions?: PollOptions
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const headers: any = {
|
const headers: any = {
|
||||||
@@ -467,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
|
||||||
@@ -535,7 +537,7 @@ export class SASViyaApiClient {
|
|||||||
return session
|
return session
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.debug) {
|
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}${
|
||||||
@@ -544,7 +546,12 @@ 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}`,
|
||||||
@@ -558,7 +565,7 @@ export class SASViyaApiClient {
|
|||||||
|
|
||||||
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`,
|
||||||
{
|
{
|
||||||
@@ -574,7 +581,7 @@ export class SASViyaApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
@@ -582,7 +589,7 @@ export class SASViyaApiClient {
|
|||||||
if (expectWebout) {
|
if (expectWebout) {
|
||||||
resultLink = `/compute/sessions/${executionSessionId}/filerefs/_webout/content`
|
resultLink = `/compute/sessions/${executionSessionId}/filerefs/_webout/content`
|
||||||
} else {
|
} else {
|
||||||
return currentJob
|
return { job: currentJob, log }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resultLink) {
|
if (resultLink) {
|
||||||
@@ -606,12 +613,10 @@ export class SASViyaApiClient {
|
|||||||
throw err
|
throw err
|
||||||
})
|
})
|
||||||
|
|
||||||
return Promise.reject(
|
return Promise.reject({
|
||||||
new ErrorResponse('Job execution failed', {
|
status: 500,
|
||||||
status: 500,
|
log: log
|
||||||
body: log
|
})
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@@ -635,6 +640,7 @@ export class SASViyaApiClient {
|
|||||||
contextName,
|
contextName,
|
||||||
accessToken,
|
accessToken,
|
||||||
data,
|
data,
|
||||||
|
debug,
|
||||||
false,
|
false,
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
@@ -951,14 +957,17 @@ export class SASViyaApiClient {
|
|||||||
* @param accessToken - an optional access token for an authorized user.
|
* @param accessToken - an optional access token for an authorized user.
|
||||||
* @param waitForResult - a boolean indicating if the function should wait for a result.
|
* @param waitForResult - a boolean indicating if the function should wait for a result.
|
||||||
* @param expectWebout - a boolean indicating whether to expect a _webout response.
|
* @param expectWebout - a boolean indicating whether to expect a _webout response.
|
||||||
|
* @param pollOptions - an object that represents poll interval(milliseconds) and maximum amount of attempts. Object example: { MAX_POLL_COUNT: 24 * 60 * 60, POLL_INTERVAL: 1000 }.
|
||||||
*/
|
*/
|
||||||
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,
|
waitForResult = true,
|
||||||
expectWebout = false
|
expectWebout = false,
|
||||||
|
pollOptions?: PollOptions
|
||||||
) {
|
) {
|
||||||
if (isRelativePath(sasJob) && !this.rootFolderName) {
|
if (isRelativePath(sasJob) && !this.rootFolderName) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -1041,8 +1050,10 @@ export class SASViyaApiClient {
|
|||||||
contextName,
|
contextName,
|
||||||
accessToken,
|
accessToken,
|
||||||
data,
|
data,
|
||||||
|
debug,
|
||||||
expectWebout,
|
expectWebout,
|
||||||
waitForResult
|
waitForResult,
|
||||||
|
pollOptions
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1114,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'
|
||||||
@@ -1227,7 +1238,7 @@ export class SASViyaApiClient {
|
|||||||
throw new Error(`The path ${path} does not exist on ${this.serverUrl}`)
|
throw new Error(`The path ${path} does not exist on ${this.serverUrl}`)
|
||||||
}
|
}
|
||||||
const { result: members } = await this.request<{ items: any[] }>(
|
const { result: members } = await this.request<{ items: any[] }>(
|
||||||
`${this.serverUrl}/folders/folders/${folder.id}/members`,
|
`${this.serverUrl}/folders/folders/${folder.id}/members?limit=${folder.memberCount}`,
|
||||||
requestInfo
|
requestInfo
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1235,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 = {
|
||||||
|
|||||||
148
src/SASjs.ts
148
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'
|
||||||
@@ -205,11 +206,20 @@ export default class SASjs {
|
|||||||
return await this.sasViyaApiClient!.createSession(contextName, accessToken)
|
return await this.sasViyaApiClient!.createSession(contextName, accessToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes the sas code against given sas server
|
||||||
|
* @param fileName - name of the file to run. It will be converted to path to the file being submitted for execution.
|
||||||
|
* @param linesOfCode - lines of sas code from the file to run.
|
||||||
|
* @param contextName - context name on which code will be run on the server.
|
||||||
|
* @param accessToken - (optional) the access token for authorizing the request.
|
||||||
|
* @param debug - (optional) if true, global debug config will be overriden
|
||||||
|
*/
|
||||||
public async executeScriptSASViya(
|
public async executeScriptSASViya(
|
||||||
fileName: string,
|
fileName: string,
|
||||||
linesOfCode: string[],
|
linesOfCode: string[],
|
||||||
contextName: string,
|
contextName: string,
|
||||||
accessToken?: string
|
accessToken?: string,
|
||||||
|
debug?: boolean
|
||||||
) {
|
) {
|
||||||
this.isMethodSupported('executeScriptSASViya', ServerType.SASViya)
|
this.isMethodSupported('executeScriptSASViya', ServerType.SASViya)
|
||||||
|
|
||||||
@@ -218,7 +228,8 @@ export default class SASjs {
|
|||||||
linesOfCode,
|
linesOfCode,
|
||||||
contextName,
|
contextName,
|
||||||
accessToken,
|
accessToken,
|
||||||
null
|
null,
|
||||||
|
debug ? debug : this.sasjsConfig.debug
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,6 +422,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 +453,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 +480,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 +490,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,
|
||||||
@@ -684,13 +722,15 @@ export default class SASjs {
|
|||||||
* @param accessToken - a valid access token that is authorised to execute compute jobs.
|
* @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.
|
* The access token is not required when the user is authenticated via the browser.
|
||||||
* @param waitForResult - a boolean that indicates whether the function needs to wait for execution to complete.
|
* @param waitForResult - a boolean that indicates whether the function needs to wait for execution to complete.
|
||||||
|
* @param pollOptions - an object that represents poll interval(milliseconds) and maximum amount of attempts. Object example: { MAX_POLL_COUNT: 24 * 60 * 60, POLL_INTERVAL: 1000 }.
|
||||||
*/
|
*/
|
||||||
public async startComputeJob(
|
public async startComputeJob(
|
||||||
sasJob: string,
|
sasJob: string,
|
||||||
data: any,
|
data: any,
|
||||||
config: any = {},
|
config: any = {},
|
||||||
accessToken?: string,
|
accessToken?: string,
|
||||||
waitForResult?: boolean
|
waitForResult?: boolean,
|
||||||
|
pollOptions?: PollOptions
|
||||||
) {
|
) {
|
||||||
config = {
|
config = {
|
||||||
...this.sasjsConfig,
|
...this.sasjsConfig,
|
||||||
@@ -707,10 +747,12 @@ export default class SASjs {
|
|||||||
return this.sasViyaApiClient?.executeComputeJob(
|
return this.sasViyaApiClient?.executeComputeJob(
|
||||||
sasJob,
|
sasJob,
|
||||||
config.contextName,
|
config.contextName,
|
||||||
|
config.debug,
|
||||||
data,
|
data,
|
||||||
accessToken,
|
accessToken,
|
||||||
!!waitForResult,
|
!!waitForResult,
|
||||||
false
|
false,
|
||||||
|
pollOptions
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -739,6 +781,7 @@ export default class SASjs {
|
|||||||
?.executeComputeJob(
|
?.executeComputeJob(
|
||||||
sasJob,
|
sasJob,
|
||||||
config.contextName,
|
config.contextName,
|
||||||
|
config.debug,
|
||||||
data,
|
data,
|
||||||
accessToken,
|
accessToken,
|
||||||
waitForResult,
|
waitForResult,
|
||||||
@@ -780,11 +823,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
|
||||||
@@ -792,10 +847,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)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -858,8 +911,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,
|
||||||
@@ -875,12 +928,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))
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1064,7 +1129,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)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -1082,7 +1147,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 }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -1091,7 +1156,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
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -1100,19 +1165,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
|
||||||
})
|
})
|
||||||
@@ -1123,7 +1203,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))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -1448,26 +1528,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[],
|
||||||
|
|||||||
@@ -168,7 +168,11 @@ export class SessionManager {
|
|||||||
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' ||
|
||||||
|
sessionState === 'running' ||
|
||||||
|
sessionState === ''
|
||||||
|
) {
|
||||||
if (stateLink) {
|
if (stateLink) {
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
console.log('Polling session status... \n') // ?
|
console.log('Polling session status... \n') // ?
|
||||||
|
|||||||
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,4 +4,5 @@ export interface Folder {
|
|||||||
id: string
|
id: string
|
||||||
uri: string
|
uri: string
|
||||||
links: Link[]
|
links: Link[]
|
||||||
|
memberCount: number
|
||||||
}
|
}
|
||||||
|
|||||||
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'
|
||||||
|
|||||||
@@ -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