diff --git a/src/SASjs.ts b/src/SASjs.ts index 8fd5cde..40a3d4e 100644 --- a/src/SASjs.ts +++ b/src/SASjs.ts @@ -24,6 +24,7 @@ import { SasjsRequestClient } from './request/SasjsRequestClient' import { JobExecutor, WebJobExecutor, + SasjsJobExecutor, ComputeJobExecutor, JesJobExecutor, Sas9JobExecutor, @@ -59,6 +60,7 @@ export default class SASjs { private authManager: AuthManager | null = null private requestClient: RequestClient | null = null private webJobExecutor: JobExecutor | null = null + private sasjsJobExecutor: JobExecutor | null = null private computeJobExecutor: JobExecutor | null = null private jesJobExecutor: JobExecutor | null = null private sas9JobExecutor: JobExecutor | null = null @@ -102,10 +104,14 @@ export default class SASjs { * @param code - a string of code from the file to run. * @param authConfig - (optional) a valid client, secret, refresh and access tokens that are authorised to execute scripts. */ - public async executeScriptSASjs(code: string, authConfig?: AuthConfig) { + public async executeScriptSASjs( + code: string, + runTime?: string, + authConfig?: AuthConfig + ) { this.isMethodSupported('executeScriptSASJS', [ServerType.Sasjs]) - return await this.sasJSApiClient?.executeScript(code, authConfig) + return await this.sasJSApiClient?.executeScript(code, runTime, authConfig) } /** @@ -691,7 +697,7 @@ export default class SASjs { // status is true if the data passes validation checks above if (validationResult.status) { if (config.serverType === ServerType.Sasjs) { - return await this.webJobExecutor!.execute( + return await this.sasjsJobExecutor!.execute( sasJob, data, config, @@ -1049,6 +1055,12 @@ export default class SASjs { this.sasViyaApiClient! ) + this.sasjsJobExecutor = new SasjsJobExecutor( + this.sasjsConfig.serverUrl, + this.jobsPath, + this.requestClient + ) + this.sas9JobExecutor = new Sas9JobExecutor( this.sasjsConfig.serverUrl, this.sasjsConfig.serverType!, diff --git a/src/SASjsApiClient.ts b/src/SASjsApiClient.ts index e0d8955..c79572c 100644 --- a/src/SASjsApiClient.ts +++ b/src/SASjsApiClient.ts @@ -3,7 +3,7 @@ import { ExecutionQuery } from './types' import { RequestClient } from './request/RequestClient' import { getAccessTokenForSasjs } from './auth/getAccessTokenForSasjs' import { refreshTokensForSasjs } from './auth/refreshTokensForSasjs' -import { parseWeboutResponse } from './utils' +import { parseWeboutResponse, SASJS_LOGS_SEPARATOR } from './utils' import { getTokens } from './auth/getTokens' export class SASjsApiClient { @@ -64,9 +64,14 @@ export class SASjsApiClient { /** * Executes code on a SASJS server. * @param code - a string of code to execute. + * @param runTime - a string to representing runTime for code execution * @param authConfig - an object for authentication. */ - public async executeScript(code: string, authConfig?: AuthConfig) { + public async executeScript( + code: string, + runTime: string = 'sas', + authConfig?: AuthConfig + ) { let access_token = (authConfig || {}).access_token if (authConfig) { ;({ access_token } = await getTokens( @@ -79,13 +84,9 @@ export class SASjsApiClient { let parsedSasjsServerLog = '' await this.requestClient - .post('SASjsApi/code/execute', { code }, access_token) + .post('SASjsApi/code/execute', { code, runTime }, access_token) .then((res: any) => { - if (res.result?.log) { - parsedSasjsServerLog = res.result.log - .map((logLine: any) => logLine.line) - .join('\n') - } + if (res.log) parsedSasjsServerLog = res.log }) .catch((err) => { parsedSasjsServerLog = err diff --git a/src/job-execution/FileUploader.ts b/src/job-execution/FileUploader.ts index 13aa107..535c82a 100644 --- a/src/job-execution/FileUploader.ts +++ b/src/job-execution/FileUploader.ts @@ -1,7 +1,8 @@ import { getValidJson, parseSasViyaDebugResponse, - parseWeboutResponse + parseWeboutResponse, + SASJS_LOGS_SEPARATOR } from '../utils' import { UploadFile } from '../types/UploadFile' import { @@ -99,21 +100,8 @@ export class FileUploader extends BaseJobExecutor { ? parseWeboutResponse(res.result, uploadUrl) : res.result break - case ServerType.Sasjs: - if (typeof res.result._webout === 'object') { - jsonResponse = res.result._webout - } else { - const webout = parseWeboutResponse( - res.result._webout, - uploadUrl - ) - jsonResponse = getValidJson(webout) - } - break } - } else if (this.serverType === ServerType.Sasjs) { - jsonResponse = getValidJson(res.result._webout) - } else { + } else if (this.serverType !== ServerType.Sasjs) { jsonResponse = typeof res.result === 'string' ? getValidJson(res.result) diff --git a/src/job-execution/JobExecutor.ts b/src/job-execution/JobExecutor.ts index 19288bb..3e32a55 100644 --- a/src/job-execution/JobExecutor.ts +++ b/src/job-execution/JobExecutor.ts @@ -1,6 +1,6 @@ import { AuthConfig, ServerType } from '@sasjs/utils/types' import { ExtraResponseAttributes } from '@sasjs/utils/types' -import { asyncForEach } from '../utils' +import { asyncForEach, isRelativePath } from '../utils' export type ExecuteFunction = () => Promise @@ -45,4 +45,17 @@ export abstract class BaseJobExecutor implements JobExecutor { protected appendWaitingRequest(request: ExecuteFunction) { this.waitingRequests.push(request) } + + protected getRequestParams(config: any): any { + const requestParams: any = {} + + if (config.debug) { + requestParams['_omittextlog'] = 'false' + requestParams['_omitsessionresults'] = 'false' + + requestParams['_debug'] = 131 + } + + return requestParams + } } diff --git a/src/job-execution/Sas9JobExecutor.ts b/src/job-execution/Sas9JobExecutor.ts index facc35c..8a8a6fb 100644 --- a/src/job-execution/Sas9JobExecutor.ts +++ b/src/job-execution/Sas9JobExecutor.ts @@ -102,7 +102,7 @@ export class Sas9JobExecutor extends BaseJobExecutor { return requestPromise } - private getRequestParams(config: any): any { + protected getRequestParams(config: any): any { const requestParams: any = {} if (config.debug) { diff --git a/src/job-execution/SasjsJobExecutor.ts b/src/job-execution/SasjsJobExecutor.ts new file mode 100644 index 0000000..d9fd230 --- /dev/null +++ b/src/job-execution/SasjsJobExecutor.ts @@ -0,0 +1,141 @@ +import * as NodeFormData from 'form-data' +import { + AuthConfig, + ExtraResponseAttributes, + ServerType +} from '@sasjs/utils/types' +import { + ErrorResponse, + JobExecutionError, + LoginRequiredError +} from '../types/errors' +import { generateFileUploadForm } from '../file/generateFileUploadForm' + +import { RequestClient } from '../request/RequestClient' + +import { isRelativePath, appendExtraResponseAttributes } from '../utils' +import { BaseJobExecutor } from './JobExecutor' + +export class SasjsJobExecutor extends BaseJobExecutor { + constructor( + serverUrl: string, + private jobsPath: string, + private requestClient: RequestClient + ) { + super(serverUrl, ServerType.Sasjs) + } + + async execute( + sasJob: string, + data: any, + config: any, + loginRequiredCallback?: any, + authConfig?: AuthConfig, + extraResponseAttributes: ExtraResponseAttributes[] = [] + ) { + const loginCallback = loginRequiredCallback + const program = + isRelativePath(sasJob) && config.appLoc + ? config.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '') + : sasJob + + let apiUrl = `${config.serverUrl}${this.jobsPath}/?${'_program=' + program}` + + let requestParams = { + ...this.getRequestParams(config) + } + + /** + * Use the available form data object (FormData in Browser, NodeFormData in + * Node) + */ + let formData = + typeof FormData === 'undefined' ? new NodeFormData() : new FormData() + + if (data) { + // file upload approach + try { + formData = generateFileUploadForm(formData, data) + } catch (e: any) { + return Promise.reject(new ErrorResponse(e?.message, e)) + } + } + + for (const key in requestParams) { + if (requestParams.hasOwnProperty(key)) { + formData.append(key, requestParams[key]) + } + } + + /* The NodeFormData object does not set the request header - so, set it */ + const contentType = + formData instanceof NodeFormData && typeof FormData === 'undefined' + ? `multipart/form-data; boundary=${formData.getBoundary()}` + : undefined + + const requestPromise = new Promise((resolve, reject) => { + this.requestClient!.post( + apiUrl, + formData, + authConfig?.access_token, + contentType + ) + .then(async (res: any) => { + if (Object.entries(res.result).length < 1) { + throw new JobExecutionError( + 0, + `No webout was returned by job ${program}. Please check the SAS log for more info.`, + res.log + ) + } + + this.requestClient!.appendRequest(res, sasJob, config.debug) + + const responseObject = appendExtraResponseAttributes( + res, + extraResponseAttributes + ) + resolve(responseObject) + }) + .catch(async (e: Error) => { + if (e instanceof JobExecutionError) { + this.requestClient!.appendRequest(e, sasJob, config.debug) + reject(new ErrorResponse(e?.message, e)) + } + + if (e instanceof LoginRequiredError) { + if (!loginRequiredCallback) { + reject( + new ErrorResponse( + 'Request is not authenticated. Make sure .env file exists with valid credentials.', + e + ) + ) + } + + this.appendWaitingRequest(() => { + return this.execute( + sasJob, + data, + config, + loginRequiredCallback, + authConfig, + extraResponseAttributes + ).then( + (res: any) => { + resolve(res) + }, + (err: any) => { + reject(err) + } + ) + }) + + if (loginCallback) await loginCallback() + } else reject(new ErrorResponse(e?.message, e)) + }) + }) + + return requestPromise + } +} diff --git a/src/job-execution/WebJobExecutor.ts b/src/job-execution/WebJobExecutor.ts index a813794..fe52903 100644 --- a/src/job-execution/WebJobExecutor.ts +++ b/src/job-execution/WebJobExecutor.ts @@ -16,12 +16,10 @@ import { SASViyaApiClient } from '../SASViyaApiClient' import { isRelativePath, parseSasViyaDebugResponse, - appendExtraResponseAttributes, - getValidJson + appendExtraResponseAttributes } from '../utils' import { BaseJobExecutor } from './JobExecutor' import { parseWeboutResponse } from '../utils/parseWeboutResponse' -import { Server } from 'https' export interface WaitingRequstPromise { promise: Promise | null @@ -121,7 +119,6 @@ export class WebJobExecutor extends BaseJobExecutor { const stringifiedData = JSON.stringify(data) if ( config.serverType === ServerType.Sas9 || - config.serverType === ServerType.Sasjs || stringifiedData.length > 500000 || stringifiedData.includes(';') ) { @@ -164,31 +161,7 @@ export class WebJobExecutor extends BaseJobExecutor { contentType ) .then(async (res: any) => { - const parsedSasjsServerLog = - this.serverType === ServerType.Sasjs - ? res.result.log.map((logLine: any) => logLine.line).join('\n') - : res.result.log - - const resObj = - this.serverType === ServerType.Sasjs - ? { - result: res.result._webout, - log: parsedSasjsServerLog - } - : res - - if ( - this.serverType === ServerType.Sasjs && - res.result._webout.length < 1 - ) { - throw new JobExecutionError( - 0, - `No webout was returned by job ${program}. Server type is SASJS and the calling function is WebJobExecutor. Please check the SAS log for more info.`, - parsedSasjsServerLog - ) - } - - this.requestClient!.appendRequest(resObj, sasJob, config.debug) + this.requestClient!.appendRequest(res, sasJob, config.debug) let jsonResponse = res.result @@ -207,21 +180,11 @@ export class WebJobExecutor extends BaseJobExecutor { ? parseWeboutResponse(res.result, apiUrl) : res.result break - case ServerType.Sasjs: - if (typeof res.result._webout === 'object') { - jsonResponse = res.result._webout - } else { - const webout = parseWeboutResponse(res.result._webout, apiUrl) - jsonResponse = getValidJson(webout) - } - break } - } else if (this.serverType === ServerType.Sasjs) { - jsonResponse = getValidJson(res.result._webout) } const responseObject = appendExtraResponseAttributes( - { result: jsonResponse, log: parsedSasjsServerLog }, + { result: jsonResponse, log: res.log }, extraResponseAttributes ) resolve(responseObject) @@ -261,9 +224,7 @@ export class WebJobExecutor extends BaseJobExecutor { }) if (loginCallback) await loginCallback() - } else { - reject(new ErrorResponse(e?.message, e)) - } + } else reject(new ErrorResponse(e?.message, e)) }) }) @@ -301,39 +262,4 @@ export class WebJobExecutor extends BaseJobExecutor { } 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 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 index f0025cb..de1b538 100644 --- a/src/job-execution/index.ts +++ b/src/job-execution/index.ts @@ -4,3 +4,4 @@ export * from './JesJobExecutor' export * from './JobExecutor' export * from './Sas9JobExecutor' export * from './WebJobExecutor' +export * from './SasjsJobExecutor' diff --git a/src/request/RequestClient.ts b/src/request/RequestClient.ts index 6e1ca66..e1c42e0 100644 --- a/src/request/RequestClient.ts +++ b/src/request/RequestClient.ts @@ -133,26 +133,6 @@ export class RequestClient implements HttpClient { } else { sasWork = response.log } - } else if (response?.result?.log) { - //In this scenario we know we got the response from SASJS server - //Log is array of `{ line: '' }` so we need to convert it back to text - //To be able to parse it with current functions. - let log: string = '' - - if (typeof log !== 'string') { - log = response.result.log - .map((logLine: any) => logLine.line) - .join('\n') - } - - sourceCode = parseSourceCode(log) - generatedCode = parseGeneratedCode(log) - - if (response?.result?._webout) { - sasWork = response.result._webout.WORK - } else { - sasWork = log - } } else if (response?.result) { // We parse only if it's a string, otherwise it would throw error if (typeof response.result === 'string') { diff --git a/src/request/SasjsRequestClient.ts b/src/request/SasjsRequestClient.ts index ad338a6..c0df4f2 100644 --- a/src/request/SasjsRequestClient.ts +++ b/src/request/SasjsRequestClient.ts @@ -1,9 +1,12 @@ import { RequestClient } from './RequestClient' +import { AxiosResponse } from 'axios' +import { SASJS_LOGS_SEPARATOR, getValidJson } from '../utils' /** * Specific request client for SASJS. * Append tokens in headers. */ + export class SasjsRequestClient extends RequestClient { getHeaders = (accessToken: string | undefined, contentType: string) => { const headers: any = {} @@ -20,4 +23,32 @@ export class SasjsRequestClient extends RequestClient { return headers } + + protected parseResponse(response: AxiosResponse) { + const etag = response?.headers ? response.headers['etag'] : '' + let parsedResponse + let log + + try { + if (typeof response.data === 'string') { + parsedResponse = JSON.parse(response.data) + } else { + parsedResponse = response.data + } + } catch { + if (response.data.includes(SASJS_LOGS_SEPARATOR)) { + parsedResponse = getValidJson( + response.data.split(SASJS_LOGS_SEPARATOR)[0] + ) + log = response.data.split(SASJS_LOGS_SEPARATOR)[1] + } else parsedResponse = response.data + } + + return { + result: parsedResponse as T, + log, + etag, + status: response.status + } + } } diff --git a/src/utils/constants.ts b/src/utils/constants.ts new file mode 100644 index 0000000..943e05a --- /dev/null +++ b/src/utils/constants.ts @@ -0,0 +1,2 @@ +export const SASJS_LOGS_SEPARATOR = + 'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784' diff --git a/src/utils/index.ts b/src/utils/index.ts index 62ceb97..4f02fc8 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -2,6 +2,7 @@ export * from './appendExtraResponseAttributes' export * from './asyncForEach' export * from './compareTimestamps' export * from './convertToCsv' +export * from './constants' export * from './createAxiosInstance' export * from './delay' export * from './fetchLogByChunks'