1
0
mirror of https://github.com/sasjs/adapter.git synced 2026-01-18 17:40:06 +00:00

Merge pull request #746 from sasjs/handle-updated-sasjs-response

fix: handled updated sasjs response
This commit is contained in:
Sabir Hassan
2022-08-24 21:03:17 +05:00
committed by GitHub
12 changed files with 222 additions and 126 deletions

View File

@@ -24,6 +24,7 @@ import { SasjsRequestClient } from './request/SasjsRequestClient'
import { import {
JobExecutor, JobExecutor,
WebJobExecutor, WebJobExecutor,
SasjsJobExecutor,
ComputeJobExecutor, ComputeJobExecutor,
JesJobExecutor, JesJobExecutor,
Sas9JobExecutor, Sas9JobExecutor,
@@ -59,6 +60,7 @@ export default class SASjs {
private authManager: AuthManager | null = null private authManager: AuthManager | null = null
private requestClient: RequestClient | null = null private requestClient: RequestClient | null = null
private webJobExecutor: JobExecutor | null = null private webJobExecutor: JobExecutor | null = null
private sasjsJobExecutor: JobExecutor | null = null
private computeJobExecutor: JobExecutor | null = null private computeJobExecutor: JobExecutor | null = null
private jesJobExecutor: JobExecutor | null = null private jesJobExecutor: JobExecutor | null = null
private sas9JobExecutor: 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 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. * @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]) 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 // status is true if the data passes validation checks above
if (validationResult.status) { if (validationResult.status) {
if (config.serverType === ServerType.Sasjs) { if (config.serverType === ServerType.Sasjs) {
return await this.webJobExecutor!.execute( return await this.sasjsJobExecutor!.execute(
sasJob, sasJob,
data, data,
config, config,
@@ -1049,6 +1055,12 @@ export default class SASjs {
this.sasViyaApiClient! this.sasViyaApiClient!
) )
this.sasjsJobExecutor = new SasjsJobExecutor(
this.sasjsConfig.serverUrl,
this.jobsPath,
this.requestClient
)
this.sas9JobExecutor = new Sas9JobExecutor( this.sas9JobExecutor = new Sas9JobExecutor(
this.sasjsConfig.serverUrl, this.sasjsConfig.serverUrl,
this.sasjsConfig.serverType!, this.sasjsConfig.serverType!,

View File

@@ -3,7 +3,7 @@ import { ExecutionQuery } from './types'
import { RequestClient } from './request/RequestClient' import { RequestClient } from './request/RequestClient'
import { getAccessTokenForSasjs } from './auth/getAccessTokenForSasjs' import { getAccessTokenForSasjs } from './auth/getAccessTokenForSasjs'
import { refreshTokensForSasjs } from './auth/refreshTokensForSasjs' import { refreshTokensForSasjs } from './auth/refreshTokensForSasjs'
import { parseWeboutResponse } from './utils' import { parseWeboutResponse, SASJS_LOGS_SEPARATOR } from './utils'
import { getTokens } from './auth/getTokens' import { getTokens } from './auth/getTokens'
export class SASjsApiClient { export class SASjsApiClient {
@@ -64,9 +64,14 @@ export class SASjsApiClient {
/** /**
* Executes code on a SASJS server. * Executes code on a SASJS server.
* @param code - a string of code to execute. * @param code - a string of code to execute.
* @param runTime - a string to representing runTime for code execution
* @param authConfig - an object for authentication. * @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 let access_token = (authConfig || {}).access_token
if (authConfig) { if (authConfig) {
;({ access_token } = await getTokens( ;({ access_token } = await getTokens(
@@ -79,13 +84,9 @@ export class SASjsApiClient {
let parsedSasjsServerLog = '' let parsedSasjsServerLog = ''
await this.requestClient await this.requestClient
.post('SASjsApi/code/execute', { code }, access_token) .post('SASjsApi/code/execute', { code, runTime }, access_token)
.then((res: any) => { .then((res: any) => {
if (res.result?.log) { if (res.log) parsedSasjsServerLog = res.log
parsedSasjsServerLog = res.result.log
.map((logLine: any) => logLine.line)
.join('\n')
}
}) })
.catch((err) => { .catch((err) => {
parsedSasjsServerLog = err parsedSasjsServerLog = err

View File

@@ -1,7 +1,8 @@
import { import {
getValidJson, getValidJson,
parseSasViyaDebugResponse, parseSasViyaDebugResponse,
parseWeboutResponse parseWeboutResponse,
SASJS_LOGS_SEPARATOR
} from '../utils' } from '../utils'
import { UploadFile } from '../types/UploadFile' import { UploadFile } from '../types/UploadFile'
import { import {
@@ -99,21 +100,8 @@ export class FileUploader extends BaseJobExecutor {
? parseWeboutResponse(res.result, uploadUrl) ? parseWeboutResponse(res.result, uploadUrl)
: res.result : res.result
break 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) { } else if (this.serverType !== ServerType.Sasjs) {
jsonResponse = getValidJson(res.result._webout)
} else {
jsonResponse = jsonResponse =
typeof res.result === 'string' typeof res.result === 'string'
? getValidJson(res.result) ? getValidJson(res.result)

View File

@@ -1,6 +1,6 @@
import { AuthConfig, ServerType } from '@sasjs/utils/types' import { AuthConfig, ServerType } from '@sasjs/utils/types'
import { ExtraResponseAttributes } from '@sasjs/utils/types' import { ExtraResponseAttributes } from '@sasjs/utils/types'
import { asyncForEach } from '../utils' import { asyncForEach, isRelativePath } from '../utils'
export type ExecuteFunction = () => Promise<any> export type ExecuteFunction = () => Promise<any>
@@ -45,4 +45,17 @@ export abstract class BaseJobExecutor implements JobExecutor {
protected appendWaitingRequest(request: ExecuteFunction) { protected appendWaitingRequest(request: ExecuteFunction) {
this.waitingRequests.push(request) 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
}
} }

View File

@@ -102,7 +102,7 @@ export class Sas9JobExecutor extends BaseJobExecutor {
return requestPromise return requestPromise
} }
private getRequestParams(config: any): any { protected getRequestParams(config: any): any {
const requestParams: any = {} const requestParams: any = {}
if (config.debug) { if (config.debug) {

View File

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

View File

@@ -16,12 +16,10 @@ import { SASViyaApiClient } from '../SASViyaApiClient'
import { import {
isRelativePath, isRelativePath,
parseSasViyaDebugResponse, parseSasViyaDebugResponse,
appendExtraResponseAttributes, appendExtraResponseAttributes
getValidJson
} from '../utils' } from '../utils'
import { BaseJobExecutor } from './JobExecutor' import { BaseJobExecutor } from './JobExecutor'
import { parseWeboutResponse } from '../utils/parseWeboutResponse' import { parseWeboutResponse } from '../utils/parseWeboutResponse'
import { Server } from 'https'
export interface WaitingRequstPromise { export interface WaitingRequstPromise {
promise: Promise<any> | null promise: Promise<any> | null
@@ -121,7 +119,6 @@ export class WebJobExecutor extends BaseJobExecutor {
const stringifiedData = JSON.stringify(data) const stringifiedData = JSON.stringify(data)
if ( if (
config.serverType === ServerType.Sas9 || config.serverType === ServerType.Sas9 ||
config.serverType === ServerType.Sasjs ||
stringifiedData.length > 500000 || stringifiedData.length > 500000 ||
stringifiedData.includes(';') stringifiedData.includes(';')
) { ) {
@@ -164,31 +161,7 @@ export class WebJobExecutor extends BaseJobExecutor {
contentType contentType
) )
.then(async (res: any) => { .then(async (res: any) => {
const parsedSasjsServerLog = this.requestClient!.appendRequest(res, sasJob, config.debug)
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)
let jsonResponse = res.result let jsonResponse = res.result
@@ -207,21 +180,11 @@ export class WebJobExecutor extends BaseJobExecutor {
? parseWeboutResponse(res.result, apiUrl) ? parseWeboutResponse(res.result, apiUrl)
: res.result : res.result
break 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( const responseObject = appendExtraResponseAttributes(
{ result: jsonResponse, log: parsedSasjsServerLog }, { result: jsonResponse, log: res.log },
extraResponseAttributes extraResponseAttributes
) )
resolve(responseObject) resolve(responseObject)
@@ -261,9 +224,7 @@ export class WebJobExecutor extends BaseJobExecutor {
}) })
if (loginCallback) await loginCallback() if (loginCallback) await loginCallback()
} else { } else reject(new ErrorResponse(e?.message, e))
reject(new ErrorResponse(e?.message, e))
}
}) })
}) })
@@ -301,39 +262,4 @@ export class WebJobExecutor extends BaseJobExecutor {
} }
return 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 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

@@ -4,3 +4,4 @@ export * from './JesJobExecutor'
export * from './JobExecutor' export * from './JobExecutor'
export * from './Sas9JobExecutor' export * from './Sas9JobExecutor'
export * from './WebJobExecutor' export * from './WebJobExecutor'
export * from './SasjsJobExecutor'

View File

@@ -133,26 +133,6 @@ export class RequestClient implements HttpClient {
} else { } else {
sasWork = response.log 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) { } else if (response?.result) {
// We parse only if it's a string, otherwise it would throw error // We parse only if it's a string, otherwise it would throw error
if (typeof response.result === 'string') { if (typeof response.result === 'string') {

View File

@@ -1,9 +1,12 @@
import { RequestClient } from './RequestClient' import { RequestClient } from './RequestClient'
import { AxiosResponse } from 'axios'
import { SASJS_LOGS_SEPARATOR, getValidJson } from '../utils'
/** /**
* Specific request client for SASJS. * Specific request client for SASJS.
* Append tokens in headers. * Append tokens in headers.
*/ */
export class SasjsRequestClient extends RequestClient { export class SasjsRequestClient extends RequestClient {
getHeaders = (accessToken: string | undefined, contentType: string) => { getHeaders = (accessToken: string | undefined, contentType: string) => {
const headers: any = {} const headers: any = {}
@@ -20,4 +23,32 @@ export class SasjsRequestClient extends RequestClient {
return headers return headers
} }
protected parseResponse<T>(response: AxiosResponse<any>) {
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
}
}
} }

2
src/utils/constants.ts Normal file
View File

@@ -0,0 +1,2 @@
export const SASJS_LOGS_SEPARATOR =
'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784'

View File

@@ -2,6 +2,7 @@ export * from './appendExtraResponseAttributes'
export * from './asyncForEach' export * from './asyncForEach'
export * from './compareTimestamps' export * from './compareTimestamps'
export * from './convertToCsv' export * from './convertToCsv'
export * from './constants'
export * from './createAxiosInstance' export * from './createAxiosInstance'
export * from './delay' export * from './delay'
export * from './fetchLogByChunks' export * from './fetchLogByChunks'