mirror of
https://github.com/sasjs/adapter.git
synced 2026-01-18 01:20:05 +00:00
fix(*): separate job execution code from main SASjs class
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import { Context, EditContextInput, ContextAllAttributes } from './types'
|
import { Context, EditContextInput, ContextAllAttributes } from './types'
|
||||||
import { isUrl } from './utils'
|
import { isUrl } from './utils'
|
||||||
import { prefixMessage } from '@sasjs/utils/error'
|
import { prefixMessage } from '@sasjs/utils/error'
|
||||||
import { RequestClient } from './request/client'
|
import { RequestClient } from './request/RequestClient'
|
||||||
|
|
||||||
export class ContextManager {
|
export class ContextManager {
|
||||||
private defaultComputeContexts = [
|
private defaultComputeContexts = [
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { isUrl } from './utils'
|
import { isUrl } from './utils'
|
||||||
import { UploadFile } from './types/UploadFile'
|
import { UploadFile } from './types/UploadFile'
|
||||||
import { ErrorResponse } from './types'
|
import { ErrorResponse } from './types'
|
||||||
import { RequestClient } from './request/client'
|
import { RequestClient } from './request/RequestClient'
|
||||||
|
|
||||||
export class FileUploader {
|
export class FileUploader {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -52,7 +52,7 @@ export class FileUploader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return this.requestClient
|
return this.requestClient
|
||||||
.post(uploadUrl, formData, undefined, headers)
|
.post(uploadUrl, formData, undefined, 'application/json', headers)
|
||||||
.then((res) => res.result)
|
.then((res) => res.result)
|
||||||
.catch((err: Error) => {
|
.catch((err: Error) => {
|
||||||
return Promise.reject(
|
return Promise.reject(
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { timestampToYYYYMMDDHHMMSS } from '@sasjs/utils/time'
|
|||||||
import { Logger, LogLevel } from '@sasjs/utils/logger'
|
import { Logger, LogLevel } from '@sasjs/utils/logger'
|
||||||
import { isAuthorizeFormRequired } from './auth/isAuthorizeFormRequired'
|
import { isAuthorizeFormRequired } from './auth/isAuthorizeFormRequired'
|
||||||
import { parseAndSubmitAuthorizeForm } from './auth'
|
import { parseAndSubmitAuthorizeForm } from './auth'
|
||||||
import { RequestClient } from './request/client'
|
import { RequestClient } from './request/RequestClient'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A client for interfacing with the SAS Viya REST API.
|
* A client for interfacing with the SAS Viya REST API.
|
||||||
|
|||||||
734
src/SASjs.ts
734
src/SASjs.ts
@@ -1,30 +1,16 @@
|
|||||||
import {
|
import { compareTimestamps, asyncForEach } from './utils'
|
||||||
convertToCSV,
|
import { SASjsConfig, UploadFile, EditContextInput, PollOptions } from './types'
|
||||||
compareTimestamps,
|
|
||||||
splitChunks,
|
|
||||||
parseSourceCode,
|
|
||||||
parseGeneratedCode,
|
|
||||||
parseWeboutResponse,
|
|
||||||
needsRetry,
|
|
||||||
asyncForEach,
|
|
||||||
isRelativePath
|
|
||||||
} from './utils'
|
|
||||||
import {
|
|
||||||
SASjsConfig,
|
|
||||||
SASjsRequest,
|
|
||||||
SASjsWaitingRequest,
|
|
||||||
CsrfToken,
|
|
||||||
UploadFile,
|
|
||||||
EditContextInput,
|
|
||||||
ErrorResponse,
|
|
||||||
PollOptions
|
|
||||||
} from './types'
|
|
||||||
import { SASViyaApiClient } from './SASViyaApiClient'
|
import { SASViyaApiClient } from './SASViyaApiClient'
|
||||||
import { SAS9ApiClient } from './SAS9ApiClient'
|
import { SAS9ApiClient } from './SAS9ApiClient'
|
||||||
import { FileUploader } from './FileUploader'
|
import { FileUploader } from './FileUploader'
|
||||||
import { isLogInRequired, AuthManager } from './auth'
|
import { AuthManager } from './auth'
|
||||||
import { ServerType } from '@sasjs/utils/types'
|
import { ServerType } from '@sasjs/utils/types'
|
||||||
import { RequestClient } from './request/client'
|
import { RequestClient } from './request/RequestClient'
|
||||||
|
import {
|
||||||
|
JobExecutor,
|
||||||
|
WebJobExecutor,
|
||||||
|
ComputeJobExecutor
|
||||||
|
} from './job-execution'
|
||||||
|
|
||||||
const defaultConfig: SASjsConfig = {
|
const defaultConfig: SASjsConfig = {
|
||||||
serverUrl: '',
|
serverUrl: '',
|
||||||
@@ -37,8 +23,6 @@ const defaultConfig: SASjsConfig = {
|
|||||||
useComputeApi: false
|
useComputeApi: false
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestRetryLimit = 5
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SASjs is a JavaScript adapter for SAS.
|
* SASjs is a JavaScript adapter for SAS.
|
||||||
*
|
*
|
||||||
@@ -46,17 +30,14 @@ const requestRetryLimit = 5
|
|||||||
export default class SASjs {
|
export default class SASjs {
|
||||||
private sasjsConfig: SASjsConfig = new SASjsConfig()
|
private sasjsConfig: SASjsConfig = new SASjsConfig()
|
||||||
private jobsPath: string = ''
|
private jobsPath: string = ''
|
||||||
private csrfTokenWeb: CsrfToken | null = null
|
|
||||||
private retryCountWeb: number = 0
|
|
||||||
private retryCountComputeApi: number = 0
|
|
||||||
private retryCountJeseApi: number = 0
|
|
||||||
private sasjsRequests: SASjsRequest[] = []
|
|
||||||
private sasjsWaitingRequests: SASjsWaitingRequest[] = []
|
|
||||||
private sasViyaApiClient: SASViyaApiClient | null = null
|
private sasViyaApiClient: SASViyaApiClient | null = null
|
||||||
private sas9ApiClient: SAS9ApiClient | null = null
|
private sas9ApiClient: SAS9ApiClient | null = null
|
||||||
private fileUploader: FileUploader | null = null
|
private fileUploader: FileUploader | null = null
|
||||||
private authManager: AuthManager | null = null
|
private authManager: AuthManager | null = null
|
||||||
private requestClient: RequestClient | null = null
|
private requestClient: RequestClient | null = null
|
||||||
|
private webJobExecutor: JobExecutor | null = null
|
||||||
|
private computeJobExecutor: JobExecutor | null = null
|
||||||
|
private jesJobExecutor: JobExecutor | null = null
|
||||||
|
|
||||||
constructor(config?: any) {
|
constructor(config?: any) {
|
||||||
this.sasjsConfig = {
|
this.sasjsConfig = {
|
||||||
@@ -260,6 +241,11 @@ export default class SASjs {
|
|||||||
debug?: boolean
|
debug?: boolean
|
||||||
) {
|
) {
|
||||||
this.isMethodSupported('executeScriptSASViya', ServerType.SasViya)
|
this.isMethodSupported('executeScriptSASViya', ServerType.SasViya)
|
||||||
|
if (!contextName) {
|
||||||
|
throw new Error(
|
||||||
|
'Context name is undefined. Please set a `contextName` in your SASjs or override config.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return await this.sasViyaApiClient!.executeScript(
|
return await this.sasViyaApiClient!.executeScript(
|
||||||
fileName,
|
fileName,
|
||||||
@@ -514,8 +500,6 @@ export default class SASjs {
|
|||||||
loginRequiredCallback?: any,
|
loginRequiredCallback?: any,
|
||||||
accessToken?: string
|
accessToken?: string
|
||||||
) {
|
) {
|
||||||
let requestResponse
|
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
...this.sasjsConfig,
|
...this.sasjsConfig,
|
||||||
...config
|
...config
|
||||||
@@ -523,36 +507,30 @@ export default class SASjs {
|
|||||||
|
|
||||||
if (config.serverType === ServerType.SasViya && config.contextName) {
|
if (config.serverType === ServerType.SasViya && config.contextName) {
|
||||||
if (config.useComputeApi) {
|
if (config.useComputeApi) {
|
||||||
requestResponse = await this.executeJobViaComputeApi(
|
return await this.computeJobExecutor!.execute(
|
||||||
sasJob,
|
sasJob,
|
||||||
data,
|
data,
|
||||||
config,
|
config,
|
||||||
loginRequiredCallback,
|
loginRequiredCallback,
|
||||||
accessToken
|
accessToken
|
||||||
)
|
)
|
||||||
|
|
||||||
this.retryCountComputeApi = 0
|
|
||||||
} else {
|
} else {
|
||||||
requestResponse = await this.executeJobViaJesApi(
|
return await this.jesJobExecutor!.execute(
|
||||||
sasJob,
|
sasJob,
|
||||||
data,
|
data,
|
||||||
config,
|
config,
|
||||||
loginRequiredCallback,
|
loginRequiredCallback,
|
||||||
accessToken
|
accessToken
|
||||||
)
|
)
|
||||||
|
|
||||||
this.retryCountJeseApi = 0
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
requestResponse = await this.executeJobViaWeb(
|
return await this.webJobExecutor!.execute(
|
||||||
sasJob,
|
sasJob,
|
||||||
data,
|
data,
|
||||||
config,
|
config,
|
||||||
loginRequiredCallback
|
loginRequiredCallback
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return requestResponse
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -674,576 +652,10 @@ export default class SASjs {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private async executeJobViaComputeApi(
|
|
||||||
sasJob: string,
|
|
||||||
data: any,
|
|
||||||
config: any,
|
|
||||||
loginRequiredCallback?: any,
|
|
||||||
accessToken?: string
|
|
||||||
) {
|
|
||||||
const sasjsWaitingRequest: SASjsWaitingRequest = {
|
|
||||||
requestPromise: {
|
|
||||||
promise: null,
|
|
||||||
resolve: null,
|
|
||||||
reject: null
|
|
||||||
},
|
|
||||||
SASjob: sasJob,
|
|
||||||
data
|
|
||||||
}
|
|
||||||
|
|
||||||
sasjsWaitingRequest.requestPromise.promise = new Promise(
|
|
||||||
async (resolve, reject) => {
|
|
||||||
const waitForResult = true
|
|
||||||
const expectWebout = true
|
|
||||||
this.sasViyaApiClient
|
|
||||||
?.executeComputeJob(
|
|
||||||
sasJob,
|
|
||||||
config.contextName,
|
|
||||||
config.debug,
|
|
||||||
data,
|
|
||||||
accessToken,
|
|
||||||
waitForResult,
|
|
||||||
expectWebout
|
|
||||||
)
|
|
||||||
.then((response) => {
|
|
||||||
if (!config.debug) {
|
|
||||||
this.appendSasjsRequest(null, sasJob, null)
|
|
||||||
} else {
|
|
||||||
this.appendSasjsRequest(response, sasJob, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
let responseJson
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (typeof response!.result === 'string') {
|
|
||||||
responseJson = JSON.parse(response!.result)
|
|
||||||
} else {
|
|
||||||
responseJson = response!.result
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
responseJson = JSON.parse(parseWeboutResponse(response!.result))
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(responseJson)
|
|
||||||
})
|
|
||||||
.catch(async (response) => {
|
|
||||||
let error = response.error || response
|
|
||||||
|
|
||||||
if (needsRetry(JSON.stringify(error))) {
|
|
||||||
if (this.retryCountComputeApi < requestRetryLimit) {
|
|
||||||
let retryResponse = await this.executeJobViaComputeApi(
|
|
||||||
sasJob,
|
|
||||||
data,
|
|
||||||
config,
|
|
||||||
loginRequiredCallback,
|
|
||||||
accessToken
|
|
||||||
)
|
|
||||||
|
|
||||||
this.retryCountComputeApi++
|
|
||||||
|
|
||||||
resolve(retryResponse)
|
|
||||||
} else {
|
|
||||||
this.retryCountComputeApi = 0
|
|
||||||
reject(
|
|
||||||
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 (loginRequiredCallback) loginRequiredCallback(true)
|
|
||||||
sasjsWaitingRequest.requestPromise.resolve = resolve
|
|
||||||
sasjsWaitingRequest.requestPromise.reject = reject
|
|
||||||
sasjsWaitingRequest.config = config
|
|
||||||
this.sasjsWaitingRequests.push(sasjsWaitingRequest)
|
|
||||||
} else {
|
|
||||||
reject(new ErrorResponse('Job execution failed.', error))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return sasjsWaitingRequest.requestPromise.promise
|
|
||||||
}
|
|
||||||
|
|
||||||
private async executeJobViaJesApi(
|
|
||||||
sasJob: string,
|
|
||||||
data: any,
|
|
||||||
config: any,
|
|
||||||
loginRequiredCallback?: any,
|
|
||||||
accessToken?: string
|
|
||||||
) {
|
|
||||||
const sasjsWaitingRequest: SASjsWaitingRequest = {
|
|
||||||
requestPromise: {
|
|
||||||
promise: null,
|
|
||||||
resolve: null,
|
|
||||||
reject: null
|
|
||||||
},
|
|
||||||
SASjob: sasJob,
|
|
||||||
data
|
|
||||||
}
|
|
||||||
|
|
||||||
sasjsWaitingRequest.requestPromise.promise = new Promise(
|
|
||||||
async (resolve, reject) => {
|
|
||||||
const session = await this.checkSession()
|
|
||||||
|
|
||||||
if (!session.isLoggedIn && !accessToken) {
|
|
||||||
if (loginRequiredCallback) loginRequiredCallback(true)
|
|
||||||
sasjsWaitingRequest.requestPromise.resolve = resolve
|
|
||||||
sasjsWaitingRequest.requestPromise.reject = reject
|
|
||||||
sasjsWaitingRequest.config = config
|
|
||||||
this.sasjsWaitingRequests.push(sasjsWaitingRequest)
|
|
||||||
} else {
|
|
||||||
resolve(
|
|
||||||
await this.sasViyaApiClient
|
|
||||||
?.executeJob(
|
|
||||||
sasJob,
|
|
||||||
config.contextName,
|
|
||||||
config.debug,
|
|
||||||
data,
|
|
||||||
accessToken
|
|
||||||
)
|
|
||||||
.then((response) => {
|
|
||||||
if (!config.debug) {
|
|
||||||
this.appendSasjsRequest(null, sasJob, null)
|
|
||||||
} else {
|
|
||||||
this.appendSasjsRequest(response, sasJob, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
let responseJson
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (typeof response!.result === 'string') {
|
|
||||||
responseJson = JSON.parse(response!.result)
|
|
||||||
} else {
|
|
||||||
responseJson = response!.result
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
responseJson = JSON.parse(
|
|
||||||
parseWeboutResponse(response!.result)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return responseJson
|
|
||||||
})
|
|
||||||
.catch(async (response) => {
|
|
||||||
if (needsRetry(JSON.stringify(response))) {
|
|
||||||
if (this.retryCountJeseApi < requestRetryLimit) {
|
|
||||||
let retryResponse = await this.executeJobViaJesApi(
|
|
||||||
sasJob,
|
|
||||||
data,
|
|
||||||
config,
|
|
||||||
loginRequiredCallback,
|
|
||||||
accessToken
|
|
||||||
)
|
|
||||||
|
|
||||||
this.retryCountJeseApi++
|
|
||||||
|
|
||||||
resolve(retryResponse)
|
|
||||||
} else {
|
|
||||||
this.retryCountJeseApi = 0
|
|
||||||
reject(
|
|
||||||
new ErrorResponse('Jes API retry requests limit reached.')
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response?.log) {
|
|
||||||
this.appendSasjsRequest(response.log, sasJob, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.toString().includes('Job was not found')) {
|
|
||||||
reject(
|
|
||||||
new ErrorResponse('Service not found on the server.', {
|
|
||||||
sasJob: sasJob
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
reject(new ErrorResponse('Job execution failed.', response))
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return sasjsWaitingRequest.requestPromise.promise
|
|
||||||
}
|
|
||||||
|
|
||||||
private async executeJobViaWeb(
|
|
||||||
sasJob: string,
|
|
||||||
data: any,
|
|
||||||
config: any,
|
|
||||||
loginRequiredCallback?: any
|
|
||||||
) {
|
|
||||||
const sasjsWaitingRequest: SASjsWaitingRequest = {
|
|
||||||
requestPromise: {
|
|
||||||
promise: null,
|
|
||||||
resolve: null,
|
|
||||||
reject: null
|
|
||||||
},
|
|
||||||
SASjob: sasJob,
|
|
||||||
data
|
|
||||||
}
|
|
||||||
const program = isRelativePath(sasJob)
|
|
||||||
? config.appLoc
|
|
||||||
? config.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
|
|
||||||
: sasJob
|
|
||||||
: sasJob
|
|
||||||
const jobUri =
|
|
||||||
config.serverType === ServerType.SasViya
|
|
||||||
? await this.getJobUri(sasJob)
|
|
||||||
: ''
|
|
||||||
const apiUrl = `${config.serverUrl}${this.jobsPath}/?${
|
|
||||||
jobUri.length > 0
|
|
||||||
? '__program=' + program + '&_job=' + jobUri
|
|
||||||
: '_program=' + program
|
|
||||||
}`
|
|
||||||
|
|
||||||
const requestParams = {
|
|
||||||
...this.getRequestParamsWeb(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
const formData = new FormData()
|
|
||||||
|
|
||||||
let isError = false
|
|
||||||
let errorMsg = ''
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
const stringifiedData = JSON.stringify(data)
|
|
||||||
if (
|
|
||||||
config.serverType === ServerType.Sas9 ||
|
|
||||||
stringifiedData.length > 500000 ||
|
|
||||||
stringifiedData.includes(';')
|
|
||||||
) {
|
|
||||||
// file upload approach
|
|
||||||
for (const tableName in data) {
|
|
||||||
if (isError) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const name = tableName
|
|
||||||
const csv = convertToCSV(data[tableName])
|
|
||||||
if (csv === 'ERROR: LARGE STRING LENGTH') {
|
|
||||||
isError = true
|
|
||||||
errorMsg =
|
|
||||||
'The max length of a string value in SASjs is 32765 characters.'
|
|
||||||
}
|
|
||||||
|
|
||||||
const file = new Blob([csv], {
|
|
||||||
type: 'application/csv'
|
|
||||||
})
|
|
||||||
|
|
||||||
formData.append(name, file, `${name}.csv`)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// param based approach
|
|
||||||
const sasjsTables = []
|
|
||||||
let tableCounter = 0
|
|
||||||
for (const tableName in data) {
|
|
||||||
if (isError) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tableCounter++
|
|
||||||
sasjsTables.push(tableName)
|
|
||||||
const csv = convertToCSV(data[tableName])
|
|
||||||
if (csv === 'ERROR: LARGE STRING LENGTH') {
|
|
||||||
isError = true
|
|
||||||
errorMsg =
|
|
||||||
'The max length of a string value in SASjs is 32765 characters.'
|
|
||||||
}
|
|
||||||
// if csv has length more then 16k, send in chunks
|
|
||||||
if (csv.length > 16000) {
|
|
||||||
const csvChunks = splitChunks(csv)
|
|
||||||
// append chunks to form data with same key
|
|
||||||
csvChunks.map((chunk) => {
|
|
||||||
formData.append(`sasjs${tableCounter}data`, chunk)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
requestParams[`sasjs${tableCounter}data`] = csv
|
|
||||||
}
|
|
||||||
}
|
|
||||||
requestParams['sasjs_tables'] = sasjsTables.join(' ')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const key in requestParams) {
|
|
||||||
if (requestParams.hasOwnProperty(key)) {
|
|
||||||
formData.append(key, requestParams[key])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let isRedirected = false
|
|
||||||
|
|
||||||
sasjsWaitingRequest.requestPromise.promise = new Promise(
|
|
||||||
(resolve, reject) => {
|
|
||||||
if (isError) {
|
|
||||||
reject(new ErrorResponse(errorMsg))
|
|
||||||
}
|
|
||||||
const headers: any = {}
|
|
||||||
if (this.csrfTokenWeb) {
|
|
||||||
headers[this.csrfTokenWeb.headerName] = this.csrfTokenWeb.value
|
|
||||||
}
|
|
||||||
fetch(apiUrl, {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData,
|
|
||||||
referrerPolicy: 'same-origin',
|
|
||||||
headers
|
|
||||||
})
|
|
||||||
.then(async (response) => {
|
|
||||||
if (!response.ok) {
|
|
||||||
if (response.status === 403) {
|
|
||||||
const tokenHeader = response.headers.get('X-CSRF-HEADER')
|
|
||||||
|
|
||||||
if (tokenHeader) {
|
|
||||||
const token = response.headers.get(tokenHeader)
|
|
||||||
this.csrfTokenWeb = {
|
|
||||||
headerName: tokenHeader,
|
|
||||||
value: token || ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.redirected && config.serverType === ServerType.Sas9) {
|
|
||||||
isRedirected = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.text()
|
|
||||||
})
|
|
||||||
.then((responseText) => {
|
|
||||||
if (
|
|
||||||
(needsRetry(responseText) || isRedirected) &&
|
|
||||||
!isLogInRequired(responseText)
|
|
||||||
) {
|
|
||||||
if (this.retryCountWeb < requestRetryLimit) {
|
|
||||||
this.retryCountWeb++
|
|
||||||
this.request(sasJob, data, config, loginRequiredCallback).then(
|
|
||||||
(res: any) => resolve(res),
|
|
||||||
(err: any) => reject(err)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
this.retryCountWeb = 0
|
|
||||||
reject(responseText)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.retryCountWeb = 0
|
|
||||||
this.parseLogFromResponse(responseText, program)
|
|
||||||
|
|
||||||
if (isLogInRequired(responseText)) {
|
|
||||||
if (loginRequiredCallback) loginRequiredCallback(true)
|
|
||||||
sasjsWaitingRequest.requestPromise.resolve = resolve
|
|
||||||
sasjsWaitingRequest.requestPromise.reject = reject
|
|
||||||
sasjsWaitingRequest.config = config
|
|
||||||
this.sasjsWaitingRequests.push(sasjsWaitingRequest)
|
|
||||||
} else {
|
|
||||||
if (config.serverType === ServerType.Sas9 && config.debug) {
|
|
||||||
const jsonResponseText = parseWeboutResponse(responseText)
|
|
||||||
|
|
||||||
if (jsonResponseText !== '') {
|
|
||||||
resolve(JSON.parse(jsonResponseText))
|
|
||||||
} else {
|
|
||||||
reject(
|
|
||||||
new ErrorResponse(
|
|
||||||
'Job WEB execution failed.',
|
|
||||||
this.parseSAS9ErrorResponse(responseText)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else if (
|
|
||||||
config.serverType === ServerType.SasViya &&
|
|
||||||
config.debug
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
this.parseSASVIYADebugResponse(responseText).then(
|
|
||||||
(resText: any) => {
|
|
||||||
try {
|
|
||||||
resolve(JSON.parse(resText))
|
|
||||||
} catch (e) {
|
|
||||||
reject(
|
|
||||||
new ErrorResponse(
|
|
||||||
'Job WEB debug response parsing failed.',
|
|
||||||
{ response: resText, exception: e }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(err: any) => {
|
|
||||||
reject(
|
|
||||||
new ErrorResponse(
|
|
||||||
'Job WEB debug response parsing failed.',
|
|
||||||
err
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} catch (e) {
|
|
||||||
reject(
|
|
||||||
new ErrorResponse(
|
|
||||||
'Job WEB debug response parsing failed.',
|
|
||||||
{ response: responseText, exception: e }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
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 {
|
|
||||||
const parsedJson = JSON.parse(responseText)
|
|
||||||
resolve(parsedJson)
|
|
||||||
} catch (e) {
|
|
||||||
reject(
|
|
||||||
new ErrorResponse('Job WEB response parsing failed.', {
|
|
||||||
response: responseText,
|
|
||||||
exception: e
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((e: Error) => {
|
|
||||||
reject(new ErrorResponse('Job WEB request failed.', e))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return sasjsWaitingRequest.requestPromise.promise
|
|
||||||
}
|
|
||||||
|
|
||||||
private resendWaitingRequests = async () => {
|
private resendWaitingRequests = async () => {
|
||||||
for (const sasjsWaitingRequest of this.sasjsWaitingRequests) {
|
await this.webJobExecutor?.resendWaitingRequests()
|
||||||
this.request(sasjsWaitingRequest.SASjob, sasjsWaitingRequest.data).then(
|
await this.computeJobExecutor?.resendWaitingRequests()
|
||||||
(res: any) => {
|
await this.jesJobExecutor?.resendWaitingRequests()
|
||||||
sasjsWaitingRequest.requestPromise.resolve(res)
|
|
||||||
},
|
|
||||||
(err: any) => {
|
|
||||||
sasjsWaitingRequest.requestPromise.reject(err)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.sasjsWaitingRequests = []
|
|
||||||
}
|
|
||||||
|
|
||||||
private getRequestParamsWeb(config: any): any {
|
|
||||||
const requestParams: any = {}
|
|
||||||
|
|
||||||
if (this.csrfTokenWeb) {
|
|
||||||
requestParams['_csrf'] = this.csrfTokenWeb.value
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.debug) {
|
|
||||||
requestParams['_omittextlog'] = 'false'
|
|
||||||
requestParams['_omitsessionresults'] = 'false'
|
|
||||||
|
|
||||||
requestParams['_debug'] = 131
|
|
||||||
}
|
|
||||||
|
|
||||||
return requestParams
|
|
||||||
}
|
|
||||||
|
|
||||||
private parseSASVIYADebugResponse(response: string) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const iframeStart = response.split(
|
|
||||||
'<iframe style="width: 99%; height: 500px" src="'
|
|
||||||
)[1]
|
|
||||||
const jsonUrl = iframeStart ? iframeStart.split('"></iframe>')[0] : null
|
|
||||||
|
|
||||||
if (jsonUrl) {
|
|
||||||
fetch(this.sasjsConfig.serverUrl + jsonUrl)
|
|
||||||
.then((res) => res.text())
|
|
||||||
.then((resText) => {
|
|
||||||
resolve(resText)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
reject('No debug info found in response.')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getJobUri(sasJob: string) {
|
|
||||||
if (!this.sasViyaApiClient) return ''
|
|
||||||
let uri = ''
|
|
||||||
|
|
||||||
let folderPath
|
|
||||||
let jobName: string
|
|
||||||
if (isRelativePath(sasJob)) {
|
|
||||||
folderPath = sasJob.split('/')[0]
|
|
||||||
jobName = sasJob.split('/')[1]
|
|
||||||
} else {
|
|
||||||
const folderPathParts = sasJob.split('/')
|
|
||||||
jobName = folderPathParts.pop() || ''
|
|
||||||
folderPath = folderPathParts.join('/')
|
|
||||||
}
|
|
||||||
|
|
||||||
const locJobs = await this.sasViyaApiClient.getJobsInFolder(folderPath)
|
|
||||||
if (locJobs) {
|
|
||||||
const job = locJobs.find(
|
|
||||||
(el: any) => el.name === jobName && el.contentType === 'jobDefinition'
|
|
||||||
)
|
|
||||||
if (job) {
|
|
||||||
uri = job.uri
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return uri
|
|
||||||
}
|
|
||||||
|
|
||||||
private parseSAS9ErrorResponse(response: string) {
|
|
||||||
const logLines = response.split('\n')
|
|
||||||
const parsedLines: string[] = []
|
|
||||||
let firstErrorLineIndex: number = -1
|
|
||||||
|
|
||||||
logLines.map((line: string, index: number) => {
|
|
||||||
if (
|
|
||||||
line.toLowerCase().includes('error') &&
|
|
||||||
!line.toLowerCase().includes('this request completed with errors.') &&
|
|
||||||
firstErrorLineIndex === -1
|
|
||||||
) {
|
|
||||||
firstErrorLineIndex = index
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
for (let i = firstErrorLineIndex - 10; i <= firstErrorLineIndex + 10; i++) {
|
|
||||||
parsedLines.push(logLines[i])
|
|
||||||
}
|
|
||||||
|
|
||||||
return parsedLines.join(', ')
|
|
||||||
}
|
|
||||||
|
|
||||||
private parseLogFromResponse(response: any, program: string) {
|
|
||||||
if (this.sasjsConfig.serverType === ServerType.Sas9) {
|
|
||||||
this.appendSasjsRequest(response, program, null)
|
|
||||||
} else {
|
|
||||||
if (!this.sasjsConfig.debug) {
|
|
||||||
this.appendSasjsRequest(null, program, null)
|
|
||||||
} else {
|
|
||||||
this.appendSasjsRequest(response, program, null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1267,89 +679,20 @@ export default class SASjs {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private async appendSasjsRequest(
|
|
||||||
response: any,
|
|
||||||
program: string,
|
|
||||||
pgmData: any
|
|
||||||
) {
|
|
||||||
let sourceCode = ''
|
|
||||||
let generatedCode = ''
|
|
||||||
let sasWork = null
|
|
||||||
|
|
||||||
if (response && response.result && response.log) {
|
|
||||||
sourceCode = parseSourceCode(response.log)
|
|
||||||
generatedCode = parseGeneratedCode(response.log)
|
|
||||||
|
|
||||||
if (this.sasjsConfig.debug) {
|
|
||||||
if (response.log) {
|
|
||||||
sasWork = response.log
|
|
||||||
} else {
|
|
||||||
sasWork = JSON.parse(parseWeboutResponse(response.result)).WORK
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
sasWork = JSON.parse(response.result).WORK
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (response) {
|
|
||||||
sourceCode = parseSourceCode(response)
|
|
||||||
generatedCode = parseGeneratedCode(response)
|
|
||||||
sasWork = await this.parseSasWork(response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.sasjsRequests.push({
|
|
||||||
logFile: (response && response.log) || response,
|
|
||||||
serviceLink: program,
|
|
||||||
timestamp: new Date(),
|
|
||||||
sourceCode,
|
|
||||||
generatedCode,
|
|
||||||
SASWORK: sasWork
|
|
||||||
})
|
|
||||||
|
|
||||||
if (this.sasjsRequests.length > 20) {
|
|
||||||
this.sasjsRequests.splice(0, 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async parseSasWork(response: any) {
|
|
||||||
if (this.sasjsConfig.debug) {
|
|
||||||
let jsonResponse
|
|
||||||
|
|
||||||
if (this.sasjsConfig.serverType === ServerType.Sas9) {
|
|
||||||
try {
|
|
||||||
jsonResponse = JSON.parse(parseWeboutResponse(response))
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await this.parseSASVIYADebugResponse(response).then(
|
|
||||||
(resText: any) => {
|
|
||||||
try {
|
|
||||||
jsonResponse = JSON.parse(resText)
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(err: any) => {
|
|
||||||
console.error(err)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (jsonResponse) {
|
|
||||||
return jsonResponse.WORK
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
public getSasRequests() {
|
public getSasRequests() {
|
||||||
const sortedRequests = this.sasjsRequests.sort(compareTimestamps)
|
const requests = [
|
||||||
|
...this.webJobExecutor!.getRequests(),
|
||||||
|
...this.computeJobExecutor!.getRequests(),
|
||||||
|
...this.jesJobExecutor!.getRequests()
|
||||||
|
]
|
||||||
|
const sortedRequests = requests.sort(compareTimestamps)
|
||||||
return sortedRequests
|
return sortedRequests
|
||||||
}
|
}
|
||||||
|
|
||||||
public clearSasRequests() {
|
public clearSasRequests() {
|
||||||
this.sasjsRequests = []
|
this.webJobExecutor!.clearRequests()
|
||||||
|
this.computeJobExecutor!.clearRequests()
|
||||||
|
this.jesJobExecutor!.clearRequests()
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupConfiguration() {
|
private setupConfiguration() {
|
||||||
@@ -1413,6 +756,19 @@ export default class SASjs {
|
|||||||
this.jobsPath,
|
this.jobsPath,
|
||||||
this.requestClient
|
this.requestClient
|
||||||
)
|
)
|
||||||
|
|
||||||
|
this.webJobExecutor = new WebJobExecutor(
|
||||||
|
this.sasjsConfig.serverUrl,
|
||||||
|
this.sasjsConfig.serverType!,
|
||||||
|
this.jobsPath,
|
||||||
|
this.requestClient,
|
||||||
|
this.sasViyaApiClient!
|
||||||
|
)
|
||||||
|
|
||||||
|
this.computeJobExecutor = new ComputeJobExecutor(
|
||||||
|
this.sasjsConfig.serverUrl,
|
||||||
|
this.sasViyaApiClient!
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createFoldersAndServices(
|
private async createFoldersAndServices(
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Session, Context, CsrfToken, SessionVariable } from './types'
|
import { Session, Context, CsrfToken, SessionVariable } from './types'
|
||||||
import { asyncForEach, isUrl } from './utils'
|
import { asyncForEach, isUrl } from './utils'
|
||||||
import { prefixMessage } from '@sasjs/utils/error'
|
import { prefixMessage } from '@sasjs/utils/error'
|
||||||
import { RequestClient } from './request/client'
|
import { RequestClient } from './request/RequestClient'
|
||||||
|
|
||||||
const MAX_SESSION_COUNT = 1
|
const MAX_SESSION_COUNT = 1
|
||||||
const RETRY_LIMIT: number = 3
|
const RETRY_LIMIT: number = 3
|
||||||
|
|||||||
24
src/file/generateFileUploadForm.ts
Normal file
24
src/file/generateFileUploadForm.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { convertToCSV } from '../utils/convertToCsv'
|
||||||
|
|
||||||
|
export const generateFileUploadForm = (
|
||||||
|
formData: FormData,
|
||||||
|
data: any
|
||||||
|
): FormData => {
|
||||||
|
for (const tableName in data) {
|
||||||
|
const name = tableName
|
||||||
|
const csv = convertToCSV(data[tableName])
|
||||||
|
if (csv === 'ERROR: LARGE STRING LENGTH') {
|
||||||
|
throw new Error(
|
||||||
|
'The max length of a string value in SASjs is 32765 characters.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = new Blob([csv], {
|
||||||
|
type: 'application/csv'
|
||||||
|
})
|
||||||
|
|
||||||
|
formData.append(name, file, `${name}.csv`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return formData
|
||||||
|
}
|
||||||
31
src/file/generateTableUploadForm.ts
Normal file
31
src/file/generateTableUploadForm.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { convertToCSV } from '../utils/convertToCsv'
|
||||||
|
import { splitChunks } from '../utils/splitChunks'
|
||||||
|
|
||||||
|
export const generateTableUploadForm = (formData: FormData, data: any) => {
|
||||||
|
const sasjsTables = []
|
||||||
|
const requestParams: any = {}
|
||||||
|
let tableCounter = 0
|
||||||
|
for (const tableName in data) {
|
||||||
|
tableCounter++
|
||||||
|
sasjsTables.push(tableName)
|
||||||
|
const csv = convertToCSV(data[tableName])
|
||||||
|
if (csv === 'ERROR: LARGE STRING LENGTH') {
|
||||||
|
throw new Error(
|
||||||
|
'The max length of a string value in SASjs is 32765 characters.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// if csv has length more then 16k, send in chunks
|
||||||
|
if (csv.length > 16000) {
|
||||||
|
const csvChunks = splitChunks(csv)
|
||||||
|
// append chunks to form data with same key
|
||||||
|
csvChunks.map((chunk) => {
|
||||||
|
formData.append(`sasjs${tableCounter}data`, chunk)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
requestParams[`sasjs${tableCounter}data`] = csv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
requestParams['sasjs_tables'] = sasjsTables.join(' ')
|
||||||
|
|
||||||
|
return { formData, requestParams }
|
||||||
|
}
|
||||||
132
src/job-execution/ComputeJobExecutor.ts
Normal file
132
src/job-execution/ComputeJobExecutor.ts
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import { ServerType } from '@sasjs/utils/types'
|
||||||
|
import { ErrorResponse } from '..'
|
||||||
|
import { SASViyaApiClient } from '../SASViyaApiClient'
|
||||||
|
import { JobExecutionError, LoginRequiredError, SASjsRequest } from '../types'
|
||||||
|
import {
|
||||||
|
asyncForEach,
|
||||||
|
parseGeneratedCode,
|
||||||
|
parseSourceCode,
|
||||||
|
parseWeboutResponse
|
||||||
|
} from '../utils'
|
||||||
|
import { ExecuteFunction, JobExecutor } from './JobExecutor'
|
||||||
|
import { parseSasWork } from './parseSasWork'
|
||||||
|
|
||||||
|
export class ComputeJobExecutor implements JobExecutor {
|
||||||
|
waitingRequests: ExecuteFunction[] = []
|
||||||
|
requests: SASjsRequest[] = []
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private serverUrl: string,
|
||||||
|
private sasViyaApiClient: SASViyaApiClient
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async execute(
|
||||||
|
sasJob: string,
|
||||||
|
data: any,
|
||||||
|
config: any,
|
||||||
|
loginRequiredCallback?: any,
|
||||||
|
accessToken?: string
|
||||||
|
) {
|
||||||
|
const loginCallback = loginRequiredCallback || (() => Promise.resolve())
|
||||||
|
const waitForResult = true
|
||||||
|
const expectWebout = true
|
||||||
|
this.sasViyaApiClient
|
||||||
|
?.executeComputeJob(
|
||||||
|
sasJob,
|
||||||
|
config.contextName,
|
||||||
|
config.debug,
|
||||||
|
data,
|
||||||
|
accessToken,
|
||||||
|
waitForResult,
|
||||||
|
expectWebout
|
||||||
|
)
|
||||||
|
.then((response) => {
|
||||||
|
this.appendRequest(response, sasJob, config.debug)
|
||||||
|
|
||||||
|
let responseJson
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (typeof response!.result === 'string') {
|
||||||
|
responseJson = JSON.parse(response!.result)
|
||||||
|
} else {
|
||||||
|
responseJson = response!.result
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
responseJson = JSON.parse(parseWeboutResponse(response!.result))
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseJson
|
||||||
|
})
|
||||||
|
.catch(async (e: Error) => {
|
||||||
|
if (e instanceof JobExecutionError) {
|
||||||
|
this.appendRequest(e, sasJob, config.debug)
|
||||||
|
}
|
||||||
|
if (e instanceof LoginRequiredError) {
|
||||||
|
await loginCallback()
|
||||||
|
this.waitingRequests.push(() =>
|
||||||
|
this.execute(sasJob, data, config, loginRequiredCallback)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return Promise.reject(new ErrorResponse(e?.message, e))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
resendWaitingRequests = async () => {
|
||||||
|
await asyncForEach(
|
||||||
|
this.waitingRequests,
|
||||||
|
async (waitingRequest: ExecuteFunction) => {
|
||||||
|
await waitingRequest()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
this.waitingRequests = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
getRequests = () => this.requests
|
||||||
|
|
||||||
|
clearRequests = () => {
|
||||||
|
this.requests = []
|
||||||
|
}
|
||||||
|
|
||||||
|
private async appendRequest(response: any, program: string, debug: boolean) {
|
||||||
|
let sourceCode = ''
|
||||||
|
let generatedCode = ''
|
||||||
|
let sasWork = null
|
||||||
|
|
||||||
|
if (debug) {
|
||||||
|
if (response?.result && response?.log) {
|
||||||
|
sourceCode = parseSourceCode(response.log)
|
||||||
|
generatedCode = parseGeneratedCode(response.log)
|
||||||
|
|
||||||
|
if (response.log) {
|
||||||
|
sasWork = response.log
|
||||||
|
} else {
|
||||||
|
sasWork = JSON.parse(parseWeboutResponse(response.result)).WORK
|
||||||
|
}
|
||||||
|
} else if (response?.result) {
|
||||||
|
sourceCode = parseSourceCode(response.result)
|
||||||
|
generatedCode = parseGeneratedCode(response.result)
|
||||||
|
sasWork = await parseSasWork(
|
||||||
|
response.result,
|
||||||
|
debug,
|
||||||
|
this.serverUrl,
|
||||||
|
ServerType.SasViya
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.requests.push({
|
||||||
|
logFile: response?.log || response?.result || response,
|
||||||
|
serviceLink: program,
|
||||||
|
timestamp: new Date(),
|
||||||
|
sourceCode,
|
||||||
|
generatedCode,
|
||||||
|
SASWORK: sasWork
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.requests.length > 20) {
|
||||||
|
this.requests.splice(0, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
122
src/job-execution/JesJobExecutor.ts
Normal file
122
src/job-execution/JesJobExecutor.ts
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import { ServerType } from '@sasjs/utils/types'
|
||||||
|
import { ErrorResponse } from '..'
|
||||||
|
import { SASViyaApiClient } from '../SASViyaApiClient'
|
||||||
|
import { JobExecutionError, LoginRequiredError, SASjsRequest } from '../types'
|
||||||
|
import {
|
||||||
|
asyncForEach,
|
||||||
|
parseGeneratedCode,
|
||||||
|
parseSourceCode,
|
||||||
|
parseWeboutResponse
|
||||||
|
} from '../utils'
|
||||||
|
import { ExecuteFunction, JobExecutor } from './JobExecutor'
|
||||||
|
import { parseSasWork } from './parseSasWork'
|
||||||
|
|
||||||
|
export class JesJobExecutor implements JobExecutor {
|
||||||
|
waitingRequests: ExecuteFunction[] = []
|
||||||
|
requests: SASjsRequest[] = []
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private serverUrl: string,
|
||||||
|
private sasViyaApiClient: SASViyaApiClient
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async execute(
|
||||||
|
sasJob: string,
|
||||||
|
data: any,
|
||||||
|
config: any,
|
||||||
|
loginRequiredCallback?: any,
|
||||||
|
accessToken?: string
|
||||||
|
) {
|
||||||
|
const loginCallback = loginRequiredCallback || (() => Promise.resolve())
|
||||||
|
await this.sasViyaApiClient
|
||||||
|
?.executeJob(sasJob, config.contextName, config.debug, data, accessToken)
|
||||||
|
.then((response) => {
|
||||||
|
this.appendRequest(response, sasJob, config.debug)
|
||||||
|
|
||||||
|
let responseJson
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (typeof response!.result === 'string') {
|
||||||
|
responseJson = JSON.parse(response!.result)
|
||||||
|
} else {
|
||||||
|
responseJson = response!.result
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
responseJson = JSON.parse(parseWeboutResponse(response!.result))
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseJson
|
||||||
|
})
|
||||||
|
.catch(async (e: Error) => {
|
||||||
|
if (e instanceof JobExecutionError) {
|
||||||
|
this.appendRequest(e, sasJob, config.debug)
|
||||||
|
}
|
||||||
|
if (e instanceof LoginRequiredError) {
|
||||||
|
await loginCallback()
|
||||||
|
this.waitingRequests.push(() =>
|
||||||
|
this.execute(sasJob, data, config, loginRequiredCallback)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return Promise.reject(new ErrorResponse(e?.message, e))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
resendWaitingRequests = async () => {
|
||||||
|
await asyncForEach(
|
||||||
|
this.waitingRequests,
|
||||||
|
async (waitingRequest: ExecuteFunction) => {
|
||||||
|
await waitingRequest()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
this.waitingRequests = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
getRequests = () => this.requests
|
||||||
|
|
||||||
|
clearRequests = () => {
|
||||||
|
this.requests = []
|
||||||
|
}
|
||||||
|
|
||||||
|
private async appendRequest(response: any, program: string, debug: boolean) {
|
||||||
|
let sourceCode = ''
|
||||||
|
let generatedCode = ''
|
||||||
|
let sasWork = null
|
||||||
|
|
||||||
|
if (debug) {
|
||||||
|
if (response?.result && response?.log) {
|
||||||
|
sourceCode = parseSourceCode(response.log)
|
||||||
|
generatedCode = parseGeneratedCode(response.log)
|
||||||
|
|
||||||
|
if (response.log) {
|
||||||
|
sasWork = response.log
|
||||||
|
} else {
|
||||||
|
sasWork = JSON.parse(parseWeboutResponse(response.result)).WORK
|
||||||
|
}
|
||||||
|
} else if (response?.result) {
|
||||||
|
sourceCode = parseSourceCode(response.result)
|
||||||
|
generatedCode = parseGeneratedCode(response.result)
|
||||||
|
sasWork = await parseSasWork(
|
||||||
|
response.result,
|
||||||
|
debug,
|
||||||
|
this.serverUrl,
|
||||||
|
ServerType.SasViya
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.requests.push({
|
||||||
|
logFile: response?.log || response?.result || response,
|
||||||
|
serviceLink: program,
|
||||||
|
timestamp: new Date(),
|
||||||
|
sourceCode,
|
||||||
|
generatedCode,
|
||||||
|
SASWORK: sasWork
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.requests.length > 20) {
|
||||||
|
this.requests.splice(0, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/job-execution/JobExecutor.ts
Normal file
17
src/job-execution/JobExecutor.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { SASjsRequest } from '../types'
|
||||||
|
|
||||||
|
export type ExecuteFunction = () => Promise<any>
|
||||||
|
|
||||||
|
export interface JobExecutor {
|
||||||
|
execute: (
|
||||||
|
sasJob: string,
|
||||||
|
data: any,
|
||||||
|
config: any,
|
||||||
|
loginRequiredCallback?: any,
|
||||||
|
accessToken?: string
|
||||||
|
) => Promise<any>
|
||||||
|
waitingRequests: ExecuteFunction[]
|
||||||
|
resendWaitingRequests: () => Promise<void>
|
||||||
|
getRequests: () => SASjsRequest[]
|
||||||
|
clearRequests: () => void
|
||||||
|
}
|
||||||
239
src/job-execution/WebJobExecutor.ts
Normal file
239
src/job-execution/WebJobExecutor.ts
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
import { ServerType } from '@sasjs/utils/types'
|
||||||
|
import { ErrorResponse, JobExecutionError, LoginRequiredError } from '..'
|
||||||
|
import { generateFileUploadForm } from '../file/generateFileUploadForm'
|
||||||
|
import { generateTableUploadForm } from '../file/generateTableUploadForm'
|
||||||
|
import { RequestClient } from '../request/RequestClient'
|
||||||
|
import { SASViyaApiClient } from '../SASViyaApiClient'
|
||||||
|
import { SASjsRequest } from '../types'
|
||||||
|
import {
|
||||||
|
asyncForEach,
|
||||||
|
isRelativePath,
|
||||||
|
parseGeneratedCode,
|
||||||
|
parseSourceCode,
|
||||||
|
parseWeboutResponse
|
||||||
|
} from '../utils'
|
||||||
|
import { ExecuteFunction, JobExecutor } from './JobExecutor'
|
||||||
|
import { parseSasWork } from './parseSasWork'
|
||||||
|
|
||||||
|
export class WebJobExecutor implements JobExecutor {
|
||||||
|
waitingRequests: ExecuteFunction[] = []
|
||||||
|
requests: SASjsRequest[] = []
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private serverUrl: string,
|
||||||
|
private serverType: ServerType,
|
||||||
|
private jobsPath: string,
|
||||||
|
private requestClient: RequestClient,
|
||||||
|
private sasViyaApiClient: SASViyaApiClient
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async execute(
|
||||||
|
sasJob: string,
|
||||||
|
data: any,
|
||||||
|
config: any,
|
||||||
|
loginRequiredCallback?: any
|
||||||
|
) {
|
||||||
|
const loginCallback = loginRequiredCallback || (() => Promise.resolve())
|
||||||
|
const program = isRelativePath(sasJob)
|
||||||
|
? config.appLoc
|
||||||
|
? config.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
|
||||||
|
: sasJob
|
||||||
|
: sasJob
|
||||||
|
const jobUri =
|
||||||
|
config.serverType === ServerType.SasViya
|
||||||
|
? await this.getJobUri(sasJob)
|
||||||
|
: ''
|
||||||
|
const apiUrl = `${config.serverUrl}${this.jobsPath}/?${
|
||||||
|
jobUri.length > 0
|
||||||
|
? '__program=' + program + '&_job=' + jobUri
|
||||||
|
: '_program=' + program
|
||||||
|
}`
|
||||||
|
|
||||||
|
let requestParams = {
|
||||||
|
...this.getRequestParams(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
let formData = new FormData()
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
const stringifiedData = JSON.stringify(data)
|
||||||
|
if (
|
||||||
|
config.serverType === ServerType.Sas9 ||
|
||||||
|
stringifiedData.length > 500000 ||
|
||||||
|
stringifiedData.includes(';')
|
||||||
|
) {
|
||||||
|
// file upload approach
|
||||||
|
try {
|
||||||
|
formData = generateFileUploadForm(formData, data)
|
||||||
|
} catch (e) {
|
||||||
|
return Promise.reject(new ErrorResponse(e?.message, e))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// param based approach
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
formData: newFormData,
|
||||||
|
requestParams: params
|
||||||
|
} = generateTableUploadForm(formData, data)
|
||||||
|
formData = newFormData
|
||||||
|
requestParams = { ...requestParams, ...params }
|
||||||
|
} catch (e) {
|
||||||
|
return Promise.reject(new ErrorResponse(e?.message, e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key in requestParams) {
|
||||||
|
if (requestParams.hasOwnProperty(key)) {
|
||||||
|
formData.append(key, requestParams[key])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.requestClient!.post(
|
||||||
|
apiUrl,
|
||||||
|
formData,
|
||||||
|
undefined,
|
||||||
|
'application/json',
|
||||||
|
{
|
||||||
|
referrerPolicy: 'same-origin'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(async (res) => {
|
||||||
|
this.appendRequest(res, sasJob, config.debug)
|
||||||
|
return res.result
|
||||||
|
})
|
||||||
|
.catch(async (e: Error) => {
|
||||||
|
if (e instanceof JobExecutionError) {
|
||||||
|
this.appendRequest(e, sasJob, config.debug)
|
||||||
|
}
|
||||||
|
if (e instanceof LoginRequiredError) {
|
||||||
|
await loginCallback()
|
||||||
|
this.waitingRequests.push(() =>
|
||||||
|
this.execute(sasJob, data, config, loginRequiredCallback)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return Promise.reject(new ErrorResponse(e?.message, e))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
resendWaitingRequests = async () => {
|
||||||
|
await asyncForEach(
|
||||||
|
this.waitingRequests,
|
||||||
|
async (waitingRequest: ExecuteFunction) => {
|
||||||
|
await waitingRequest()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
this.waitingRequests = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
getRequests = () => this.requests
|
||||||
|
|
||||||
|
clearRequests = () => {
|
||||||
|
this.requests = []
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getJobUri(sasJob: string) {
|
||||||
|
if (!this.sasViyaApiClient) return ''
|
||||||
|
let uri = ''
|
||||||
|
|
||||||
|
let folderPath
|
||||||
|
let jobName: string
|
||||||
|
if (isRelativePath(sasJob)) {
|
||||||
|
folderPath = sasJob.split('/')[0]
|
||||||
|
jobName = sasJob.split('/')[1]
|
||||||
|
} else {
|
||||||
|
const folderPathParts = sasJob.split('/')
|
||||||
|
jobName = folderPathParts.pop() || ''
|
||||||
|
folderPath = folderPathParts.join('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
const locJobs = await this.sasViyaApiClient.getJobsInFolder(folderPath)
|
||||||
|
if (locJobs) {
|
||||||
|
const job = locJobs.find(
|
||||||
|
(el: any) => el.name === jobName && el.contentType === 'jobDefinition'
|
||||||
|
)
|
||||||
|
if (job) {
|
||||||
|
uri = job.uri
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uri
|
||||||
|
}
|
||||||
|
|
||||||
|
private getRequestParams(config: any): any {
|
||||||
|
const requestParams: any = {}
|
||||||
|
|
||||||
|
if (config.debug) {
|
||||||
|
requestParams['_omittextlog'] = 'false'
|
||||||
|
requestParams['_omitsessionresults'] = 'false'
|
||||||
|
|
||||||
|
requestParams['_debug'] = 131
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestParams
|
||||||
|
}
|
||||||
|
|
||||||
|
private async appendRequest(response: any, program: string, debug: boolean) {
|
||||||
|
let sourceCode = ''
|
||||||
|
let generatedCode = ''
|
||||||
|
let sasWork = null
|
||||||
|
|
||||||
|
if (debug) {
|
||||||
|
if (response?.result && response?.log) {
|
||||||
|
sourceCode = parseSourceCode(response.log)
|
||||||
|
generatedCode = parseGeneratedCode(response.log)
|
||||||
|
|
||||||
|
if (response.log) {
|
||||||
|
sasWork = response.log
|
||||||
|
} else {
|
||||||
|
sasWork = JSON.parse(parseWeboutResponse(response.result)).WORK
|
||||||
|
}
|
||||||
|
} else if (response?.result) {
|
||||||
|
sourceCode = parseSourceCode(response.result)
|
||||||
|
generatedCode = parseGeneratedCode(response.result)
|
||||||
|
sasWork = await parseSasWork(
|
||||||
|
response.result,
|
||||||
|
debug,
|
||||||
|
this.serverUrl,
|
||||||
|
this.serverType
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.requests.push({
|
||||||
|
logFile: response?.log || response?.result || response,
|
||||||
|
serviceLink: program,
|
||||||
|
timestamp: new Date(),
|
||||||
|
sourceCode,
|
||||||
|
generatedCode,
|
||||||
|
SASWORK: sasWork
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.requests.length > 20) {
|
||||||
|
this.requests.splice(0, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseSAS9ErrorResponse(response: string) {
|
||||||
|
const logLines = response.split('\n')
|
||||||
|
const parsedLines: string[] = []
|
||||||
|
let firstErrorLineIndex: number = -1
|
||||||
|
|
||||||
|
logLines.map((line: string, index: number) => {
|
||||||
|
if (
|
||||||
|
line.toLowerCase().includes('error') &&
|
||||||
|
!line.toLowerCase().includes('this request completed with errors.') &&
|
||||||
|
firstErrorLineIndex === -1
|
||||||
|
) {
|
||||||
|
firstErrorLineIndex = index
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
for (let i = firstErrorLineIndex - 10; i <= firstErrorLineIndex + 10; i++) {
|
||||||
|
parsedLines.push(logLines[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedLines.join(', ')
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/job-execution/index.ts
Normal file
5
src/job-execution/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export * from './ComputeJobExecutor'
|
||||||
|
export * from './JesJobExecutor'
|
||||||
|
export * from './JobExecutor'
|
||||||
|
export * from './parseSasWork'
|
||||||
|
export * from './WebJobExecutor'
|
||||||
61
src/job-execution/parseSasWork.ts
Normal file
61
src/job-execution/parseSasWork.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { ServerType } from '@sasjs/utils/types'
|
||||||
|
import { parseWeboutResponse } from '../utils'
|
||||||
|
|
||||||
|
export const parseSasWork = async (
|
||||||
|
response: any,
|
||||||
|
debug: boolean,
|
||||||
|
serverUrl: string,
|
||||||
|
serverType: ServerType
|
||||||
|
) => {
|
||||||
|
if (debug) {
|
||||||
|
let jsonResponse
|
||||||
|
|
||||||
|
if (serverType === ServerType.Sas9) {
|
||||||
|
try {
|
||||||
|
jsonResponse = JSON.parse(parseWeboutResponse(response))
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await parseSASVIYADebugResponse(response, serverUrl).then(
|
||||||
|
(resText: any) => {
|
||||||
|
try {
|
||||||
|
jsonResponse = JSON.parse(resText)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(err: any) => {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsonResponse) {
|
||||||
|
return jsonResponse.WORK
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseSASVIYADebugResponse = async (
|
||||||
|
response: string,
|
||||||
|
serverUrl: string
|
||||||
|
) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const iframeStart = response.split(
|
||||||
|
'<iframe style="width: 99%; height: 500px" src="'
|
||||||
|
)[1]
|
||||||
|
const jsonUrl = iframeStart ? iframeStart.split('"></iframe>')[0] : null
|
||||||
|
|
||||||
|
if (jsonUrl) {
|
||||||
|
fetch(serverUrl + jsonUrl)
|
||||||
|
.then((res) => res.text())
|
||||||
|
.then((resText) => {
|
||||||
|
resolve(resText)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
reject('No debug info found in response.')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
||||||
import { CsrfToken } from '..'
|
import { CsrfToken, JobExecutionError } from '..'
|
||||||
|
import { LoginRequiredError } from '../types'
|
||||||
|
import { AuthorizeError } from '../types/AuthorizeError'
|
||||||
|
|
||||||
export class RequestClient {
|
export class RequestClient {
|
||||||
private csrfToken: CsrfToken | undefined
|
private csrfToken: CsrfToken | undefined
|
||||||
@@ -58,24 +60,30 @@ export class RequestClient {
|
|||||||
url: string,
|
url: string,
|
||||||
data: any,
|
data: any,
|
||||||
accessToken: string | undefined,
|
accessToken: string | undefined,
|
||||||
|
contentType = 'application/json',
|
||||||
overrideHeaders: { [key: string]: string | number } = {}
|
overrideHeaders: { [key: string]: string | number } = {}
|
||||||
): Promise<{ result: T; etag: string }> {
|
): Promise<{ result: T; etag: string }> {
|
||||||
const headers = {
|
const headers = {
|
||||||
...this.getHeaders(accessToken, 'application/json'),
|
...this.getHeaders(accessToken, contentType),
|
||||||
...overrideHeaders
|
...overrideHeaders
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.httpClient
|
return this.httpClient
|
||||||
.post<T>(url, data, { headers, withCredentials: true })
|
.post<T>(url, data, { headers, withCredentials: true })
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
|
throwIfError(response)
|
||||||
return {
|
return {
|
||||||
result: response.data as T,
|
result: response.data as T,
|
||||||
etag: response.headers['etag'] as string
|
etag: response.headers['etag'] as string
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch(async (e) => {
|
||||||
const response = e.response as AxiosResponse
|
const response = e.response as AxiosResponse
|
||||||
if (response.status === 403 || response.status === 449) {
|
if (e instanceof AuthorizeError) {
|
||||||
|
await this.post(e.confirmUrl, { value: true }, undefined)
|
||||||
|
return this.post<T>(url, data, accessToken)
|
||||||
|
}
|
||||||
|
if (response?.status === 403 || response?.status === 449) {
|
||||||
this.parseAndSetCsrfToken(response)
|
this.parseAndSetCsrfToken(response)
|
||||||
|
|
||||||
if (this.csrfToken) {
|
if (this.csrfToken) {
|
||||||
@@ -108,15 +116,19 @@ export class RequestClient {
|
|||||||
etag: response.headers['etag'] as string
|
etag: response.headers['etag'] as string
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const response_1 = e.response as AxiosResponse
|
const response = e.response as AxiosResponse
|
||||||
if (response_1.status === 403 || response_1.status === 449) {
|
if (response?.status === 403 || response?.status === 449) {
|
||||||
this.parseAndSetCsrfToken(response_1)
|
this.parseAndSetCsrfToken(response)
|
||||||
|
|
||||||
if (this.csrfToken) {
|
if (this.csrfToken) {
|
||||||
return this.put<T>(url, data, accessToken)
|
return this.put<T>(url, data, accessToken)
|
||||||
}
|
}
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (response?.status === 401) {
|
||||||
|
throw new LoginRequiredError()
|
||||||
|
}
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,9 +150,9 @@ export class RequestClient {
|
|||||||
etag: response.headers['etag']
|
etag: response.headers['etag']
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const response_1 = e.response as AxiosResponse
|
const response = e.response as AxiosResponse
|
||||||
if (response_1.status === 403 || response_1.status === 449) {
|
if (response?.status === 403 || response?.status === 449) {
|
||||||
this.parseAndSetCsrfToken(response_1)
|
this.parseAndSetCsrfToken(response)
|
||||||
|
|
||||||
if (this.csrfToken) {
|
if (this.csrfToken) {
|
||||||
return this.delete<T>(url, accessToken)
|
return this.delete<T>(url, accessToken)
|
||||||
@@ -168,9 +180,9 @@ export class RequestClient {
|
|||||||
etag: response.headers['etag'] as string
|
etag: response.headers['etag'] as string
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const response_1 = e.response as AxiosResponse
|
const response = e.response as AxiosResponse
|
||||||
if (response_1.status === 403 || response_1.status === 449) {
|
if (response?.status === 403 || response?.status === 449) {
|
||||||
this.parseAndSetCsrfToken(response_1)
|
this.parseAndSetCsrfToken(response)
|
||||||
|
|
||||||
if (this.csrfToken) {
|
if (this.csrfToken) {
|
||||||
return this.patch<T>(url, accessToken)
|
return this.patch<T>(url, accessToken)
|
||||||
@@ -201,9 +213,9 @@ export class RequestClient {
|
|||||||
etag: response.headers['etag'] as string
|
etag: response.headers['etag'] as string
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const response_1 = e.response as AxiosResponse
|
const response = e.response as AxiosResponse
|
||||||
if (response_1.status === 403 || response_1.status === 449) {
|
if (response?.status === 403 || response?.status === 449) {
|
||||||
this.parseAndSetFileUploadCsrfToken(response_1)
|
this.parseAndSetFileUploadCsrfToken(response)
|
||||||
|
|
||||||
if (this.fileUploadCsrfToken) {
|
if (this.fileUploadCsrfToken) {
|
||||||
return this.uploadFile(url, content, accessToken)
|
return this.uploadFile(url, content, accessToken)
|
||||||
@@ -214,10 +226,17 @@ export class RequestClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getHeaders(accessToken: string | undefined, contentType: string) {
|
private getHeaders = (
|
||||||
|
accessToken: string | undefined,
|
||||||
|
contentType: string
|
||||||
|
) => {
|
||||||
const headers: any = {
|
const headers: any = {
|
||||||
'Content-Type': contentType
|
'Content-Type': contentType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (contentType === 'text/plain') {
|
||||||
|
headers.Accept = '*/*'
|
||||||
|
}
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
headers.Authorization = `Bearer ${accessToken}`
|
headers.Authorization = `Bearer ${accessToken}`
|
||||||
}
|
}
|
||||||
@@ -228,7 +247,7 @@ export class RequestClient {
|
|||||||
return headers
|
return headers
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseAndSetFileUploadCsrfToken(response: AxiosResponse) {
|
private parseAndSetFileUploadCsrfToken = (response: AxiosResponse) => {
|
||||||
const token = this.parseCsrfToken(response)
|
const token = this.parseCsrfToken(response)
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
@@ -236,7 +255,7 @@ export class RequestClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseAndSetCsrfToken(response: AxiosResponse) {
|
private parseAndSetCsrfToken = (response: AxiosResponse) => {
|
||||||
const token = this.parseCsrfToken(response)
|
const token = this.parseCsrfToken(response)
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
@@ -244,7 +263,7 @@ export class RequestClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseCsrfToken(response: AxiosResponse): CsrfToken | undefined {
|
private parseCsrfToken = (response: AxiosResponse): CsrfToken | undefined => {
|
||||||
const tokenHeader = (response.headers[
|
const tokenHeader = (response.headers[
|
||||||
'x-csrf-header'
|
'x-csrf-header'
|
||||||
] as string)?.toLowerCase()
|
] as string)?.toLowerCase()
|
||||||
@@ -260,3 +279,54 @@ export class RequestClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const throwIfError = (response: AxiosResponse) => {
|
||||||
|
if (response.data?.entityID?.includes('login')) {
|
||||||
|
throw new LoginRequiredError()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.data?.auth_request) {
|
||||||
|
throw new AuthorizeError(
|
||||||
|
response.data.message,
|
||||||
|
response.data.options.confirm.location
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const error = parseError(response.data as string)
|
||||||
|
if (error) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseError = (data: string) => {
|
||||||
|
try {
|
||||||
|
const responseJson = JSON.parse(data?.replace(/[\n\r]/g, ' '))
|
||||||
|
return responseJson.errorCode && responseJson.message
|
||||||
|
? new JobExecutionError(
|
||||||
|
responseJson.errorCode,
|
||||||
|
responseJson.message,
|
||||||
|
data?.replace(/[\n\r]/g, ' ')
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
} catch (_) {
|
||||||
|
try {
|
||||||
|
const hasError = data?.includes('{"errorCode')
|
||||||
|
if (hasError) {
|
||||||
|
const parts = data.split('{"errorCode')
|
||||||
|
if (parts.length > 1) {
|
||||||
|
const error = '{"errorCode' + parts[1].split('"}')[0] + '"}'
|
||||||
|
const errorJson = JSON.parse(error.replace(/[\n\r]/g, ' '))
|
||||||
|
return new JobExecutionError(
|
||||||
|
errorJson.errorCode,
|
||||||
|
errorJson.message,
|
||||||
|
data?.replace(/[\n\r]/g, '\n')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
} catch (_) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/types/AuthorizeError.ts
Normal file
7
src/types/AuthorizeError.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export class AuthorizeError extends Error {
|
||||||
|
constructor(public message: string, public confirmUrl: string) {
|
||||||
|
super(message)
|
||||||
|
this.name = 'AuthorizeError'
|
||||||
|
Object.setPrototypeOf(this, AuthorizeError.prototype)
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/types/JobExecutionError.ts
Normal file
11
src/types/JobExecutionError.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export class JobExecutionError extends Error {
|
||||||
|
constructor(
|
||||||
|
public errorCode: number,
|
||||||
|
public errorMessage: string,
|
||||||
|
public result: string
|
||||||
|
) {
|
||||||
|
super(`Error Code ${errorCode}: ${errorMessage}`)
|
||||||
|
this.name = 'JobExecutionError'
|
||||||
|
Object.setPrototypeOf(this, JobExecutionError.prototype)
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/types/LoginRequiredError.ts
Normal file
7
src/types/LoginRequiredError.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export class LoginRequiredError extends Error {
|
||||||
|
constructor() {
|
||||||
|
super('Auth error: You must be logged in to access this resource')
|
||||||
|
this.name = 'LoginRequiredError'
|
||||||
|
Object.setPrototypeOf(this, LoginRequiredError.prototype)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,9 +3,11 @@ export * from './CsrfToken'
|
|||||||
export * from './ErrorResponse'
|
export * from './ErrorResponse'
|
||||||
export * from './Folder'
|
export * from './Folder'
|
||||||
export * from './Job'
|
export * from './Job'
|
||||||
|
export * from './JobExecutionError'
|
||||||
export * from './JobDefinition'
|
export * from './JobDefinition'
|
||||||
export * from './JobResult'
|
export * from './JobResult'
|
||||||
export * from './Link'
|
export * from './Link'
|
||||||
|
export * from './LoginRequiredError'
|
||||||
export * from './SASjsConfig'
|
export * from './SASjsConfig'
|
||||||
export * from './SASjsRequest'
|
export * from './SASjsRequest'
|
||||||
export * from './SASjsWaitingRequest'
|
export * from './SASjsWaitingRequest'
|
||||||
|
|||||||
Reference in New Issue
Block a user