1
0
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:
Krishna Acondy
2021-01-27 20:30:13 +00:00
parent e0d85f458b
commit d7ecaf5932
18 changed files with 798 additions and 714 deletions

View File

@@ -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 = [

View File

@@ -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(

View File

@@ -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.

View File

@@ -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(

View File

@@ -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

View 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
}

View 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 }
}

View 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)
}
}
}

View 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)
}
}
}

View 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
}

View 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(', ')
}
}

View File

@@ -0,0 +1,5 @@
export * from './ComputeJobExecutor'
export * from './JesJobExecutor'
export * from './JobExecutor'
export * from './parseSasWork'
export * from './WebJobExecutor'

View 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.')
}
})
}

View File

@@ -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
}
}
}

View 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)
}
}

View 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)
}
}

View 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)
}
}

View File

@@ -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'