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

Compare commits

...

24 Commits

Author SHA1 Message Date
Yury Shkoda
361ec84638 Merge pull request #141 from sasjs/fetch-log-feat
chore(log): made 'fetchLogFileContent' method public
2020-10-30 11:28:07 +03:00
Yury Shkoda
35cc1e4f62 chore(fetchLogFileContent): made accessToken optional 2020-10-30 11:26:43 +03:00
Yury Shkoda
64a976e888 doc: updated docs 2020-10-30 10:36:53 +03:00
Yury Shkoda
7e2cb8491f feat(log): made 'fetchLogFileContent' method public 2020-10-30 10:36:04 +03:00
Krishna Acondy
2cdab7522d Merge pull request #139 from sasjs/location-issue
fix(location): added handle cases when 'location' is not defined
2020-10-29 08:07:11 +00:00
Yury Shkoda
a07eabc408 fix(location): added handle cases when 'location' is not defined 2020-10-29 10:07:30 +03:00
Yury Shkoda
d5920c5885 Merge pull request #134 from sasjs/executeComputeJob
fix(executeComputeJob): added fix for cases when code was not provided
2020-10-21 11:55:43 +03:00
Yury Shkoda
6a3a6b4485 fix(executeComputeJob): added fix for cases when code was not provided 2020-10-21 11:45:21 +03:00
Krishna Acondy
2b1df0c61a Merge pull request #123 from sasjs/sasjs-job
feat(start-compute-job): Add API that returns immediately after job is started
2020-10-16 11:27:02 +01:00
Krishna Acondy
216725f306 chore(doc): update documentation 2020-10-16 11:04:03 +01:00
Krishna Acondy
3183f89a62 chore(*): fix lint warning 2020-10-16 10:58:04 +01:00
Krishna Acondy
f5cc16c3bd chore(create-job): add tests 2020-10-16 10:56:10 +01:00
Krishna Acondy
e78dc76e56 fix(config): set debug to false by default
feat(create-job): add the ability to wait for result
2020-10-16 10:55:56 +01:00
Krishna Acondy
bfdb5ef0a6 chore(*): regenerate documentation 2020-10-16 09:13:48 +01:00
Krishna Acondy
35353d3fce Merge branch 'master' into sasjs-job 2020-10-15 09:11:50 +01:00
Yury Shkoda
7a02c8ad34 Merge pull request #131 from sasjs/issue-124
fix(session): add internal SAS error handler
2020-10-14 14:03:58 +03:00
Yury Shkoda
331d9b0010 fix(session): add internal SAS error handler 2020-10-14 12:53:59 +03:00
Yury Shkoda
ef5686cce7 Merge branch 'master' into sasjs-job 2020-10-12 09:21:00 +03:00
Yury Shkoda
fa87111f4a Merge pull request #126 from sasjs/issue-124
fix(context): fixed 'getExecutableContexts' method
2020-10-07 17:53:31 +03:00
Yury Shkoda
94967b0f6c fix(context): fixed 'getExecutableContexts' method 2020-10-07 17:25:47 +03:00
Krishna Acondy
a07c16fb52 chore(start-compute-job): add test 2020-10-06 09:21:58 +01:00
Krishna Acondy
fd6905ea9f feat(start-compute-job): add API that starts a compute job and immediately returns the session 2020-10-06 09:21:15 +01:00
Krishna Acondy
08f58b5f4f fix(debug): only set session manager debug if it is defined 2020-10-06 08:17:02 +01:00
Krishna Acondy
bd8012fe3e fix(*): revert to older version of isomorphic-fetch 2020-10-03 18:19:06 +01:00
24 changed files with 8753 additions and 159 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -76,7 +76,7 @@
<section class="tsd-index-section ">
<h3>Modules</h3>
<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/utils.html" class="tsd-kind-icon">utils</a></li>
</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

48
package-lock.json generated
View File

@@ -3703,6 +3703,24 @@
"integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==",
"dev": true
},
"encoding": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
"requires": {
"iconv-lite": "^0.6.2"
},
"dependencies": {
"iconv-lite": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz",
"integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
}
}
}
},
"end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
@@ -5249,8 +5267,7 @@
"is-stream": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
"integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=",
"dev": true
"integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
},
"is-text-path": {
"version": "1.0.1",
@@ -5298,12 +5315,23 @@
"dev": true
},
"isomorphic-fetch": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz",
"integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==",
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz",
"integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=",
"requires": {
"node-fetch": "^2.6.1",
"whatwg-fetch": "^3.4.1"
"node-fetch": "^1.0.1",
"whatwg-fetch": ">=0.10.0"
},
"dependencies": {
"node-fetch": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz",
"integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==",
"requires": {
"encoding": "^0.1.11",
"is-stream": "^1.0.1"
}
}
}
},
"isstream": {
@@ -8458,7 +8486,8 @@
"node-fetch": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==",
"dev": true
},
"node-int64": {
"version": "0.4.0",
@@ -13559,8 +13588,7 @@
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"sane": {
"version": "4.1.0",

View File

@@ -59,6 +59,6 @@
"dependencies": {
"es6-promise": "^4.2.8",
"form-data": "^3.0.0",
"isomorphic-fetch": "^3.0.0"
"isomorphic-fetch": "^2.2.1"
}
}

View File

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

View File

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

View File

@@ -52,7 +52,9 @@ export class SASViyaApiClient {
public set debug(value: boolean) {
this._debug = value
this.sessionManager.debug = value
if (this.sessionManager) {
this.sessionManager.debug = value
}
}
/**
@@ -145,41 +147,51 @@ export class SASViyaApiClient {
const promises = contextsList.map((context: any) => {
const linesOfCode = ['%put &=sysuserid;']
return this.executeScript(
`test-${context.name}`,
linesOfCode,
context.name,
accessToken,
null,
true
).catch(() => null)
return () =>
this.executeScript(
`test-${context.name}`,
linesOfCode,
context.name,
accessToken,
null,
true,
true
).catch((err) => err)
})
const results = await Promise.all(promises)
let results: any[] = []
for (const promise of promises) results.push(await promise())
results.forEach((result: any, index: number) => {
if (result) {
let sysUserId = ''
if (result && result.body && result.body.details) {
try {
const resultParsed = JSON.parse(result.body.details)
if (result.log) {
const sysUserIdLog = result.log
.split('\n')
.find((line: string) => line.startsWith('SYSUSERID='))
if (resultParsed && resultParsed.body) {
let sysUserId = ''
if (sysUserIdLog) {
sysUserId = sysUserIdLog.replace('SYSUSERID=', '')
const sysUserIdLog = resultParsed.body
.split('\n')
.find((line: string) => line.startsWith('SYSUSERID='))
if (sysUserIdLog) {
sysUserId = sysUserIdLog.replace('SYSUSERID=', '')
executableContexts.push({
createdBy: contextsList[index].createdBy,
id: contextsList[index].id,
name: contextsList[index].name,
version: contextsList[index].version,
attributes: {
sysUserId
}
})
}
}
} catch (error) {
throw error
}
executableContexts.push({
createdBy: contextsList[index].createdBy,
id: contextsList[index].id,
name: contextsList[index].name,
version: contextsList[index].version,
attributes: {
sysUserId
}
})
}
})
@@ -327,7 +339,9 @@ export class SASViyaApiClient {
originalContext = await this.getComputeContextByName(
contextName,
accessToken
).catch((_) => {})
).catch((err) => {
throw err
})
// Try to find context by id, when context name has been changed.
if (!originalContext) {
@@ -423,7 +437,8 @@ export class SASViyaApiClient {
contextName: string,
accessToken?: string,
data = null,
expectWebout = false
expectWebout = false,
waitForResult = true
): Promise<any> {
try {
const headers: any = {
@@ -435,7 +450,12 @@ export class SASViyaApiClient {
}
let executionSessionId: string
const session = await this.sessionManager.getSession(accessToken)
const session = await this.sessionManager
.getSession(accessToken)
.catch((err) => {
throw err
})
executionSessionId = session!.id
const jobArguments: { [key: string]: any } = {
@@ -474,7 +494,9 @@ export class SASViyaApiClient {
if (data) {
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
@@ -505,7 +527,13 @@ export class SASViyaApiClient {
const { result: postedJob, etag } = await this.request<Job>(
`${this.serverUrl}/compute/sessions/${executionSessionId}/jobs`,
postJobRequest
)
).catch((err) => {
throw err
})
if (!waitForResult) {
return session
}
if (this.debug) {
console.log(`Job has been submitted for '${fileName}'.`)
@@ -521,7 +549,9 @@ export class SASViyaApiClient {
const { result: currentJob } = await this.request<Job>(
`${this.serverUrl}/compute/sessions/${executionSessionId}/jobs/${postedJob.id}`,
{ headers }
)
).catch((err) => {
throw err
})
let jobResult
let log
@@ -534,9 +564,13 @@ export class SASViyaApiClient {
{
headers
}
).then((res: any) =>
res.result.items.map((i: any) => i.line).join('\n')
)
.then((res: any) =>
res.result.items.map((i: any) => i.line).join('\n')
)
.catch((err) => {
throw err
})
}
if (jobStatus === 'failed' || jobStatus === 'error') {
@@ -547,6 +581,8 @@ export class SASViyaApiClient {
if (expectWebout) {
resultLink = `/compute/sessions/${executionSessionId}/filerefs/_webout/content`
} else {
return currentJob
}
if (resultLink) {
@@ -562,9 +598,13 @@ export class SASViyaApiClient {
{
headers
}
).then((res: any) =>
res.result.items.map((i: any) => i.line).join('\n')
)
.then((res: any) =>
res.result.items.map((i: any) => i.line).join('\n')
)
.catch((err) => {
throw err
})
return Promise.reject(
new ErrorResponse('Job execution failed', {
@@ -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 }
} catch (e) {
@@ -590,7 +634,9 @@ export class SASViyaApiClient {
linesOfCode,
contextName,
accessToken,
data
data,
false,
true
)
} else {
throw e
@@ -903,13 +949,16 @@ export class SASViyaApiClient {
* @param debug - sets the _debug flag in the job arguments.
* @param data - any data to be passed in as input to the job.
* @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(
sasJob: string,
contextName: string,
debug: boolean,
data?: any,
accessToken?: string
accessToken?: string,
waitForResult = true,
expectWebout = false
) {
if (isRelativePath(sasJob) && !this.rootFolderName) {
throw new Error(
@@ -983,6 +1032,8 @@ export class SASViyaApiClient {
jobToExecute.code = code
}
if (!code) code = ''
const linesToExecute = code.replace(/\r\n/g, '\n').split('\n')
return await this.executeScript(
sasJob,
@@ -990,7 +1041,8 @@ export class SASViyaApiClient {
contextName,
accessToken,
data,
true
expectWebout,
waitForResult
)
}

View File

@@ -44,7 +44,7 @@ const defaultConfig: SASjsConfig = {
pathSASViya: '/SASJobExecution',
appLoc: '/Public/seedapp',
serverType: ServerType.SASViya,
debug: true,
debug: false,
contextName: 'SAS Job Execution compute context',
useComputeApi: false
}
@@ -670,6 +670,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(
sasJob: string,
data: any,
@@ -689,13 +733,16 @@ export default class SASjs {
sasjsWaitingRequest.requestPromise.promise = new Promise(
async (resolve, reject) => {
const waitForResult = true
const expectWebout = true
this.sasViyaApiClient
?.executeComputeJob(
sasJob,
config.contextName,
config.debug,
data,
accessToken
accessToken,
waitForResult,
expectWebout
)
.then((response) => {
if (!config.debug) {
@@ -1217,10 +1264,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) => {
fetch(logLink, {
method: 'GET'
method: 'GET',
headers
})
.then((response: any) => response.text())
.then((response: any) => resolve(response))
@@ -1318,11 +1375,15 @@ export default class SASjs {
this.sasjsConfig.serverUrl === undefined ||
this.sasjsConfig.serverUrl === ''
) {
let url = `${location.protocol}//${location.hostname}`
if (location.port) {
url = `${url}:${location.port}`
if (typeof location !== 'undefined') {
let url = `${location.protocol}//${location.hostname}`
if (location.port) url = `${url}:${location.port}`
this.sasjsConfig.serverUrl = url
} else {
this.sasjsConfig.serverUrl = ''
}
this.sasjsConfig.serverUrl = url
}
if (this.sasjsConfig.serverUrl.slice(-1) === '/') {

View File

@@ -2,6 +2,12 @@ import { Session, Context, CsrfToken } from './types'
import { asyncForEach, makeRequest, isUrl } from './utils'
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 {
constructor(
@@ -27,19 +33,22 @@ export class SessionManager {
async getSession(accessToken?: string) {
await this.createSessions(accessToken)
this.createAndWaitForSession(accessToken)
await this.createAndWaitForSession(accessToken)
const session = this.sessions.pop()
const secondsSinceSessionCreation =
(new Date().getTime() - new Date(session!.creationTimeStamp).getTime()) /
1000
if (
!session!.attributes ||
secondsSinceSessionCreation >= session!.attributes.sessionInactiveTimeout
) {
await this.createSessions(accessToken)
const freshSession = this.sessions.pop()
return freshSession
}
return session
}
@@ -48,22 +57,37 @@ export class SessionManager {
method: 'DELETE',
headers: this.getHeaders(accessToken)
}
return await this.request<Session>(
`${this.serverUrl}/compute/sessions/${id}`,
deleteSessionRequest
).then(() => {
this.sessions = this.sessions.filter((s) => s.id !== id)
})
)
.then(() => {
this.sessions = this.sessions.filter((s) => s.id !== id)
})
.catch((err) => {
throw err
})
}
private async createSessions(accessToken?: string) {
if (!this.sessions.length) {
if (!this.currentContext) {
await this.setCurrentContext(accessToken)
await this.setCurrentContext(accessToken).catch((err) => {
throw err
})
}
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)
}).catch((err) => {
throw err
})
}
}
@@ -73,13 +97,18 @@ export class SessionManager {
method: 'POST',
headers: this.getHeaders(accessToken)
}
const { result: createdSession, etag } = await this.request<Session>(
`${this.serverUrl}/compute/contexts/${this.currentContext!.id}/sessions`,
createSessionRequest
)
).catch((err) => {
throw err
})
await this.waitForSession(createdSession, etag, accessToken)
this.sessions.push(createdSession)
return createdSession
}
@@ -89,6 +118,8 @@ export class SessionManager {
items: Context[]
}>(`${this.serverUrl}/compute/contexts?limit=10000`, {
headers: this.getHeaders(accessToken)
}).catch((err) => {
throw err
})
const contextsList =
@@ -107,6 +138,8 @@ export class SessionManager {
}
this.currentContext = currentContext
Promise.resolve()
}
}
@@ -114,6 +147,7 @@ export class SessionManager {
const headers: any = {
'Content-Type': 'application/json'
}
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`
}
@@ -132,24 +166,41 @@ export class SessionManager {
'If-None-Match': etag
}
const stateLink = session.links.find((l: any) => l.rel === 'state')
return new Promise(async (resolve, _) => {
if (sessionState === 'pending') {
if (stateLink) {
if (this.debug) {
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`,
{
headers
},
'text'
)
).catch((err) => {
throw err
})
sessionState = state.trim()
if (this.debug) {
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)
}
} else {
@@ -169,6 +220,7 @@ export class SessionManager {
[this.csrfToken.headerName]: this.csrfToken.value
}
}
return await makeRequest<T>(
url,
options,
@@ -177,6 +229,36 @@ export class SessionManager {
this.setCsrfToken(token)
},
contentType
)
).catch((err) => {
throw err
})
}
private async requestSessionStatus<T>(
url: string,
options: RequestInit,
contentType: 'text' | 'json' = 'json'
) {
if (this.csrfToken) {
options.headers = {
...options.headers,
[this.csrfToken.headerName]: this.csrfToken.value
}
}
return await makeRequest<T>(
url,
options,
(token) => {
this.csrfToken = token
this.setCsrfToken(token)
},
contentType
).catch((err) => {
if (err.status === INTERNAL_SAS_ERROR.status)
return { result: INTERNAL_SAS_ERROR.message }
throw err
})
}
}

View File

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