mirror of
https://github.com/sasjs/adapter.git
synced 2026-01-05 03:30:05 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a02c8ad34 | ||
|
|
331d9b0010 | ||
|
|
fa87111f4a | ||
|
|
94967b0f6c | ||
|
|
bd8012fe3e | ||
|
|
fa531b34fd | ||
|
|
354443c98b | ||
|
|
ee30ab195f | ||
|
|
02c1712d22 | ||
|
|
37def7a956 | ||
|
|
653e3d05e0 | ||
|
|
e2ea3f4ddc |
25
package-lock.json
generated
25
package-lock.json
generated
@@ -3704,11 +3704,21 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"encoding": {
|
"encoding": {
|
||||||
"version": "0.1.12",
|
"version": "0.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
|
||||||
"integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=",
|
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"iconv-lite": "~0.4.13"
|
"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": {
|
"end-of-stream": {
|
||||||
@@ -4967,6 +4977,7 @@
|
|||||||
"version": "0.4.24",
|
"version": "0.4.24",
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||||
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"safer-buffer": ">= 2.1.2 < 3"
|
"safer-buffer": ">= 2.1.2 < 3"
|
||||||
}
|
}
|
||||||
@@ -16070,9 +16081,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"whatwg-fetch": {
|
"whatwg-fetch": {
|
||||||
"version": "3.0.0",
|
"version": "3.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.4.1.tgz",
|
||||||
"integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q=="
|
"integrity": "sha512-sofZVzE1wKwO+EYPbWfiwzaKovWiZXf4coEzjGP9b2GBVgQRLQUZ2QcuPpQExGDAW5GItpEm6Tl4OU5mywnAoQ=="
|
||||||
},
|
},
|
||||||
"whatwg-mimetype": {
|
"whatwg-mimetype": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export class SASViyaApiClient {
|
|||||||
|
|
||||||
private csrfToken: CsrfToken | null = null
|
private csrfToken: CsrfToken | null = null
|
||||||
private fileUploadCsrfToken: CsrfToken | null = null
|
private fileUploadCsrfToken: CsrfToken | null = null
|
||||||
|
private _debug = false
|
||||||
private sessionManager = new SessionManager(
|
private sessionManager = new SessionManager(
|
||||||
this.serverUrl,
|
this.serverUrl,
|
||||||
this.contextName,
|
this.contextName,
|
||||||
@@ -45,6 +46,15 @@ export class SASViyaApiClient {
|
|||||||
)
|
)
|
||||||
private folderMap = new Map<string, Job[]>()
|
private folderMap = new Map<string, Job[]>()
|
||||||
|
|
||||||
|
public get debug() {
|
||||||
|
return this._debug
|
||||||
|
}
|
||||||
|
|
||||||
|
public set debug(value: boolean) {
|
||||||
|
this._debug = value
|
||||||
|
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.
|
||||||
*/
|
*/
|
||||||
@@ -135,42 +145,50 @@ export class SASViyaApiClient {
|
|||||||
const promises = contextsList.map((context: any) => {
|
const promises = contextsList.map((context: any) => {
|
||||||
const linesOfCode = ['%put &=sysuserid;']
|
const linesOfCode = ['%put &=sysuserid;']
|
||||||
|
|
||||||
return this.executeScript(
|
return () =>
|
||||||
`test-${context.name}`,
|
this.executeScript(
|
||||||
linesOfCode,
|
`test-${context.name}`,
|
||||||
context.name,
|
linesOfCode,
|
||||||
accessToken,
|
context.name,
|
||||||
false,
|
accessToken,
|
||||||
null,
|
null,
|
||||||
true
|
true
|
||||||
).catch(() => null)
|
).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.body && result.body.details) {
|
||||||
let sysUserId = ''
|
try {
|
||||||
|
const resultParsed = JSON.parse(result.body.details)
|
||||||
|
|
||||||
if (result.log) {
|
if (resultParsed && resultParsed.body) {
|
||||||
const sysUserIdLog = result.log
|
let sysUserId = ''
|
||||||
.split('\n')
|
|
||||||
.find((line: string) => line.startsWith('SYSUSERID='))
|
|
||||||
|
|
||||||
if (sysUserIdLog) {
|
const sysUserIdLog = resultParsed.body
|
||||||
sysUserId = sysUserIdLog.replace('SYSUSERID=', '')
|
.split('\n')
|
||||||
|
.find((line: string) => line.startsWith('SYSUSERID='))
|
||||||
|
|
||||||
|
if (sysUserIdLog) {
|
||||||
|
sysUserId = sysUserIdLog.replace('SYSUSERID=', '')
|
||||||
|
|
||||||
|
executableContexts.push({
|
||||||
|
createdBy: contextsList[index].createdBy,
|
||||||
|
id: contextsList[index].id,
|
||||||
|
name: contextsList[index].name,
|
||||||
|
version: contextsList[index].version,
|
||||||
|
attributes: {
|
||||||
|
sysUserId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
executableContexts.push({
|
|
||||||
createdBy: contextsList[index].createdBy,
|
|
||||||
id: contextsList[index].id,
|
|
||||||
name: contextsList[index].name,
|
|
||||||
version: contextsList[index].version,
|
|
||||||
attributes: {
|
|
||||||
sysUserId
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -318,7 +336,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) {
|
||||||
@@ -404,7 +424,6 @@ export class SASViyaApiClient {
|
|||||||
* @param contextName - the context to execute the code in.
|
* @param contextName - the context to execute the code in.
|
||||||
* @param accessToken - an access token for an authorized user.
|
* @param accessToken - an access token for an authorized user.
|
||||||
* @param sessionId - optional session ID to reuse.
|
* @param sessionId - optional session ID to reuse.
|
||||||
* @param silent - optional flag to disable logging.
|
|
||||||
* @param data - execution data.
|
* @param data - execution data.
|
||||||
* @param debug - when set to true, the log will be returned.
|
* @param debug - when set to true, the log will be returned.
|
||||||
* @param expectWebout - when set to true, the automatic _webout fileref will be checked for content, and that content returned. This fileref is used when the Job contains a SASjs web request (as opposed to executing arbitrary SAS code).
|
* @param expectWebout - when set to true, the automatic _webout fileref will be checked for content, and that content returned. This fileref is used when the Job contains a SASjs web request (as opposed to executing arbitrary SAS code).
|
||||||
@@ -414,12 +433,9 @@ export class SASViyaApiClient {
|
|||||||
linesOfCode: string[],
|
linesOfCode: string[],
|
||||||
contextName: string,
|
contextName: string,
|
||||||
accessToken?: string,
|
accessToken?: string,
|
||||||
silent = false,
|
|
||||||
data = null,
|
data = null,
|
||||||
debug = false,
|
|
||||||
expectWebout = false
|
expectWebout = false
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
silent = !debug
|
|
||||||
try {
|
try {
|
||||||
const headers: any = {
|
const headers: any = {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
@@ -430,7 +446,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 } = {
|
||||||
@@ -442,7 +463,7 @@ export class SASViyaApiClient {
|
|||||||
_OMITTEXTLOG: true
|
_OMITTEXTLOG: true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (debug) {
|
if (this.debug) {
|
||||||
jobArguments['_OMITTEXTLOG'] = false
|
jobArguments['_OMITTEXTLOG'] = false
|
||||||
jobArguments['_OMITSESSIONRESULTS'] = false
|
jobArguments['_OMITSESSIONRESULTS'] = false
|
||||||
jobArguments['_DEBUG'] = 131
|
jobArguments['_DEBUG'] = 131
|
||||||
@@ -469,7 +490,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
|
||||||
|
|
||||||
@@ -500,9 +523,11 @@ export class SASViyaApiClient {
|
|||||||
const { result: postedJob, etag } = await this.request<Job>(
|
const { result: postedJob, etag } = await this.request<Job>(
|
||||||
`${this.serverUrl}/compute/sessions/${executionSessionId}/jobs`,
|
`${this.serverUrl}/compute/sessions/${executionSessionId}/jobs`,
|
||||||
postJobRequest
|
postJobRequest
|
||||||
)
|
).catch((err) => {
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
|
||||||
if (!silent) {
|
if (this.debug) {
|
||||||
console.log(`Job has been submitted for '${fileName}'.`)
|
console.log(`Job has been submitted for '${fileName}'.`)
|
||||||
console.log(
|
console.log(
|
||||||
`You can monitor the job progress at '${this.serverUrl}${
|
`You can monitor the job progress at '${this.serverUrl}${
|
||||||
@@ -511,32 +536,33 @@ export class SASViyaApiClient {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const jobStatus = await this.pollJobState(
|
const jobStatus = await this.pollJobState(postedJob, etag, accessToken)
|
||||||
postedJob,
|
|
||||||
etag,
|
|
||||||
accessToken,
|
|
||||||
silent
|
|
||||||
)
|
|
||||||
|
|
||||||
const { result: currentJob } = await this.request<Job>(
|
const { result: currentJob } = await this.request<Job>(
|
||||||
`${this.serverUrl}/compute/sessions/${executionSessionId}/jobs/${postedJob.id}`,
|
`${this.serverUrl}/compute/sessions/${executionSessionId}/jobs/${postedJob.id}`,
|
||||||
{ headers }
|
{ headers }
|
||||||
)
|
).catch((err) => {
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
|
||||||
let jobResult
|
let jobResult
|
||||||
let log
|
let log
|
||||||
|
|
||||||
const logLink = currentJob.links.find((l) => l.rel === 'log')
|
const logLink = currentJob.links.find((l) => l.rel === 'log')
|
||||||
|
|
||||||
if (debug && logLink) {
|
if (this.debug && logLink) {
|
||||||
log = await this.request<any>(
|
log = await this.request<any>(
|
||||||
`${this.serverUrl}${logLink.href}/content?limit=10000`,
|
`${this.serverUrl}${logLink.href}/content?limit=10000`,
|
||||||
{
|
{
|
||||||
headers
|
headers
|
||||||
}
|
}
|
||||||
).then((res: any) =>
|
|
||||||
res.result.items.map((i: any) => i.line).join('\n')
|
|
||||||
)
|
)
|
||||||
|
.then((res: any) =>
|
||||||
|
res.result.items.map((i: any) => i.line).join('\n')
|
||||||
|
)
|
||||||
|
.catch((err) => {
|
||||||
|
throw err
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (jobStatus === 'failed' || jobStatus === 'error') {
|
if (jobStatus === 'failed' || jobStatus === 'error') {
|
||||||
@@ -562,9 +588,13 @@ export class SASViyaApiClient {
|
|||||||
{
|
{
|
||||||
headers
|
headers
|
||||||
}
|
}
|
||||||
).then((res: any) =>
|
|
||||||
res.result.items.map((i: any) => i.line).join('\n')
|
|
||||||
)
|
)
|
||||||
|
.then((res: any) =>
|
||||||
|
res.result.items.map((i: any) => i.line).join('\n')
|
||||||
|
)
|
||||||
|
.catch((err) => {
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
|
||||||
return Promise.reject(
|
return Promise.reject(
|
||||||
new ErrorResponse('Job execution failed', {
|
new ErrorResponse('Job execution failed', {
|
||||||
@@ -580,7 +610,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,9 +624,7 @@ export class SASViyaApiClient {
|
|||||||
linesOfCode,
|
linesOfCode,
|
||||||
contextName,
|
contextName,
|
||||||
accessToken,
|
accessToken,
|
||||||
silent,
|
data
|
||||||
data,
|
|
||||||
debug
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
throw e
|
throw e
|
||||||
@@ -991,9 +1023,7 @@ export class SASViyaApiClient {
|
|||||||
linesToExecute,
|
linesToExecute,
|
||||||
contextName,
|
contextName,
|
||||||
accessToken,
|
accessToken,
|
||||||
true,
|
|
||||||
data,
|
data,
|
||||||
debug,
|
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1125,12 +1155,7 @@ export class SASViyaApiClient {
|
|||||||
`${this.serverUrl}/jobExecution/jobs?_action=wait`,
|
`${this.serverUrl}/jobExecution/jobs?_action=wait`,
|
||||||
postJobRequest
|
postJobRequest
|
||||||
)
|
)
|
||||||
const jobStatus = await this.pollJobState(
|
const jobStatus = await this.pollJobState(postedJob, etag, accessToken)
|
||||||
postedJob,
|
|
||||||
etag,
|
|
||||||
accessToken,
|
|
||||||
true
|
|
||||||
)
|
|
||||||
const { result: currentJob } = await this.request<Job>(
|
const { result: currentJob } = await this.request<Job>(
|
||||||
`${this.serverUrl}/jobExecution/jobs/${postedJob.id}`,
|
`${this.serverUrl}/jobExecution/jobs/${postedJob.id}`,
|
||||||
{ headers }
|
{ headers }
|
||||||
@@ -1195,8 +1220,7 @@ export class SASViyaApiClient {
|
|||||||
private async pollJobState(
|
private async pollJobState(
|
||||||
postedJob: any,
|
postedJob: any,
|
||||||
etag: string | null,
|
etag: string | null,
|
||||||
accessToken?: string,
|
accessToken?: string
|
||||||
silent = false
|
|
||||||
) {
|
) {
|
||||||
const MAX_POLL_COUNT = 1000
|
const MAX_POLL_COUNT = 1000
|
||||||
const POLL_INTERVAL = 100
|
const POLL_INTERVAL = 100
|
||||||
@@ -1235,7 +1259,7 @@ export class SASViyaApiClient {
|
|||||||
postedJobState === 'pending'
|
postedJobState === 'pending'
|
||||||
) {
|
) {
|
||||||
if (stateLink) {
|
if (stateLink) {
|
||||||
if (!silent) {
|
if (this.debug) {
|
||||||
console.log('Polling job status... \n')
|
console.log('Polling job status... \n')
|
||||||
}
|
}
|
||||||
const { result: jobState } = await this.request<string>(
|
const { result: jobState } = await this.request<string>(
|
||||||
@@ -1247,7 +1271,7 @@ export class SASViyaApiClient {
|
|||||||
)
|
)
|
||||||
|
|
||||||
postedJobState = jobState.trim()
|
postedJobState = jobState.trim()
|
||||||
if (!silent) {
|
if (this.debug) {
|
||||||
console.log(`Current state: ${postedJobState}\n`)
|
console.log(`Current state: ${postedJobState}\n`)
|
||||||
}
|
}
|
||||||
pollCount++
|
pollCount++
|
||||||
@@ -1263,49 +1287,6 @@ export class SASViyaApiClient {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private async waitForSession(
|
|
||||||
session: Session,
|
|
||||||
etag: string | null,
|
|
||||||
accessToken?: string,
|
|
||||||
silent = false
|
|
||||||
) {
|
|
||||||
let sessionState = session.state
|
|
||||||
let pollCount = 0
|
|
||||||
const headers: any = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'If-None-Match': etag
|
|
||||||
}
|
|
||||||
if (accessToken) {
|
|
||||||
headers.Authorization = `Bearer ${accessToken}`
|
|
||||||
}
|
|
||||||
const stateLink = session.links.find((l: any) => l.rel === 'state')
|
|
||||||
return new Promise(async (resolve, _) => {
|
|
||||||
if (sessionState === 'pending') {
|
|
||||||
if (stateLink) {
|
|
||||||
if (!silent) {
|
|
||||||
console.log('Polling session status... \n')
|
|
||||||
}
|
|
||||||
const { result: state } = await this.request<string>(
|
|
||||||
`${this.serverUrl}${stateLink.href}?wait=30`,
|
|
||||||
{
|
|
||||||
headers
|
|
||||||
},
|
|
||||||
'text'
|
|
||||||
)
|
|
||||||
|
|
||||||
sessionState = state.trim()
|
|
||||||
if (!silent) {
|
|
||||||
console.log(`Current state: ${sessionState}\n`)
|
|
||||||
}
|
|
||||||
pollCount++
|
|
||||||
resolve(sessionState)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
resolve(sessionState)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private async uploadTables(data: any, accessToken?: string) {
|
private async uploadTables(data: any, accessToken?: string) {
|
||||||
const uploadedFiles = []
|
const uploadedFiles = []
|
||||||
const headers: any = {
|
const headers: any = {
|
||||||
|
|||||||
14
src/SASjs.ts
14
src/SASjs.ts
@@ -209,9 +209,7 @@ export default class SASjs {
|
|||||||
fileName: string,
|
fileName: string,
|
||||||
linesOfCode: string[],
|
linesOfCode: string[],
|
||||||
contextName: string,
|
contextName: string,
|
||||||
accessToken?: string,
|
accessToken?: string
|
||||||
sessionId = '',
|
|
||||||
silent = false
|
|
||||||
) {
|
) {
|
||||||
this.isMethodSupported('executeScriptSASViya', ServerType.SASViya)
|
this.isMethodSupported('executeScriptSASViya', ServerType.SASViya)
|
||||||
|
|
||||||
@@ -220,9 +218,7 @@ export default class SASjs {
|
|||||||
linesOfCode,
|
linesOfCode,
|
||||||
contextName,
|
contextName,
|
||||||
accessToken,
|
accessToken,
|
||||||
silent,
|
null
|
||||||
null,
|
|
||||||
this.sasjsConfig.debug
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -410,6 +406,9 @@ export default class SASjs {
|
|||||||
*/
|
*/
|
||||||
public setDebugState(value: boolean) {
|
public setDebugState(value: boolean) {
|
||||||
this.sasjsConfig.debug = value
|
this.sasjsConfig.debug = value
|
||||||
|
if (this.sasViyaApiClient) {
|
||||||
|
this.sasViyaApiClient.debug = value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -635,6 +634,7 @@ export default class SASjs {
|
|||||||
this.sasjsConfig.contextName,
|
this.sasjsConfig.contextName,
|
||||||
this.setCsrfTokenApi
|
this.setCsrfTokenApi
|
||||||
)
|
)
|
||||||
|
sasApiClient.debug = this.sasjsConfig.debug
|
||||||
} else if (this.sasjsConfig.serverType === ServerType.SAS9) {
|
} else if (this.sasjsConfig.serverType === ServerType.SAS9) {
|
||||||
sasApiClient = new SAS9ApiClient(serverUrl)
|
sasApiClient = new SAS9ApiClient(serverUrl)
|
||||||
}
|
}
|
||||||
@@ -1352,6 +1352,8 @@ export default class SASjs {
|
|||||||
this.sasjsConfig.contextName,
|
this.sasjsConfig.contextName,
|
||||||
this.setCsrfTokenApi
|
this.setCsrfTokenApi
|
||||||
)
|
)
|
||||||
|
|
||||||
|
this.sasViyaApiClient.debug = this.sasjsConfig.debug
|
||||||
}
|
}
|
||||||
if (this.sasjsConfig.serverType === ServerType.SAS9) {
|
if (this.sasjsConfig.serverType === ServerType.SAS9) {
|
||||||
if (this.sas9ApiClient)
|
if (this.sas9ApiClient)
|
||||||
|
|||||||
@@ -2,6 +2,12 @@ import { Session, Context, CsrfToken } from './types'
|
|||||||
import { asyncForEach, makeRequest, isUrl } from './utils'
|
import { asyncForEach, makeRequest, isUrl } from './utils'
|
||||||
|
|
||||||
const MAX_SESSION_COUNT = 1
|
const MAX_SESSION_COUNT = 1
|
||||||
|
const RETRY_LIMIT: number = 3
|
||||||
|
let RETRY_COUNT: number = 0
|
||||||
|
const INTERNAL_SAS_ERROR = {
|
||||||
|
status: 304,
|
||||||
|
message: 'Not Modified'
|
||||||
|
}
|
||||||
|
|
||||||
export class SessionManager {
|
export class SessionManager {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -15,22 +21,34 @@ export class SessionManager {
|
|||||||
private sessions: Session[] = []
|
private sessions: Session[] = []
|
||||||
private currentContext: Context | null = null
|
private currentContext: Context | null = null
|
||||||
private csrfToken: CsrfToken | null = null
|
private csrfToken: CsrfToken | null = null
|
||||||
|
private _debug: boolean = false
|
||||||
|
|
||||||
|
public get debug() {
|
||||||
|
return this._debug
|
||||||
|
}
|
||||||
|
|
||||||
|
public set debug(value: boolean) {
|
||||||
|
this._debug = value
|
||||||
|
}
|
||||||
|
|
||||||
async getSession(accessToken?: string) {
|
async getSession(accessToken?: string) {
|
||||||
await this.createSessions(accessToken)
|
await this.createSessions(accessToken)
|
||||||
this.createAndWaitForSession(accessToken)
|
await this.createAndWaitForSession(accessToken)
|
||||||
const session = this.sessions.pop()
|
const session = this.sessions.pop()
|
||||||
const secondsSinceSessionCreation =
|
const secondsSinceSessionCreation =
|
||||||
(new Date().getTime() - new Date(session!.creationTimeStamp).getTime()) /
|
(new Date().getTime() - new Date(session!.creationTimeStamp).getTime()) /
|
||||||
1000
|
1000
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!session!.attributes ||
|
!session!.attributes ||
|
||||||
secondsSinceSessionCreation >= session!.attributes.sessionInactiveTimeout
|
secondsSinceSessionCreation >= session!.attributes.sessionInactiveTimeout
|
||||||
) {
|
) {
|
||||||
await this.createSessions(accessToken)
|
await this.createSessions(accessToken)
|
||||||
const freshSession = this.sessions.pop()
|
const freshSession = this.sessions.pop()
|
||||||
|
|
||||||
return freshSession
|
return freshSession
|
||||||
}
|
}
|
||||||
|
|
||||||
return session
|
return session
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,22 +57,37 @@ export class SessionManager {
|
|||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: this.getHeaders(accessToken)
|
headers: this.getHeaders(accessToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.request<Session>(
|
return await this.request<Session>(
|
||||||
`${this.serverUrl}/compute/sessions/${id}`,
|
`${this.serverUrl}/compute/sessions/${id}`,
|
||||||
deleteSessionRequest
|
deleteSessionRequest
|
||||||
).then(() => {
|
)
|
||||||
this.sessions = this.sessions.filter((s) => s.id !== id)
|
.then(() => {
|
||||||
})
|
this.sessions = this.sessions.filter((s) => s.id !== id)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
throw err
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createSessions(accessToken?: string) {
|
private async createSessions(accessToken?: string) {
|
||||||
if (!this.sessions.length) {
|
if (!this.sessions.length) {
|
||||||
if (!this.currentContext) {
|
if (!this.currentContext) {
|
||||||
await this.setCurrentContext(accessToken)
|
await this.setCurrentContext(accessToken).catch((err) => {
|
||||||
|
throw err
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
await asyncForEach(new Array(MAX_SESSION_COUNT), async () => {
|
await asyncForEach(new Array(MAX_SESSION_COUNT), async () => {
|
||||||
const createdSession = await this.createAndWaitForSession(accessToken)
|
const createdSession = await this.createAndWaitForSession(
|
||||||
|
accessToken
|
||||||
|
).catch((err) => {
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
|
||||||
this.sessions.push(createdSession)
|
this.sessions.push(createdSession)
|
||||||
|
}).catch((err) => {
|
||||||
|
throw err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,13 +97,18 @@ export class SessionManager {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: this.getHeaders(accessToken)
|
headers: this.getHeaders(accessToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { result: createdSession, etag } = await this.request<Session>(
|
const { result: createdSession, etag } = await this.request<Session>(
|
||||||
`${this.serverUrl}/compute/contexts/${this.currentContext!.id}/sessions`,
|
`${this.serverUrl}/compute/contexts/${this.currentContext!.id}/sessions`,
|
||||||
createSessionRequest
|
createSessionRequest
|
||||||
)
|
).catch((err) => {
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
|
||||||
await this.waitForSession(createdSession, etag, accessToken)
|
await this.waitForSession(createdSession, etag, accessToken)
|
||||||
|
|
||||||
this.sessions.push(createdSession)
|
this.sessions.push(createdSession)
|
||||||
|
|
||||||
return createdSession
|
return createdSession
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,6 +118,8 @@ export class SessionManager {
|
|||||||
items: Context[]
|
items: Context[]
|
||||||
}>(`${this.serverUrl}/compute/contexts?limit=10000`, {
|
}>(`${this.serverUrl}/compute/contexts?limit=10000`, {
|
||||||
headers: this.getHeaders(accessToken)
|
headers: this.getHeaders(accessToken)
|
||||||
|
}).catch((err) => {
|
||||||
|
throw err
|
||||||
})
|
})
|
||||||
|
|
||||||
const contextsList =
|
const contextsList =
|
||||||
@@ -98,6 +138,8 @@ export class SessionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.currentContext = currentContext
|
this.currentContext = currentContext
|
||||||
|
|
||||||
|
Promise.resolve()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,6 +147,7 @@ export class SessionManager {
|
|||||||
const headers: any = {
|
const headers: any = {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
headers.Authorization = `Bearer ${accessToken}`
|
headers.Authorization = `Bearer ${accessToken}`
|
||||||
}
|
}
|
||||||
@@ -115,8 +158,7 @@ export class SessionManager {
|
|||||||
private async waitForSession(
|
private async waitForSession(
|
||||||
session: Session,
|
session: Session,
|
||||||
etag: string | null,
|
etag: string | null,
|
||||||
accessToken?: string,
|
accessToken?: string
|
||||||
silent = false
|
|
||||||
) {
|
) {
|
||||||
let sessionState = session.state
|
let sessionState = session.state
|
||||||
const headers: any = {
|
const headers: any = {
|
||||||
@@ -124,24 +166,41 @@ export class SessionManager {
|
|||||||
'If-None-Match': etag
|
'If-None-Match': etag
|
||||||
}
|
}
|
||||||
const stateLink = session.links.find((l: any) => l.rel === 'state')
|
const stateLink = session.links.find((l: any) => l.rel === 'state')
|
||||||
|
|
||||||
return new Promise(async (resolve, _) => {
|
return new Promise(async (resolve, _) => {
|
||||||
if (sessionState === 'pending') {
|
if (sessionState === 'pending') {
|
||||||
if (stateLink) {
|
if (stateLink) {
|
||||||
if (!silent) {
|
if (this.debug) {
|
||||||
console.log('Polling session status... \n') // ?
|
console.log('Polling session status... \n') // ?
|
||||||
}
|
}
|
||||||
const { result: state } = await this.request<string>(
|
|
||||||
|
const { result: state } = await this.requestSessionStatus<string>(
|
||||||
`${this.serverUrl}${stateLink.href}?wait=30`,
|
`${this.serverUrl}${stateLink.href}?wait=30`,
|
||||||
{
|
{
|
||||||
headers
|
headers
|
||||||
},
|
},
|
||||||
'text'
|
'text'
|
||||||
)
|
).catch((err) => {
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
|
||||||
sessionState = state.trim()
|
sessionState = state.trim()
|
||||||
if (!silent) {
|
|
||||||
|
if (this.debug) {
|
||||||
console.log(`Current state is '${sessionState}'\n`)
|
console.log(`Current state is '${sessionState}'\n`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// There is an internal error present in SAS Viya 3.5
|
||||||
|
// Retry to wait for a session status in such case of SAS internal error
|
||||||
|
if (
|
||||||
|
sessionState === INTERNAL_SAS_ERROR.message &&
|
||||||
|
RETRY_COUNT < RETRY_LIMIT
|
||||||
|
) {
|
||||||
|
RETRY_COUNT++
|
||||||
|
|
||||||
|
resolve(this.waitForSession(session, etag, accessToken))
|
||||||
|
}
|
||||||
|
|
||||||
resolve(sessionState)
|
resolve(sessionState)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -161,6 +220,7 @@ export class SessionManager {
|
|||||||
[this.csrfToken.headerName]: this.csrfToken.value
|
[this.csrfToken.headerName]: this.csrfToken.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return await makeRequest<T>(
|
return await makeRequest<T>(
|
||||||
url,
|
url,
|
||||||
options,
|
options,
|
||||||
@@ -169,6 +229,36 @@ export class SessionManager {
|
|||||||
this.setCsrfToken(token)
|
this.setCsrfToken(token)
|
||||||
},
|
},
|
||||||
contentType
|
contentType
|
||||||
)
|
).catch((err) => {
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private async requestSessionStatus<T>(
|
||||||
|
url: string,
|
||||||
|
options: RequestInit,
|
||||||
|
contentType: 'text' | 'json' = 'json'
|
||||||
|
) {
|
||||||
|
if (this.csrfToken) {
|
||||||
|
options.headers = {
|
||||||
|
...options.headers,
|
||||||
|
[this.csrfToken.headerName]: this.csrfToken.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await makeRequest<T>(
|
||||||
|
url,
|
||||||
|
options,
|
||||||
|
(token) => {
|
||||||
|
this.csrfToken = token
|
||||||
|
this.setCsrfToken(token)
|
||||||
|
},
|
||||||
|
contentType
|
||||||
|
).catch((err) => {
|
||||||
|
if (err.status === INTERNAL_SAS_ERROR.status)
|
||||||
|
return { result: INTERNAL_SAS_ERROR.message }
|
||||||
|
|
||||||
|
throw err
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { CsrfToken } from '../types'
|
|||||||
import { needsRetry } from './needsRetry'
|
import { needsRetry } from './needsRetry'
|
||||||
|
|
||||||
let retryCount: number = 0
|
let retryCount: number = 0
|
||||||
let retryLimit: number = 5
|
const retryLimit: number = 5
|
||||||
|
|
||||||
export async function makeRequest<T>(
|
export async function makeRequest<T>(
|
||||||
url: string,
|
url: string,
|
||||||
@@ -18,57 +18,118 @@ export async function makeRequest<T>(
|
|||||||
: (res: Response) => res.text()
|
: (res: Response) => res.text()
|
||||||
let etag = null
|
let etag = null
|
||||||
|
|
||||||
const result = await fetch(url, request).then(async (response) => {
|
const result = await fetch(url, request)
|
||||||
if (response.redirected && response.url.includes('SASLogon/login')) {
|
.then(async (response) => {
|
||||||
return Promise.reject({ status: 401 })
|
if (response.redirected && response.url.includes('SASLogon/login')) {
|
||||||
}
|
return Promise.reject({ status: 401 })
|
||||||
if (!response.ok) {
|
}
|
||||||
if (response.status === 403) {
|
|
||||||
const tokenHeader = response.headers.get('X-CSRF-HEADER')
|
|
||||||
|
|
||||||
if (tokenHeader) {
|
if (!response.ok) {
|
||||||
const token = response.headers.get(tokenHeader)
|
if (response.status === 403) {
|
||||||
callback({
|
const tokenHeader = response.headers.get('X-CSRF-HEADER')
|
||||||
headerName: tokenHeader,
|
|
||||||
value: token || ''
|
if (tokenHeader) {
|
||||||
|
const token = response.headers.get(tokenHeader)
|
||||||
|
callback({
|
||||||
|
headerName: tokenHeader,
|
||||||
|
value: token || ''
|
||||||
|
})
|
||||||
|
|
||||||
|
retryRequest = {
|
||||||
|
...request,
|
||||||
|
headers: { ...request.headers, [tokenHeader]: token }
|
||||||
|
}
|
||||||
|
|
||||||
|
return await fetch(url, retryRequest).then((res) => {
|
||||||
|
etag = res.headers.get('ETag')
|
||||||
|
return responseTransform(res)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
let body: any = await response.text().catch((err) => {
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
body = JSON.parse(body)
|
||||||
|
|
||||||
|
body.message = `Forbidden. Check your permissions and user groups, and also the scopes granted when registering your CLIENT_ID. ${
|
||||||
|
body.message || ''
|
||||||
|
}`
|
||||||
|
|
||||||
|
body = JSON.stringify(body)
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
return Promise.reject({ status: response.status, body })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let body: any = await response.text().catch((err) => {
|
||||||
|
throw err
|
||||||
})
|
})
|
||||||
|
|
||||||
retryRequest = {
|
if (needsRetry(body)) {
|
||||||
...request,
|
if (retryCount < retryLimit) {
|
||||||
headers: { ...request.headers, [tokenHeader]: token }
|
retryCount++
|
||||||
|
let retryResponse = await makeRequest(
|
||||||
|
url,
|
||||||
|
retryRequest || request,
|
||||||
|
callback,
|
||||||
|
contentType
|
||||||
|
).catch((err) => {
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
retryCount = 0
|
||||||
|
|
||||||
|
etag = retryResponse.etag
|
||||||
|
return retryResponse.result
|
||||||
|
} else {
|
||||||
|
retryCount = 0
|
||||||
|
|
||||||
|
throw new Error('Request retry limit exceeded')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return fetch(url, retryRequest).then((res) => {
|
if (response.status === 401) {
|
||||||
etag = res.headers.get('ETag')
|
try {
|
||||||
return responseTransform(res)
|
body = JSON.parse(body)
|
||||||
})
|
|
||||||
} else {
|
|
||||||
let body: any = await response.text()
|
|
||||||
|
|
||||||
try {
|
body.message = `Unauthorized request. Check your credentials(client, secret, access token). ${
|
||||||
body = JSON.parse(body)
|
body.message || ''
|
||||||
|
}`
|
||||||
|
|
||||||
body.message = `Forbidden. Check your permissions and user groups. ${
|
body = JSON.stringify(body)
|
||||||
body.message || ''
|
} catch (_) {}
|
||||||
}`
|
}
|
||||||
|
|
||||||
body = JSON.stringify(body)
|
|
||||||
} catch (_) {}
|
|
||||||
|
|
||||||
return Promise.reject({ status: response.status, body })
|
return Promise.reject({ status: response.status, body })
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let body: any = await response.text()
|
if (response.status === 204) {
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
const responseTransformed = await responseTransform(response).catch(
|
||||||
|
(err) => {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
)
|
||||||
|
let responseText = ''
|
||||||
|
|
||||||
if (needsRetry(body)) {
|
if (typeof responseTransformed === 'string') {
|
||||||
|
responseText = responseTransformed
|
||||||
|
} else {
|
||||||
|
responseText = JSON.stringify(responseTransformed)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needsRetry(responseText)) {
|
||||||
if (retryCount < retryLimit) {
|
if (retryCount < retryLimit) {
|
||||||
retryCount++
|
retryCount++
|
||||||
let retryResponse = await makeRequest(
|
const retryResponse = await makeRequest(
|
||||||
url,
|
url,
|
||||||
retryRequest || request,
|
retryRequest || request,
|
||||||
callback,
|
callback,
|
||||||
contentType
|
contentType
|
||||||
)
|
).catch((err) => {
|
||||||
|
throw err
|
||||||
|
})
|
||||||
retryCount = 0
|
retryCount = 0
|
||||||
|
|
||||||
etag = retryResponse.etag
|
etag = retryResponse.etag
|
||||||
@@ -80,57 +141,14 @@ export async function makeRequest<T>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.status === 401) {
|
etag = response.headers.get('ETag')
|
||||||
try {
|
|
||||||
body = JSON.parse(body)
|
|
||||||
|
|
||||||
body.message = `Unauthorized request. Check your credentials(client, secret, access token). ${
|
return responseTransformed
|
||||||
body.message || ''
|
|
||||||
}`
|
|
||||||
|
|
||||||
body = JSON.stringify(body)
|
|
||||||
} catch (_) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.reject({ status: response.status, body })
|
|
||||||
}
|
}
|
||||||
} else {
|
})
|
||||||
if (response.status === 204) {
|
.catch((err) => {
|
||||||
return Promise.resolve()
|
throw err
|
||||||
}
|
})
|
||||||
const responseTransformed = await responseTransform(response)
|
|
||||||
let responseText = ''
|
|
||||||
|
|
||||||
if (typeof responseTransformed === 'string') {
|
|
||||||
responseText = responseTransformed
|
|
||||||
} else {
|
|
||||||
responseText = JSON.stringify(responseTransformed)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (needsRetry(responseText)) {
|
|
||||||
if (retryCount < retryLimit) {
|
|
||||||
retryCount++
|
|
||||||
const retryResponse = await makeRequest(
|
|
||||||
url,
|
|
||||||
retryRequest || request,
|
|
||||||
callback,
|
|
||||||
contentType
|
|
||||||
)
|
|
||||||
retryCount = 0
|
|
||||||
|
|
||||||
etag = retryResponse.etag
|
|
||||||
return retryResponse.result
|
|
||||||
} else {
|
|
||||||
retryCount = 0
|
|
||||||
|
|
||||||
throw new Error('Request retry limit exceeded')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
etag = response.headers.get('ETag')
|
|
||||||
return responseTransformed
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return { result, etag }
|
return { result, etag }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user