mirror of
https://github.com/sasjs/adapter.git
synced 2026-04-21 05:01:31 +00:00
Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 232f4ec3fb | |||
| 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 | |||
| fa87111f4a | |||
| 94967b0f6c | |||
| 3f796b300d | |||
| a07c16fb52 | |||
| fd6905ea9f | |||
| 08f58b5f4f |
@@ -27,6 +27,16 @@ jobs:
|
|||||||
run: npm run lint
|
run: npm run lint
|
||||||
- name: Run unit tests
|
- name: Run unit tests
|
||||||
run: npm test
|
run: npm test
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
CLIENT: ${{secrets.CLIENT}}
|
||||||
|
SECRET: ${{secrets.SECRET}}
|
||||||
|
SAS_USERNAME: ${{secrets.SAS_USERNAME}}
|
||||||
|
SAS_PASSWORD: ${{secrets.SAS_PASSWORD}}
|
||||||
|
SERVER_URL: ${{secrets.SERVER_URL}}
|
||||||
|
SERVER_TYPE: ${{secrets.SERVER_TYPE}}
|
||||||
|
ACCESS_TOKEN: ${{secrets.ACCESS_TOKEN}}
|
||||||
|
REFRESH_TOKEN: ${{secrets.REFRESH_TOKEN}}
|
||||||
- name: Build Package
|
- name: Build Package
|
||||||
run: npm run package:lib
|
run: npm run package:lib
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -14,4 +14,5 @@ What code changes have been made to achieve the intent.
|
|||||||
|
|
||||||
- [ ] Code is formatted correctly (`npm run lint:fix`).
|
- [ ] Code is formatted correctly (`npm run lint:fix`).
|
||||||
- [ ] All unit tests are passing (`npm test`).
|
- [ ] All unit tests are passing (`npm test`).
|
||||||
|
- [ ] All `sasjs-tests` unit tests are passing (`npm test`).
|
||||||
- [ ] All `sasjs-tests` are passing (instructions available [here](https://github.com/sasjs/adapter/blob/master/sasjs-tests/README.md)).
|
- [ ] All `sasjs-tests` are passing (instructions available [here](https://github.com/sasjs/adapter/blob/master/sasjs-tests/README.md)).
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
@@ -76,7 +76,7 @@
|
|||||||
<section class="tsd-index-section ">
|
<section class="tsd-index-section ">
|
||||||
<h3>Modules</h3>
|
<h3>Modules</h3>
|
||||||
<ul class="tsd-index-list">
|
<ul class="tsd-index-list">
|
||||||
<li class="tsd-kind-module tsd-is-not-exported"><a href="modules/reflection-762.html" class="tsd-kind-icon"><em>Module</em></a></li>
|
<li class="tsd-kind-module tsd-is-not-exported"><a href="modules/reflection-790.html" class="tsd-kind-icon"><em>Module</em></a></li>
|
||||||
<li class="tsd-kind-module"><a href="modules/types.html" class="tsd-kind-icon">types</a></li>
|
<li class="tsd-kind-module"><a href="modules/types.html" class="tsd-kind-icon">types</a></li>
|
||||||
<li class="tsd-kind-module"><a href="modules/utils.html" class="tsd-kind-icon">utils</a></li>
|
<li class="tsd-kind-module"><a href="modules/utils.html" class="tsd-kind-icon">utils</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Generated
+1044
-1539
File diff suppressed because it is too large
Load Diff
+6
-5
@@ -37,15 +37,16 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/isomorphic-fetch": "0.0.35",
|
"@types/isomorphic-fetch": "0.0.35",
|
||||||
"@types/jest": "^26.0.14",
|
"@types/jest": "^26.0.15",
|
||||||
"cp": "^0.2.0",
|
"cp": "^0.2.0",
|
||||||
|
"dotenv": "^8.2.0",
|
||||||
"jest": "^25.5.4",
|
"jest": "^25.5.4",
|
||||||
"path": "^0.12.7",
|
"path": "^0.12.7",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"semantic-release": "^17.1.2",
|
"semantic-release": "^17.2.3",
|
||||||
"terser-webpack-plugin": "^4.2.2",
|
"terser-webpack-plugin": "^4.2.3",
|
||||||
"ts-jest": "^25.5.1",
|
"ts-jest": "^25.5.1",
|
||||||
"ts-loader": "^8.0.4",
|
"ts-loader": "^8.0.11",
|
||||||
"tslint": "^6.1.3",
|
"tslint": "^6.1.3",
|
||||||
"tslint-config-prettier": "^1.18.0",
|
"tslint-config-prettier": "^1.18.0",
|
||||||
"typedoc": "^0.17.8",
|
"typedoc": "^0.17.8",
|
||||||
@@ -53,7 +54,7 @@
|
|||||||
"typedoc-plugin-external-module-name": "^4.0.3",
|
"typedoc-plugin-external-module-name": "^4.0.3",
|
||||||
"typescript": "^3.9.7",
|
"typescript": "^3.9.7",
|
||||||
"webpack": "^4.44.2",
|
"webpack": "^4.44.2",
|
||||||
"webpack-cli": "^3.3.12"
|
"webpack-cli": "^4.2.0"
|
||||||
},
|
},
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
+15
-4
@@ -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,8 +19,11 @@ 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) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
if (files?.length < 1)
|
if (files?.length < 1)
|
||||||
throw new Error('At least one file must be provided.')
|
reject(new ErrorResponse('At least one file must be provided.'))
|
||||||
|
if (!sasJob || sasJob === '')
|
||||||
|
reject(new ErrorResponse('sasJob must be provided.'))
|
||||||
|
|
||||||
let paramsString = ''
|
let paramsString = ''
|
||||||
|
|
||||||
@@ -40,7 +44,6 @@ export class FileUploader {
|
|||||||
'cache-control': 'no-cache'
|
'cache-control': 'no-cache'
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
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))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+75
-23
@@ -52,8 +52,10 @@ export class SASViyaApiClient {
|
|||||||
|
|
||||||
public set debug(value: boolean) {
|
public set debug(value: boolean) {
|
||||||
this._debug = value
|
this._debug = value
|
||||||
|
if (this.sessionManager) {
|
||||||
this.sessionManager.debug = value
|
this.sessionManager.debug = value
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a list of jobs in the currently set root folder.
|
* Returns a list of jobs in the currently set root folder.
|
||||||
@@ -145,31 +147,36 @@ 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 () =>
|
||||||
|
this.executeScript(
|
||||||
`test-${context.name}`,
|
`test-${context.name}`,
|
||||||
linesOfCode,
|
linesOfCode,
|
||||||
context.name,
|
context.name,
|
||||||
accessToken,
|
accessToken,
|
||||||
null,
|
null,
|
||||||
|
true,
|
||||||
true
|
true
|
||||||
).catch(() => null)
|
).catch((err) => err)
|
||||||
})
|
})
|
||||||
|
|
||||||
const results = await Promise.all(promises)
|
let results: any[] = []
|
||||||
|
|
||||||
|
for (const promise of promises) results.push(await promise())
|
||||||
|
|
||||||
results.forEach((result: any, index: number) => {
|
results.forEach((result: any, index: number) => {
|
||||||
if (result) {
|
if (result && result.error && result.error.details) {
|
||||||
|
try {
|
||||||
|
const resultParsed = result.error.details
|
||||||
|
|
||||||
|
if (resultParsed && resultParsed.body) {
|
||||||
let sysUserId = ''
|
let sysUserId = ''
|
||||||
|
|
||||||
if (result.log) {
|
const sysUserIdLog = resultParsed.body
|
||||||
const sysUserIdLog = result.log
|
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.find((line: string) => line.startsWith('SYSUSERID='))
|
.find((line: string) => line.startsWith('SYSUSERID='))
|
||||||
|
|
||||||
if (sysUserIdLog) {
|
if (sysUserIdLog) {
|
||||||
sysUserId = sysUserIdLog.replace('SYSUSERID=', '')
|
sysUserId = sysUserIdLog.replace('SYSUSERID=', '')
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
executableContexts.push({
|
executableContexts.push({
|
||||||
createdBy: contextsList[index].createdBy,
|
createdBy: contextsList[index].createdBy,
|
||||||
@@ -181,6 +188,11 @@ export class SASViyaApiClient {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return executableContexts
|
return executableContexts
|
||||||
@@ -327,7 +339,9 @@ export class SASViyaApiClient {
|
|||||||
originalContext = await this.getComputeContextByName(
|
originalContext = await this.getComputeContextByName(
|
||||||
contextName,
|
contextName,
|
||||||
accessToken
|
accessToken
|
||||||
).catch((_) => {})
|
).catch((err) => {
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
|
||||||
// Try to find context by id, when context name has been changed.
|
// Try to find context by id, when context name has been changed.
|
||||||
if (!originalContext) {
|
if (!originalContext) {
|
||||||
@@ -423,7 +437,8 @@ export class SASViyaApiClient {
|
|||||||
contextName: string,
|
contextName: string,
|
||||||
accessToken?: string,
|
accessToken?: string,
|
||||||
data = null,
|
data = null,
|
||||||
expectWebout = false
|
expectWebout = false,
|
||||||
|
waitForResult = true
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const headers: any = {
|
const headers: any = {
|
||||||
@@ -435,7 +450,12 @@ export class SASViyaApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let executionSessionId: string
|
let executionSessionId: string
|
||||||
const session = await this.sessionManager.getSession(accessToken)
|
const session = await this.sessionManager
|
||||||
|
.getSession(accessToken)
|
||||||
|
.catch((err) => {
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
|
||||||
executionSessionId = session!.id
|
executionSessionId = session!.id
|
||||||
|
|
||||||
const jobArguments: { [key: string]: any } = {
|
const jobArguments: { [key: string]: any } = {
|
||||||
@@ -474,7 +494,9 @@ export class SASViyaApiClient {
|
|||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
if (JSON.stringify(data).includes(';')) {
|
if (JSON.stringify(data).includes(';')) {
|
||||||
files = await this.uploadTables(data, accessToken)
|
files = await this.uploadTables(data, accessToken).catch((err) => {
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
|
||||||
jobVariables['_webin_file_count'] = files.length
|
jobVariables['_webin_file_count'] = files.length
|
||||||
|
|
||||||
@@ -505,7 +527,13 @@ 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 (!waitForResult) {
|
||||||
|
return session
|
||||||
|
}
|
||||||
|
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
console.log(`Job has been submitted for '${fileName}'.`)
|
console.log(`Job has been submitted for '${fileName}'.`)
|
||||||
@@ -521,7 +549,9 @@ export class SASViyaApiClient {
|
|||||||
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
|
||||||
@@ -534,9 +564,13 @@ export class SASViyaApiClient {
|
|||||||
{
|
{
|
||||||
headers
|
headers
|
||||||
}
|
}
|
||||||
).then((res: any) =>
|
)
|
||||||
|
.then((res: any) =>
|
||||||
res.result.items.map((i: any) => i.line).join('\n')
|
res.result.items.map((i: any) => i.line).join('\n')
|
||||||
)
|
)
|
||||||
|
.catch((err) => {
|
||||||
|
throw err
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (jobStatus === 'failed' || jobStatus === 'error') {
|
if (jobStatus === 'failed' || jobStatus === 'error') {
|
||||||
@@ -547,6 +581,8 @@ export class SASViyaApiClient {
|
|||||||
|
|
||||||
if (expectWebout) {
|
if (expectWebout) {
|
||||||
resultLink = `/compute/sessions/${executionSessionId}/filerefs/_webout/content`
|
resultLink = `/compute/sessions/${executionSessionId}/filerefs/_webout/content`
|
||||||
|
} else {
|
||||||
|
return currentJob
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resultLink) {
|
if (resultLink) {
|
||||||
@@ -562,12 +598,16 @@ export class SASViyaApiClient {
|
|||||||
{
|
{
|
||||||
headers
|
headers
|
||||||
}
|
}
|
||||||
).then((res: any) =>
|
)
|
||||||
|
.then((res: any) =>
|
||||||
res.result.items.map((i: any) => i.line).join('\n')
|
res.result.items.map((i: any) => i.line).join('\n')
|
||||||
)
|
)
|
||||||
|
.catch((err) => {
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
|
||||||
return Promise.reject(
|
return Promise.reject(
|
||||||
new ErrorResponse('Job execution failed', {
|
new ErrorResponse('Job execution failed.', {
|
||||||
status: 500,
|
status: 500,
|
||||||
body: log
|
body: log
|
||||||
})
|
})
|
||||||
@@ -580,7 +620,11 @@ export class SASViyaApiClient {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.sessionManager.clearSession(executionSessionId, accessToken)
|
await this.sessionManager
|
||||||
|
.clearSession(executionSessionId, accessToken)
|
||||||
|
.catch((err) => {
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
|
||||||
return { result: jobResult?.result, log }
|
return { result: jobResult?.result, log }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -590,7 +634,9 @@ export class SASViyaApiClient {
|
|||||||
linesOfCode,
|
linesOfCode,
|
||||||
contextName,
|
contextName,
|
||||||
accessToken,
|
accessToken,
|
||||||
data
|
data,
|
||||||
|
false,
|
||||||
|
true
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
throw e
|
throw e
|
||||||
@@ -903,13 +949,16 @@ export class SASViyaApiClient {
|
|||||||
* @param debug - sets the _debug flag in the job arguments.
|
* @param debug - sets the _debug flag in the job arguments.
|
||||||
* @param data - any data to be passed in as input to the job.
|
* @param data - any data to be passed in as input to the job.
|
||||||
* @param accessToken - an optional access token for an authorized user.
|
* @param accessToken - an optional access token for an authorized user.
|
||||||
|
* @param waitForResult - a boolean indicating if the function should wait for a result.
|
||||||
|
* @param expectWebout - a boolean indicating whether to expect a _webout response.
|
||||||
*/
|
*/
|
||||||
public async executeComputeJob(
|
public async executeComputeJob(
|
||||||
sasJob: string,
|
sasJob: string,
|
||||||
contextName: string,
|
contextName: string,
|
||||||
debug: boolean,
|
|
||||||
data?: any,
|
data?: any,
|
||||||
accessToken?: string
|
accessToken?: string,
|
||||||
|
waitForResult = true,
|
||||||
|
expectWebout = false
|
||||||
) {
|
) {
|
||||||
if (isRelativePath(sasJob) && !this.rootFolderName) {
|
if (isRelativePath(sasJob) && !this.rootFolderName) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -983,6 +1032,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,
|
||||||
@@ -990,7 +1041,8 @@ export class SASViyaApiClient {
|
|||||||
contextName,
|
contextName,
|
||||||
accessToken,
|
accessToken,
|
||||||
data,
|
data,
|
||||||
true
|
expectWebout,
|
||||||
|
waitForResult
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1062,7 +1114,7 @@ export class SASViyaApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!jobToExecute) {
|
if (!jobToExecute) {
|
||||||
throw new Error(`The job ${sasJob} was not found.`)
|
throw new Error(`Job was not found.`)
|
||||||
}
|
}
|
||||||
const jobDefinitionLink = jobToExecute?.links.find(
|
const jobDefinitionLink = jobToExecute?.links.find(
|
||||||
(l) => l.rel === 'getResource'
|
(l) => l.rel === 'getResource'
|
||||||
|
|||||||
+150
-45
@@ -44,7 +44,7 @@ const defaultConfig: SASjsConfig = {
|
|||||||
pathSASViya: '/SASJobExecution',
|
pathSASViya: '/SASJobExecution',
|
||||||
appLoc: '/Public/seedapp',
|
appLoc: '/Public/seedapp',
|
||||||
serverType: ServerType.SASViya,
|
serverType: ServerType.SASViya,
|
||||||
debug: true,
|
debug: false,
|
||||||
contextName: 'SAS Job Execution compute context',
|
contextName: 'SAS Job Execution compute context',
|
||||||
useComputeApi: false
|
useComputeApi: false
|
||||||
}
|
}
|
||||||
@@ -411,6 +411,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 +442,16 @@ export default class SASjs {
|
|||||||
const loginResponse = await fetch(this.loginUrl.replace('.do', ''))
|
const loginResponse = await fetch(this.loginUrl.replace('.do', ''))
|
||||||
const responseText = await loginResponse.text()
|
const responseText = await loginResponse.text()
|
||||||
const isLoggedIn = /<button.+onClick.+logout/gm.test(responseText)
|
const isLoggedIn = /<button.+onClick.+logout/gm.test(responseText)
|
||||||
|
let loginForm: any = null
|
||||||
|
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
loginForm = await this.getLoginForm(responseText)
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
isLoggedIn,
|
isLoggedIn,
|
||||||
userName: this.userName
|
userName: this.userName,
|
||||||
|
loginForm
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -440,7 +469,7 @@ export default class SASjs {
|
|||||||
|
|
||||||
this.userName = loginParams.username
|
this.userName = loginParams.username
|
||||||
|
|
||||||
const { isLoggedIn } = await this.checkSession()
|
const { isLoggedIn, loginForm } = await this.checkSession()
|
||||||
if (isLoggedIn) {
|
if (isLoggedIn) {
|
||||||
this.resendWaitingRequests()
|
this.resendWaitingRequests()
|
||||||
|
|
||||||
@@ -450,15 +479,13 @@ export default class SASjs {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const loginForm = await this.getLoginForm()
|
|
||||||
|
|
||||||
for (const key in loginForm) {
|
for (const key in loginForm) {
|
||||||
loginParams[key] = loginForm[key]
|
loginParams[key] = loginForm[key]
|
||||||
}
|
}
|
||||||
const loginParamsStr = serialize(loginParams)
|
const loginParamsStr = serialize(loginParams)
|
||||||
|
|
||||||
return fetch(this.loginUrl, {
|
return fetch(this.loginUrl, {
|
||||||
method: 'post',
|
method: 'POST',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
referrerPolicy: 'same-origin',
|
referrerPolicy: 'same-origin',
|
||||||
body: loginParamsStr,
|
body: loginParamsStr,
|
||||||
@@ -670,6 +697,50 @@ export default class SASjs {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kicks off execution of the given job via the compute API.
|
||||||
|
* @returns an object representing the compute session created for the given job.
|
||||||
|
* @param sasJob - the path to the SAS program (ultimately resolves to
|
||||||
|
* the SAS `_program` parameter to run a Job Definition or SAS 9 Stored
|
||||||
|
* Process). Is prepended at runtime with the value of `appLoc`.
|
||||||
|
* @param data - a JSON object containing one or more tables to be sent to
|
||||||
|
* SAS. Can be `null` if no inputs required.
|
||||||
|
* @param config - provide any changes to the config here, for instance to
|
||||||
|
* enable/disable `debug`. Any change provided will override the global config,
|
||||||
|
* for that particular function call.
|
||||||
|
* @param accessToken - a valid access token that is authorised to execute compute jobs.
|
||||||
|
* The access token is not required when the user is authenticated via the browser.
|
||||||
|
* @param waitForResult - a boolean that indicates whether the function needs to wait for execution to complete.
|
||||||
|
*/
|
||||||
|
public async startComputeJob(
|
||||||
|
sasJob: string,
|
||||||
|
data: any,
|
||||||
|
config: any = {},
|
||||||
|
accessToken?: string,
|
||||||
|
waitForResult?: boolean
|
||||||
|
) {
|
||||||
|
config = {
|
||||||
|
...this.sasjsConfig,
|
||||||
|
...config
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isMethodSupported('startComputeJob', ServerType.SASViya)
|
||||||
|
if (!config.contextName) {
|
||||||
|
throw new Error(
|
||||||
|
'Context name is undefined. Please set a `contextName` in your SASjs or override config.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.sasViyaApiClient?.executeComputeJob(
|
||||||
|
sasJob,
|
||||||
|
config.contextName,
|
||||||
|
data,
|
||||||
|
accessToken,
|
||||||
|
!!waitForResult,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private async executeJobViaComputeApi(
|
private async executeJobViaComputeApi(
|
||||||
sasJob: string,
|
sasJob: string,
|
||||||
data: any,
|
data: any,
|
||||||
@@ -689,13 +760,16 @@ export default class SASjs {
|
|||||||
|
|
||||||
sasjsWaitingRequest.requestPromise.promise = new Promise(
|
sasjsWaitingRequest.requestPromise.promise = new Promise(
|
||||||
async (resolve, reject) => {
|
async (resolve, reject) => {
|
||||||
|
const waitForResult = true
|
||||||
|
const expectWebout = true
|
||||||
this.sasViyaApiClient
|
this.sasViyaApiClient
|
||||||
?.executeComputeJob(
|
?.executeComputeJob(
|
||||||
sasJob,
|
sasJob,
|
||||||
config.contextName,
|
config.contextName,
|
||||||
config.debug,
|
|
||||||
data,
|
data,
|
||||||
accessToken
|
accessToken,
|
||||||
|
waitForResult,
|
||||||
|
expectWebout
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (!config.debug) {
|
if (!config.debug) {
|
||||||
@@ -733,11 +807,23 @@ export default class SASjs {
|
|||||||
} else {
|
} else {
|
||||||
this.retryCountComputeApi = 0
|
this.retryCountComputeApi = 0
|
||||||
reject(
|
reject(
|
||||||
new ErrorResponse('Compute API retry requests limit reached')
|
new ErrorResponse('Compute API retry requests limit reached.')
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (response?.log) {
|
||||||
|
this.appendSasjsRequest(response.log, sasJob, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.toString().includes('Job was not found')) {
|
||||||
|
reject(
|
||||||
|
new ErrorResponse('Service not found on the server.', {
|
||||||
|
sasJob: sasJob
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (error && error.status === 401) {
|
if (error && error.status === 401) {
|
||||||
if (loginRequiredCallback) loginRequiredCallback(true)
|
if (loginRequiredCallback) loginRequiredCallback(true)
|
||||||
sasjsWaitingRequest.requestPromise.resolve = resolve
|
sasjsWaitingRequest.requestPromise.resolve = resolve
|
||||||
@@ -745,10 +831,8 @@ export default class SASjs {
|
|||||||
sasjsWaitingRequest.config = config
|
sasjsWaitingRequest.config = config
|
||||||
this.sasjsWaitingRequests.push(sasjsWaitingRequest)
|
this.sasjsWaitingRequests.push(sasjsWaitingRequest)
|
||||||
} else {
|
} else {
|
||||||
reject(new ErrorResponse('Job execution failed', error))
|
reject(new ErrorResponse('Job execution failed.', error))
|
||||||
}
|
}
|
||||||
|
|
||||||
this.appendSasjsRequest(response.log, sasJob, null)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -828,12 +912,24 @@ export default class SASjs {
|
|||||||
} else {
|
} else {
|
||||||
this.retryCountJeseApi = 0
|
this.retryCountJeseApi = 0
|
||||||
reject(
|
reject(
|
||||||
new ErrorResponse('Jes API retry requests limit reached')
|
new ErrorResponse('Jes API retry requests limit reached.')
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reject(new ErrorResponse('Job execution failed', e))
|
if (e?.log) {
|
||||||
|
this.appendSasjsRequest(e.log, sasJob, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.toString().includes('Job was not found')) {
|
||||||
|
reject(
|
||||||
|
new ErrorResponse('Service not found on the server.', {
|
||||||
|
sasJob: sasJob
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
reject(new ErrorResponse('Job execution failed.', e))
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1017,7 +1113,7 @@ export default class SASjs {
|
|||||||
} else {
|
} else {
|
||||||
reject(
|
reject(
|
||||||
new ErrorResponse(
|
new ErrorResponse(
|
||||||
'Job WEB execution failed',
|
'Job WEB execution failed.',
|
||||||
this.parseSAS9ErrorResponse(responseText)
|
this.parseSAS9ErrorResponse(responseText)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -1035,7 +1131,7 @@ export default class SASjs {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
reject(
|
reject(
|
||||||
new ErrorResponse(
|
new ErrorResponse(
|
||||||
'Job WEB debug response parsing failed',
|
'Job WEB debug response parsing failed.',
|
||||||
{ response: resText, exception: e }
|
{ response: resText, exception: e }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -1044,7 +1140,7 @@ export default class SASjs {
|
|||||||
(err: any) => {
|
(err: any) => {
|
||||||
reject(
|
reject(
|
||||||
new ErrorResponse(
|
new ErrorResponse(
|
||||||
'Job WEB debug response parsing failed',
|
'Job WEB debug response parsing failed.',
|
||||||
err
|
err
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -1053,19 +1149,34 @@ export default class SASjs {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
reject(
|
reject(
|
||||||
new ErrorResponse(
|
new ErrorResponse(
|
||||||
'Job WEB debug response parsing failed',
|
'Job WEB debug response parsing failed.',
|
||||||
{ response: responseText, exception: e }
|
{ response: responseText, exception: e }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.updateUsername(responseText)
|
this.updateUsername(responseText)
|
||||||
|
if (
|
||||||
|
responseText.includes(
|
||||||
|
'The requested URL /SASStoredProcess/do/ was not found on this server.'
|
||||||
|
) ||
|
||||||
|
responseText.includes('Stored process not found')
|
||||||
|
) {
|
||||||
|
reject(
|
||||||
|
new ErrorResponse(
|
||||||
|
'Service not found on the server.',
|
||||||
|
{ service: sasJob },
|
||||||
|
responseText
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsedJson = JSON.parse(responseText)
|
const parsedJson = JSON.parse(responseText)
|
||||||
resolve(parsedJson)
|
resolve(parsedJson)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
reject(
|
reject(
|
||||||
new ErrorResponse('Job WEB response parsing failed', {
|
new ErrorResponse('Job WEB response parsing failed.', {
|
||||||
response: responseText,
|
response: responseText,
|
||||||
exception: e
|
exception: e
|
||||||
})
|
})
|
||||||
@@ -1076,7 +1187,7 @@ export default class SASjs {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((e: Error) => {
|
.catch((e: Error) => {
|
||||||
reject(new ErrorResponse('Job WEB request failed', e))
|
reject(new ErrorResponse('Job WEB request failed.', e))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -1217,10 +1328,20 @@ export default class SASjs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fetchLogFileContent(logLink: string) {
|
/**
|
||||||
|
* Fetches content of the log file
|
||||||
|
* @param logLink - url of the log file.
|
||||||
|
* @param accessToken - an access token for an authorized user.
|
||||||
|
*/
|
||||||
|
public fetchLogFileContent(logLink: string, accessToken?: string) {
|
||||||
|
const headers: any = { 'Content-Type': 'application/json' }
|
||||||
|
|
||||||
|
if (accessToken) headers.Authorization = 'Bearer ' + accessToken
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
fetch(logLink, {
|
fetch(logLink, {
|
||||||
method: 'GET'
|
method: 'GET',
|
||||||
|
headers
|
||||||
})
|
})
|
||||||
.then((response: any) => response.text())
|
.then((response: any) => response.text())
|
||||||
.then((response: any) => resolve(response))
|
.then((response: any) => resolve(response))
|
||||||
@@ -1318,11 +1439,15 @@ export default class SASjs {
|
|||||||
this.sasjsConfig.serverUrl === undefined ||
|
this.sasjsConfig.serverUrl === undefined ||
|
||||||
this.sasjsConfig.serverUrl === ''
|
this.sasjsConfig.serverUrl === ''
|
||||||
) {
|
) {
|
||||||
|
if (typeof location !== 'undefined') {
|
||||||
let url = `${location.protocol}//${location.hostname}`
|
let url = `${location.protocol}//${location.hostname}`
|
||||||
if (location.port) {
|
|
||||||
url = `${url}:${location.port}`
|
if (location.port) url = `${url}:${location.port}`
|
||||||
}
|
|
||||||
this.sasjsConfig.serverUrl = url
|
this.sasjsConfig.serverUrl = url
|
||||||
|
} else {
|
||||||
|
this.sasjsConfig.serverUrl = ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.sasjsConfig.serverUrl.slice(-1) === '/') {
|
if (this.sasjsConfig.serverUrl.slice(-1) === '/') {
|
||||||
@@ -1387,26 +1512,6 @@ export default class SASjs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getLoginForm() {
|
|
||||||
const pattern: RegExp = /<form.+action="(.*Logon[^"]*).*>/
|
|
||||||
const response = await fetch(this.loginUrl).then((r) => r.text())
|
|
||||||
const matches = pattern.exec(response)
|
|
||||||
const formInputs: any = {}
|
|
||||||
if (matches && matches.length) {
|
|
||||||
this.setLoginUrl(matches)
|
|
||||||
const inputs = response.match(/<input.*"hidden"[^>]*>/g)
|
|
||||||
if (inputs) {
|
|
||||||
inputs.forEach((inputStr: string) => {
|
|
||||||
const valueMatch = inputStr.match(/name="([^"]*)"\svalue="([^"]*)/)
|
|
||||||
if (valueMatch && valueMatch.length) {
|
|
||||||
formInputs[valueMatch[1]] = valueMatch[2]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Object.keys(formInputs).length ? formInputs : null
|
|
||||||
}
|
|
||||||
|
|
||||||
private async createFoldersAndServices(
|
private async createFoldersAndServices(
|
||||||
parentFolder: string,
|
parentFolder: string,
|
||||||
membersJson: any[],
|
membersJson: any[],
|
||||||
|
|||||||
+90
-8
@@ -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(() => {
|
)
|
||||||
|
.then(() => {
|
||||||
this.sessions = this.sessions.filter((s) => s.id !== id)
|
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
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import dotenv from 'dotenv'
|
||||||
|
import { SessionManager } from '../SessionManager'
|
||||||
|
import { CsrfToken } from '../types'
|
||||||
|
|
||||||
|
describe('SessionManager', () => {
|
||||||
|
const setCsrfToken = jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementation((csrfToken: CsrfToken) => console.log(csrfToken))
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
dotenv.config()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should instantiate', () => {
|
||||||
|
const sessionManager = new SessionManager(
|
||||||
|
'http://test-server.com',
|
||||||
|
'test context',
|
||||||
|
setCsrfToken
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(sessionManager).toBeInstanceOf(SessionManager)
|
||||||
|
expect(sessionManager.debug).toBeFalsy()
|
||||||
|
expect((sessionManager as any).serverUrl).toEqual('http://test-server.com')
|
||||||
|
expect((sessionManager as any).contextName).toEqual('test context')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should set the debug flag', () => {
|
||||||
|
const sessionManager = new SessionManager(
|
||||||
|
'http://test-server.com',
|
||||||
|
'test context',
|
||||||
|
setCsrfToken
|
||||||
|
)
|
||||||
|
|
||||||
|
sessionManager.debug = true
|
||||||
|
|
||||||
|
expect(sessionManager.debug).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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
|
|
||||||
|
|
||||||
|
if (typeof details !== 'object') {
|
||||||
try {
|
try {
|
||||||
detailsString = JSON.stringify(details)
|
detailsString = JSON.parse(details)
|
||||||
} catch {
|
} catch {
|
||||||
raw = details
|
raw = details
|
||||||
|
detailsString = ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.body = {
|
this.error = {
|
||||||
message,
|
message,
|
||||||
details: detailsString,
|
details: detailsString,
|
||||||
raw
|
raw
|
||||||
|
|||||||
@@ -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,10 +18,12 @@ 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)
|
||||||
|
.then(async (response) => {
|
||||||
if (response.redirected && response.url.includes('SASLogon/login')) {
|
if (response.redirected && response.url.includes('SASLogon/login')) {
|
||||||
return Promise.reject({ status: 401 })
|
return Promise.reject({ status: 401 })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
if (response.status === 403) {
|
if (response.status === 403) {
|
||||||
const tokenHeader = response.headers.get('X-CSRF-HEADER')
|
const tokenHeader = response.headers.get('X-CSRF-HEADER')
|
||||||
@@ -38,12 +40,14 @@ export async function makeRequest<T>(
|
|||||||
headers: { ...request.headers, [tokenHeader]: token }
|
headers: { ...request.headers, [tokenHeader]: token }
|
||||||
}
|
}
|
||||||
|
|
||||||
return fetch(url, retryRequest).then((res) => {
|
return await fetch(url, retryRequest).then((res) => {
|
||||||
etag = res.headers.get('ETag')
|
etag = res.headers.get('ETag')
|
||||||
return responseTransform(res)
|
return responseTransform(res)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
let body: any = await response.text()
|
let body: any = await response.text().catch((err) => {
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
body = JSON.parse(body)
|
body = JSON.parse(body)
|
||||||
@@ -58,7 +62,9 @@ export async function makeRequest<T>(
|
|||||||
return Promise.reject({ status: response.status, body })
|
return Promise.reject({ status: response.status, body })
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let body: any = await response.text()
|
let body: any = await response.text().catch((err) => {
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
|
||||||
if (needsRetry(body)) {
|
if (needsRetry(body)) {
|
||||||
if (retryCount < retryLimit) {
|
if (retryCount < retryLimit) {
|
||||||
@@ -68,7 +74,9 @@ export async function makeRequest<T>(
|
|||||||
retryRequest || request,
|
retryRequest || request,
|
||||||
callback,
|
callback,
|
||||||
contentType
|
contentType
|
||||||
)
|
).catch((err) => {
|
||||||
|
throw err
|
||||||
|
})
|
||||||
retryCount = 0
|
retryCount = 0
|
||||||
|
|
||||||
etag = retryResponse.etag
|
etag = retryResponse.etag
|
||||||
@@ -98,7 +106,11 @@ export async function makeRequest<T>(
|
|||||||
if (response.status === 204) {
|
if (response.status === 204) {
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
const responseTransformed = await responseTransform(response)
|
const responseTransformed = await responseTransform(response).catch(
|
||||||
|
(err) => {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
)
|
||||||
let responseText = ''
|
let responseText = ''
|
||||||
|
|
||||||
if (typeof responseTransformed === 'string') {
|
if (typeof responseTransformed === 'string') {
|
||||||
@@ -115,7 +127,9 @@ export async function makeRequest<T>(
|
|||||||
retryRequest || request,
|
retryRequest || request,
|
||||||
callback,
|
callback,
|
||||||
contentType
|
contentType
|
||||||
)
|
).catch((err) => {
|
||||||
|
throw err
|
||||||
|
})
|
||||||
retryCount = 0
|
retryCount = 0
|
||||||
|
|
||||||
etag = retryResponse.etag
|
etag = retryResponse.etag
|
||||||
@@ -128,9 +142,13 @@ export async function makeRequest<T>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
etag = response.headers.get('ETag')
|
etag = response.headers.get('ETag')
|
||||||
|
|
||||||
return responseTransformed
|
return responseTransformed
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
|
||||||
return { result, etag }
|
return { result, etag }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user