mirror of
https://github.com/sasjs/adapter.git
synced 2025-12-11 01:14:36 +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 { isUrl } from './utils'
|
||||
import { prefixMessage } from '@sasjs/utils/error'
|
||||
import { RequestClient } from './request/client'
|
||||
import { RequestClient } from './request/RequestClient'
|
||||
|
||||
export class ContextManager {
|
||||
private defaultComputeContexts = [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { isUrl } from './utils'
|
||||
import { UploadFile } from './types/UploadFile'
|
||||
import { ErrorResponse } from './types'
|
||||
import { RequestClient } from './request/client'
|
||||
import { RequestClient } from './request/RequestClient'
|
||||
|
||||
export class FileUploader {
|
||||
constructor(
|
||||
@@ -52,7 +52,7 @@ export class FileUploader {
|
||||
}
|
||||
|
||||
return this.requestClient
|
||||
.post(uploadUrl, formData, undefined, headers)
|
||||
.post(uploadUrl, formData, undefined, 'application/json', headers)
|
||||
.then((res) => res.result)
|
||||
.catch((err: Error) => {
|
||||
return Promise.reject(
|
||||
|
||||
@@ -17,7 +17,7 @@ import { timestampToYYYYMMDDHHMMSS } from '@sasjs/utils/time'
|
||||
import { Logger, LogLevel } from '@sasjs/utils/logger'
|
||||
import { isAuthorizeFormRequired } from './auth/isAuthorizeFormRequired'
|
||||
import { parseAndSubmitAuthorizeForm } from './auth'
|
||||
import { RequestClient } from './request/client'
|
||||
import { RequestClient } from './request/RequestClient'
|
||||
|
||||
/**
|
||||
* A client for interfacing with the SAS Viya REST API.
|
||||
|
||||
734
src/SASjs.ts
734
src/SASjs.ts
@@ -1,30 +1,16 @@
|
||||
import {
|
||||
convertToCSV,
|
||||
compareTimestamps,
|
||||
splitChunks,
|
||||
parseSourceCode,
|
||||
parseGeneratedCode,
|
||||
parseWeboutResponse,
|
||||
needsRetry,
|
||||
asyncForEach,
|
||||
isRelativePath
|
||||
} from './utils'
|
||||
import {
|
||||
SASjsConfig,
|
||||
SASjsRequest,
|
||||
SASjsWaitingRequest,
|
||||
CsrfToken,
|
||||
UploadFile,
|
||||
EditContextInput,
|
||||
ErrorResponse,
|
||||
PollOptions
|
||||
} from './types'
|
||||
import { compareTimestamps, asyncForEach } from './utils'
|
||||
import { SASjsConfig, UploadFile, EditContextInput, PollOptions } from './types'
|
||||
import { SASViyaApiClient } from './SASViyaApiClient'
|
||||
import { SAS9ApiClient } from './SAS9ApiClient'
|
||||
import { FileUploader } from './FileUploader'
|
||||
import { isLogInRequired, AuthManager } from './auth'
|
||||
import { AuthManager } from './auth'
|
||||
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 = {
|
||||
serverUrl: '',
|
||||
@@ -37,8 +23,6 @@ const defaultConfig: SASjsConfig = {
|
||||
useComputeApi: false
|
||||
}
|
||||
|
||||
const requestRetryLimit = 5
|
||||
|
||||
/**
|
||||
* SASjs is a JavaScript adapter for SAS.
|
||||
*
|
||||
@@ -46,17 +30,14 @@ const requestRetryLimit = 5
|
||||
export default class SASjs {
|
||||
private sasjsConfig: SASjsConfig = new SASjsConfig()
|
||||
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 sas9ApiClient: SAS9ApiClient | null = null
|
||||
private fileUploader: FileUploader | null = null
|
||||
private authManager: AuthManager | 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) {
|
||||
this.sasjsConfig = {
|
||||
@@ -260,6 +241,11 @@ export default class SASjs {
|
||||
debug?: boolean
|
||||
) {
|
||||
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(
|
||||
fileName,
|
||||
@@ -514,8 +500,6 @@ export default class SASjs {
|
||||
loginRequiredCallback?: any,
|
||||
accessToken?: string
|
||||
) {
|
||||
let requestResponse
|
||||
|
||||
config = {
|
||||
...this.sasjsConfig,
|
||||
...config
|
||||
@@ -523,36 +507,30 @@ export default class SASjs {
|
||||
|
||||
if (config.serverType === ServerType.SasViya && config.contextName) {
|
||||
if (config.useComputeApi) {
|
||||
requestResponse = await this.executeJobViaComputeApi(
|
||||
return await this.computeJobExecutor!.execute(
|
||||
sasJob,
|
||||
data,
|
||||
config,
|
||||
loginRequiredCallback,
|
||||
accessToken
|
||||
)
|
||||
|
||||
this.retryCountComputeApi = 0
|
||||
} else {
|
||||
requestResponse = await this.executeJobViaJesApi(
|
||||
return await this.jesJobExecutor!.execute(
|
||||
sasJob,
|
||||
data,
|
||||
config,
|
||||
loginRequiredCallback,
|
||||
accessToken
|
||||
)
|
||||
|
||||
this.retryCountJeseApi = 0
|
||||
}
|
||||
} else {
|
||||
requestResponse = await this.executeJobViaWeb(
|
||||
return await this.webJobExecutor!.execute(
|
||||
sasJob,
|
||||
data,
|
||||
config,
|
||||
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 () => {
|
||||
for (const sasjsWaitingRequest of this.sasjsWaitingRequests) {
|
||||
this.request(sasjsWaitingRequest.SASjob, sasjsWaitingRequest.data).then(
|
||||
(res: any) => {
|
||||
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)
|
||||
}
|
||||
}
|
||||
await this.webJobExecutor?.resendWaitingRequests()
|
||||
await this.computeJobExecutor?.resendWaitingRequests()
|
||||
await this.jesJobExecutor?.resendWaitingRequests()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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() {
|
||||
const sortedRequests = this.sasjsRequests.sort(compareTimestamps)
|
||||
const requests = [
|
||||
...this.webJobExecutor!.getRequests(),
|
||||
...this.computeJobExecutor!.getRequests(),
|
||||
...this.jesJobExecutor!.getRequests()
|
||||
]
|
||||
const sortedRequests = requests.sort(compareTimestamps)
|
||||
return sortedRequests
|
||||
}
|
||||
|
||||
public clearSasRequests() {
|
||||
this.sasjsRequests = []
|
||||
this.webJobExecutor!.clearRequests()
|
||||
this.computeJobExecutor!.clearRequests()
|
||||
this.jesJobExecutor!.clearRequests()
|
||||
}
|
||||
|
||||
private setupConfiguration() {
|
||||
@@ -1413,6 +756,19 @@ export default class SASjs {
|
||||
this.jobsPath,
|
||||
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(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Session, Context, CsrfToken, SessionVariable } from './types'
|
||||
import { asyncForEach, isUrl } from './utils'
|
||||
import { prefixMessage } from '@sasjs/utils/error'
|
||||
import { RequestClient } from './request/client'
|
||||
import { RequestClient } from './request/RequestClient'
|
||||
|
||||
const MAX_SESSION_COUNT = 1
|
||||
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 { CsrfToken } from '..'
|
||||
import { CsrfToken, JobExecutionError } from '..'
|
||||
import { LoginRequiredError } from '../types'
|
||||
import { AuthorizeError } from '../types/AuthorizeError'
|
||||
|
||||
export class RequestClient {
|
||||
private csrfToken: CsrfToken | undefined
|
||||
@@ -58,24 +60,30 @@ export class RequestClient {
|
||||
url: string,
|
||||
data: any,
|
||||
accessToken: string | undefined,
|
||||
contentType = 'application/json',
|
||||
overrideHeaders: { [key: string]: string | number } = {}
|
||||
): Promise<{ result: T; etag: string }> {
|
||||
const headers = {
|
||||
...this.getHeaders(accessToken, 'application/json'),
|
||||
...this.getHeaders(accessToken, contentType),
|
||||
...overrideHeaders
|
||||
}
|
||||
|
||||
return this.httpClient
|
||||
.post<T>(url, data, { headers, withCredentials: true })
|
||||
.then((response) => {
|
||||
throwIfError(response)
|
||||
return {
|
||||
result: response.data as T,
|
||||
etag: response.headers['etag'] as string
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
.catch(async (e) => {
|
||||
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)
|
||||
|
||||
if (this.csrfToken) {
|
||||
@@ -108,15 +116,19 @@ export class RequestClient {
|
||||
etag: response.headers['etag'] as string
|
||||
}
|
||||
} catch (e) {
|
||||
const response_1 = e.response as AxiosResponse
|
||||
if (response_1.status === 403 || response_1.status === 449) {
|
||||
this.parseAndSetCsrfToken(response_1)
|
||||
const response = e.response as AxiosResponse
|
||||
if (response?.status === 403 || response?.status === 449) {
|
||||
this.parseAndSetCsrfToken(response)
|
||||
|
||||
if (this.csrfToken) {
|
||||
return this.put<T>(url, data, accessToken)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
|
||||
if (response?.status === 401) {
|
||||
throw new LoginRequiredError()
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
@@ -138,9 +150,9 @@ export class RequestClient {
|
||||
etag: response.headers['etag']
|
||||
}
|
||||
} catch (e) {
|
||||
const response_1 = e.response as AxiosResponse
|
||||
if (response_1.status === 403 || response_1.status === 449) {
|
||||
this.parseAndSetCsrfToken(response_1)
|
||||
const response = e.response as AxiosResponse
|
||||
if (response?.status === 403 || response?.status === 449) {
|
||||
this.parseAndSetCsrfToken(response)
|
||||
|
||||
if (this.csrfToken) {
|
||||
return this.delete<T>(url, accessToken)
|
||||
@@ -168,9 +180,9 @@ export class RequestClient {
|
||||
etag: response.headers['etag'] as string
|
||||
}
|
||||
} catch (e) {
|
||||
const response_1 = e.response as AxiosResponse
|
||||
if (response_1.status === 403 || response_1.status === 449) {
|
||||
this.parseAndSetCsrfToken(response_1)
|
||||
const response = e.response as AxiosResponse
|
||||
if (response?.status === 403 || response?.status === 449) {
|
||||
this.parseAndSetCsrfToken(response)
|
||||
|
||||
if (this.csrfToken) {
|
||||
return this.patch<T>(url, accessToken)
|
||||
@@ -201,9 +213,9 @@ export class RequestClient {
|
||||
etag: response.headers['etag'] as string
|
||||
}
|
||||
} catch (e) {
|
||||
const response_1 = e.response as AxiosResponse
|
||||
if (response_1.status === 403 || response_1.status === 449) {
|
||||
this.parseAndSetFileUploadCsrfToken(response_1)
|
||||
const response = e.response as AxiosResponse
|
||||
if (response?.status === 403 || response?.status === 449) {
|
||||
this.parseAndSetFileUploadCsrfToken(response)
|
||||
|
||||
if (this.fileUploadCsrfToken) {
|
||||
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 = {
|
||||
'Content-Type': contentType
|
||||
}
|
||||
|
||||
if (contentType === 'text/plain') {
|
||||
headers.Accept = '*/*'
|
||||
}
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`
|
||||
}
|
||||
@@ -228,7 +247,7 @@ export class RequestClient {
|
||||
return headers
|
||||
}
|
||||
|
||||
private parseAndSetFileUploadCsrfToken(response: AxiosResponse) {
|
||||
private parseAndSetFileUploadCsrfToken = (response: AxiosResponse) => {
|
||||
const token = this.parseCsrfToken(response)
|
||||
|
||||
if (token) {
|
||||
@@ -236,7 +255,7 @@ export class RequestClient {
|
||||
}
|
||||
}
|
||||
|
||||
private parseAndSetCsrfToken(response: AxiosResponse) {
|
||||
private parseAndSetCsrfToken = (response: AxiosResponse) => {
|
||||
const token = this.parseCsrfToken(response)
|
||||
|
||||
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[
|
||||
'x-csrf-header'
|
||||
] 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 './Folder'
|
||||
export * from './Job'
|
||||
export * from './JobExecutionError'
|
||||
export * from './JobDefinition'
|
||||
export * from './JobResult'
|
||||
export * from './Link'
|
||||
export * from './LoginRequiredError'
|
||||
export * from './SASjsConfig'
|
||||
export * from './SASjsRequest'
|
||||
export * from './SASjsWaitingRequest'
|
||||
|
||||
Reference in New Issue
Block a user