diff --git a/src/ContextManager.ts b/src/ContextManager.ts
index c9a897c..bee8a5d 100644
--- a/src/ContextManager.ts
+++ b/src/ContextManager.ts
@@ -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 = [
diff --git a/src/FileUploader.ts b/src/FileUploader.ts
index 05f2453..0f41b49 100644
--- a/src/FileUploader.ts
+++ b/src/FileUploader.ts
@@ -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(
diff --git a/src/SASViyaApiClient.ts b/src/SASViyaApiClient.ts
index 01c1350..1661e5d 100644
--- a/src/SASViyaApiClient.ts
+++ b/src/SASViyaApiClient.ts
@@ -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.
diff --git a/src/SASjs.ts b/src/SASjs.ts
index 945a7a1..3f4755c 100644
--- a/src/SASjs.ts
+++ b/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(
- '')[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(
diff --git a/src/SessionManager.ts b/src/SessionManager.ts
index 54d1476..7b5bfcd 100644
--- a/src/SessionManager.ts
+++ b/src/SessionManager.ts
@@ -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
diff --git a/src/file/generateFileUploadForm.ts b/src/file/generateFileUploadForm.ts
new file mode 100644
index 0000000..5e2cdcc
--- /dev/null
+++ b/src/file/generateFileUploadForm.ts
@@ -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
+}
diff --git a/src/file/generateTableUploadForm.ts b/src/file/generateTableUploadForm.ts
new file mode 100644
index 0000000..140cae4
--- /dev/null
+++ b/src/file/generateTableUploadForm.ts
@@ -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 }
+}
diff --git a/src/job-execution/ComputeJobExecutor.ts b/src/job-execution/ComputeJobExecutor.ts
new file mode 100644
index 0000000..9c56762
--- /dev/null
+++ b/src/job-execution/ComputeJobExecutor.ts
@@ -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)
+ }
+ }
+}
diff --git a/src/job-execution/JesJobExecutor.ts b/src/job-execution/JesJobExecutor.ts
new file mode 100644
index 0000000..786da0a
--- /dev/null
+++ b/src/job-execution/JesJobExecutor.ts
@@ -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)
+ }
+ }
+}
diff --git a/src/job-execution/JobExecutor.ts b/src/job-execution/JobExecutor.ts
new file mode 100644
index 0000000..5b8b784
--- /dev/null
+++ b/src/job-execution/JobExecutor.ts
@@ -0,0 +1,17 @@
+import { SASjsRequest } from '../types'
+
+export type ExecuteFunction = () => Promise
+
+export interface JobExecutor {
+ execute: (
+ sasJob: string,
+ data: any,
+ config: any,
+ loginRequiredCallback?: any,
+ accessToken?: string
+ ) => Promise
+ waitingRequests: ExecuteFunction[]
+ resendWaitingRequests: () => Promise
+ getRequests: () => SASjsRequest[]
+ clearRequests: () => void
+}
diff --git a/src/job-execution/WebJobExecutor.ts b/src/job-execution/WebJobExecutor.ts
new file mode 100644
index 0000000..d43c32c
--- /dev/null
+++ b/src/job-execution/WebJobExecutor.ts
@@ -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(', ')
+ }
+}
diff --git a/src/job-execution/index.ts b/src/job-execution/index.ts
new file mode 100644
index 0000000..f053824
--- /dev/null
+++ b/src/job-execution/index.ts
@@ -0,0 +1,5 @@
+export * from './ComputeJobExecutor'
+export * from './JesJobExecutor'
+export * from './JobExecutor'
+export * from './parseSasWork'
+export * from './WebJobExecutor'
diff --git a/src/job-execution/parseSasWork.ts b/src/job-execution/parseSasWork.ts
new file mode 100644
index 0000000..9057073
--- /dev/null
+++ b/src/job-execution/parseSasWork.ts
@@ -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(
+ '')[0] : null
+
+ if (jsonUrl) {
+ fetch(serverUrl + jsonUrl)
+ .then((res) => res.text())
+ .then((resText) => {
+ resolve(resText)
+ })
+ } else {
+ reject('No debug info found in response.')
+ }
+ })
+}
diff --git a/src/request/client.ts b/src/request/RequestClient.ts
similarity index 66%
rename from src/request/client.ts
rename to src/request/RequestClient.ts
index a8a46eb..d609cac 100644
--- a/src/request/client.ts
+++ b/src/request/RequestClient.ts
@@ -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(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(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(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(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(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
+ }
+ }
+}
diff --git a/src/types/AuthorizeError.ts b/src/types/AuthorizeError.ts
new file mode 100644
index 0000000..694f84f
--- /dev/null
+++ b/src/types/AuthorizeError.ts
@@ -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)
+ }
+}
diff --git a/src/types/JobExecutionError.ts b/src/types/JobExecutionError.ts
new file mode 100644
index 0000000..dc542b2
--- /dev/null
+++ b/src/types/JobExecutionError.ts
@@ -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)
+ }
+}
diff --git a/src/types/LoginRequiredError.ts b/src/types/LoginRequiredError.ts
new file mode 100644
index 0000000..854c032
--- /dev/null
+++ b/src/types/LoginRequiredError.ts
@@ -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)
+ }
+}
diff --git a/src/types/index.ts b/src/types/index.ts
index 6d45331..2dc953d 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -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'