1
0
mirror of https://github.com/sasjs/adapter.git synced 2026-01-07 04:20:05 +00:00

Compare commits

...

18 Commits

Author SHA1 Message Date
Muhammad Saad
8a883c09f6 Merge pull request #549 from sasjs/improvements-file-uploader
fix: FileUploader extends BaseJobExecutor
2021-09-14 18:47:26 +05:00
Saad Jutt
15ff90025a fix(fileUploader): added loginCallback 2021-09-14 05:58:40 +05:00
Saad Jutt
10cf4998f5 fix(loginPrompt): z-index added 2021-09-13 18:02:26 +05:00
Saad Jutt
f714f20f29 chore(fileUploader): support loginCallback and re-submit request 2021-09-13 17:45:35 +05:00
Saad Jutt
19adcc3115 chore: FileUploader extends BaseJobExecutor 2021-09-13 17:42:41 +05:00
Muhammad Saad
bb6b25bac7 Merge pull request #548 from sasjs/code-improvements
chore(imprvements): code changes for fileUploader
2021-09-13 17:40:00 +05:00
Saad Jutt
ec9dbd7ad6 chore(imprvements): code changes for fileUploader 2021-09-13 07:30:26 +05:00
Allan Bowe
2cfba99bda Merge pull request #540 from sasjs/issue-532
fix: move SASjsRequest array from BaseJobExecutor class to RequestClient class
2021-09-11 17:17:33 +03:00
Krishna Acondy
2ccc7b5499 chore(request-client): rename method 2021-09-09 07:41:20 +01:00
9c099b899c fix: If the request client has already been instantiated, update config 2021-09-09 11:03:48 +05:00
d52c5b26a0 chore: merge master into issue-532 2021-09-08 15:16:06 +05:00
46ef7b19f6 fix: before instantiating RequestClient check if its already instantiated 2021-09-08 15:02:22 +05:00
73f50c0435 chore: merge master into issue-532 2021-09-06 13:39:58 +05:00
5ee57f3d07 chore: added jsdoc header 2021-09-03 14:54:35 +05:00
146b0715bf fix: set debug: false in config of fileUploader tests 2021-09-03 13:57:39 +05:00
dfc1d567a5 fix: append sasjs requests array from uploadFile 2021-09-03 13:55:49 +05:00
779200f5fc fix: throw error if null or undefined is passed to getValidJson 2021-09-03 13:54:02 +05:00
cf4c4cfca9 fix: move SASjsRequest array from BaseJobExecutor class to RequestClient class 2021-09-03 13:51:58 +05:00
15 changed files with 382 additions and 256 deletions

View File

@@ -1,98 +0,0 @@
import { isUrl, getValidJson, parseSasViyaDebugResponse } from './utils'
import { UploadFile } from './types/UploadFile'
import { ErrorResponse, LoginRequiredError } from './types/errors'
import { RequestClient } from './request/RequestClient'
import { ServerType } from '@sasjs/utils/types'
import SASjs from './SASjs'
import { Server } from 'https'
import { SASjsConfig } from './types'
import { config } from 'process'
export class FileUploader {
constructor(
private sasjsConfig: SASjsConfig,
private jobsPath: string,
private requestClient: RequestClient
) {
if (this.sasjsConfig.serverUrl) isUrl(this.sasjsConfig.serverUrl)
}
public uploadFile(sasJob: string, files: UploadFile[], params: any) {
if (files?.length < 1)
return Promise.reject(
new ErrorResponse('At least one file must be provided.')
)
if (!sasJob || sasJob === '')
return Promise.reject(new ErrorResponse('sasJob must be provided.'))
let paramsString = ''
for (let param in params) {
if (params.hasOwnProperty(param)) {
paramsString += `&${param}=${params[param]}`
}
}
const program = this.sasjsConfig.appLoc
? this.sasjsConfig.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
: sasJob
const uploadUrl = `${this.jobsPath}/?${
'_program=' + program
}${paramsString}`
const formData = new FormData()
for (let file of files) {
formData.append('file', file.file, file.fileName)
}
const csrfToken = this.requestClient.getCsrfToken('file')
if (csrfToken) formData.append('_csrf', csrfToken.value)
if (this.sasjsConfig.debug) formData.append('_debug', '131')
if (
this.sasjsConfig.serverType === ServerType.SasViya &&
this.sasjsConfig.contextName
)
formData.append('_contextname', this.sasjsConfig.contextName)
const headers = {
'cache-control': 'no-cache',
Accept: '*/*',
'Content-Type': 'text/plain'
}
// currently only web approach is supported for file upload
// therefore log is part of response with debug enabled and must be parsed
return this.requestClient
.post(uploadUrl, formData, undefined, 'application/json', headers)
.then(async (res) => {
if (
this.sasjsConfig.serverType === ServerType.SasViya &&
this.sasjsConfig.debug
) {
const jsonResponse = await parseSasViyaDebugResponse(
res.result as string,
this.requestClient,
this.sasjsConfig.serverUrl
)
return jsonResponse
}
return typeof res.result === 'string'
? getValidJson(res.result)
: res.result
//TODO: append to SASjs requests
})
.catch((err: Error) => {
if (err instanceof LoginRequiredError) {
return Promise.reject(
new ErrorResponse('You must be logged in to upload a file.', err)
)
}
return Promise.reject(
new ErrorResponse('File upload request failed.', err)
)
})
}
}

View File

@@ -51,6 +51,16 @@ export class SASViyaApiClient {
) )
private folderMap = new Map<string, Job[]>() private folderMap = new Map<string, Job[]>()
/**
* A helper method used to call appendRequest method of RequestClient
* @param response - response from sasjs request
* @param program - name of program
* @param debug - a boolean that indicates whether debug was enabled or not
*/
public appendRequest(response: any, program: string, debug: boolean) {
this.requestClient!.appendRequest(response, program, debug)
}
public get debug() { public get debug() {
return this._debug return this._debug
} }

View File

@@ -8,7 +8,6 @@ import {
} from './types' } from './types'
import { SASViyaApiClient } from './SASViyaApiClient' import { SASViyaApiClient } from './SASViyaApiClient'
import { SAS9ApiClient } from './SAS9ApiClient' import { SAS9ApiClient } from './SAS9ApiClient'
import { FileUploader } from './FileUploader'
import { AuthManager } from './auth' import { AuthManager } from './auth'
import { import {
ServerType, ServerType,
@@ -22,7 +21,8 @@ import {
WebJobExecutor, WebJobExecutor,
ComputeJobExecutor, ComputeJobExecutor,
JesJobExecutor, JesJobExecutor,
Sas9JobExecutor Sas9JobExecutor,
FileUploader
} from './job-execution' } from './job-execution'
import { ErrorResponse } from './types/errors' import { ErrorResponse } from './types/errors'
import { LoginOptions, LoginResult } from './types/Login' import { LoginOptions, LoginResult } from './types/Login'
@@ -507,7 +507,7 @@ export default class SASjs {
...this.sasjsConfig, ...this.sasjsConfig,
...config ...config
} }
await this.setupConfiguration() this.setupConfiguration()
} }
/** /**
@@ -571,24 +571,32 @@ export default class SASjs {
* Process). Is prepended at runtime with the value of `appLoc`. * Process). Is prepended at runtime with the value of `appLoc`.
* @param files - array of files to be uploaded, including File object and file name. * @param files - array of files to be uploaded, including File object and file name.
* @param params - request URL parameters. * @param params - request URL parameters.
* @param overrideSasjsConfig - object to override existing config (optional) * @param config - provide any changes to the config here, for instance to
* enable/disable `debug`. Any change provided will override the global config,
* for that particular function call.
* @param loginRequiredCallback - a function that is called if the
* user is not logged in (eg to display a login form). The request will be
* resubmitted after successful login.
*/ */
public uploadFile( public async uploadFile(
sasJob: string, sasJob: string,
files: UploadFile[], files: UploadFile[],
params: any, params: { [key: string]: any } | null,
overrideSasjsConfig?: any config: { [key: string]: any } = {},
loginRequiredCallback?: () => any
) { ) {
const fileUploader = overrideSasjsConfig config = {
? new FileUploader( ...this.sasjsConfig,
{ ...this.sasjsConfig, ...overrideSasjsConfig }, ...config
this.jobsPath, }
this.requestClient! const data = { files, params }
)
: this.fileUploader ||
new FileUploader(this.sasjsConfig, this.jobsPath, this.requestClient!)
return fileUploader.uploadFile(sasJob, files, params) return await this.fileUploader!.execute(
sasJob,
data,
config,
loginRequiredCallback
)
} }
/** /**
@@ -874,6 +882,7 @@ export default class SASjs {
await this.webJobExecutor?.resendWaitingRequests() await this.webJobExecutor?.resendWaitingRequests()
await this.computeJobExecutor?.resendWaitingRequests() await this.computeJobExecutor?.resendWaitingRequests()
await this.jesJobExecutor?.resendWaitingRequests() await this.jesJobExecutor?.resendWaitingRequests()
await this.fileUploader?.resendWaitingRequests()
} }
/** /**
@@ -905,20 +914,18 @@ export default class SASjs {
}) })
} }
/**
* this method returns an array of SASjsRequest
* @returns SASjsRequest[]
*/
public getSasRequests() { public getSasRequests() {
const requests = [ const requests = [...this.requestClient!.getRequests()]
...this.webJobExecutor!.getRequests(),
...this.computeJobExecutor!.getRequests(),
...this.jesJobExecutor!.getRequests()
]
const sortedRequests = requests.sort(compareTimestamps) const sortedRequests = requests.sort(compareTimestamps)
return sortedRequests return sortedRequests
} }
public clearSasRequests() { public clearSasRequests() {
this.webJobExecutor!.clearRequests() this.requestClient!.clearRequests()
this.computeJobExecutor!.clearRequests()
this.jesJobExecutor!.clearRequests()
} }
private setupConfiguration() { private setupConfiguration() {
@@ -941,10 +948,17 @@ export default class SASjs {
this.sasjsConfig.serverUrl = this.sasjsConfig.serverUrl.slice(0, -1) this.sasjsConfig.serverUrl = this.sasjsConfig.serverUrl.slice(0, -1)
} }
this.requestClient = new RequestClient( if (!this.requestClient) {
this.sasjsConfig.serverUrl, this.requestClient = new RequestClient(
this.sasjsConfig.allowInsecureRequests this.sasjsConfig.serverUrl,
) this.sasjsConfig.allowInsecureRequests
)
} else {
this.requestClient.setConfig(
this.sasjsConfig.serverUrl,
this.sasjsConfig.allowInsecureRequests
)
}
this.jobsPath = this.jobsPath =
this.sasjsConfig.serverType === ServerType.SasViya this.sasjsConfig.serverType === ServerType.SasViya
@@ -986,7 +1000,8 @@ export default class SASjs {
} }
this.fileUploader = new FileUploader( this.fileUploader = new FileUploader(
this.sasjsConfig, this.sasjsConfig.serverUrl,
this.sasjsConfig.serverType!,
this.jobsPath, this.jobsPath,
this.requestClient this.requestClient
) )

View File

@@ -35,14 +35,12 @@ export class ComputeJobExecutor extends BaseJobExecutor {
expectWebout expectWebout
) )
.then((response) => { .then((response) => {
this.appendRequest(response, sasJob, config.debug) this.sasViyaApiClient.appendRequest(response, sasJob, config.debug)
resolve(response.result) resolve(response.result)
}) })
.catch(async (e: Error) => { .catch(async (e: Error) => {
if (e instanceof ComputeJobExecutionError) { if (e instanceof ComputeJobExecutionError) {
this.appendRequest(e, sasJob, config.debug) this.sasViyaApiClient.appendRequest(e, sasJob, config.debug)
reject(new ErrorResponse(e?.message, e)) reject(new ErrorResponse(e?.message, e))
} }

View File

@@ -0,0 +1,143 @@
import {
getValidJson,
parseSasViyaDebugResponse,
parseWeboutResponse
} from '../utils'
import { UploadFile } from '../types/UploadFile'
import {
ErrorResponse,
JobExecutionError,
LoginRequiredError
} from '../types/errors'
import { RequestClient } from '../request/RequestClient'
import { ServerType } from '@sasjs/utils/types'
import { BaseJobExecutor } from './JobExecutor'
interface dataFileUpload {
files: UploadFile[]
params: { [key: string]: any } | null
}
export class FileUploader extends BaseJobExecutor {
constructor(
serverUrl: string,
serverType: ServerType,
private jobsPath: string,
private requestClient: RequestClient
) {
super(serverUrl, serverType)
}
public async execute(
sasJob: string,
data: any,
config: any,
loginRequiredCallback?: any
) {
const { files, params }: dataFileUpload = data
const loginCallback = loginRequiredCallback || (() => Promise.resolve())
if (!files?.length)
throw new ErrorResponse('At least one file must be provided.')
if (!sasJob || sasJob === '')
throw new ErrorResponse('sasJob must be provided.')
let paramsString = ''
for (let param in params)
if (params.hasOwnProperty(param))
paramsString += `&${param}=${params[param]}`
const program = config.appLoc
? config.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
: sasJob
const uploadUrl = `${this.jobsPath}/?${
'_program=' + program
}${paramsString}`
const formData = new FormData()
for (let file of files) {
formData.append('file', file.file, file.fileName)
}
const csrfToken = this.requestClient.getCsrfToken('file')
if (csrfToken) formData.append('_csrf', csrfToken.value)
if (config.debug) formData.append('_debug', '131')
if (config.serverType === ServerType.SasViya && config.contextName)
formData.append('_contextname', config.contextName)
const headers = {
'cache-control': 'no-cache',
Accept: '*/*',
'Content-Type': 'text/plain'
}
// currently only web approach is supported for file upload
// therefore log is part of response with debug enabled and must be parsed
const requestPromise = new Promise((resolve, reject) => {
this.requestClient
.post(uploadUrl, formData, undefined, 'application/json', headers)
.then(async (res: any) => {
this.requestClient.appendRequest(res, sasJob, config.debug)
let jsonResponse = res.result
if (config.debug) {
switch (this.serverType) {
case ServerType.SasViya:
jsonResponse = await parseSasViyaDebugResponse(
res.result,
this.requestClient,
config.serverUrl
)
break
case ServerType.Sas9:
jsonResponse =
typeof res.result === 'string'
? parseWeboutResponse(res.result, uploadUrl)
: res.result
break
}
} else {
jsonResponse =
typeof res.result === 'string'
? getValidJson(res.result)
: res.result
}
resolve(jsonResponse)
})
.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) {
this.appendWaitingRequest(() => {
return this.execute(
sasJob,
data,
config,
loginRequiredCallback
).then(
(res: any) => {
resolve(res)
},
(err: any) => {
reject(err)
}
)
})
await loginCallback()
} else {
reject(new ErrorResponse('File upload request failed.', e))
}
})
})
return requestPromise
}
}

View File

@@ -28,7 +28,7 @@ export class JesJobExecutor extends BaseJobExecutor {
this.sasViyaApiClient this.sasViyaApiClient
?.executeJob(sasJob, config.contextName, config.debug, data, authConfig) ?.executeJob(sasJob, config.contextName, config.debug, data, authConfig)
.then((response: any) => { .then((response: any) => {
this.appendRequest(response, sasJob, config.debug) this.sasViyaApiClient.appendRequest(response, sasJob, config.debug)
const responseObject = appendExtraResponseAttributes( const responseObject = appendExtraResponseAttributes(
response, response,
@@ -39,7 +39,7 @@ export class JesJobExecutor extends BaseJobExecutor {
}) })
.catch(async (e: Error) => { .catch(async (e: Error) => {
if (e instanceof JobExecutionError) { if (e instanceof JobExecutionError) {
this.appendRequest(e, sasJob, config.debug) this.sasViyaApiClient.appendRequest(e, sasJob, config.debug)
reject(new ErrorResponse(e?.message, e)) reject(new ErrorResponse(e?.message, e))
} }

View File

@@ -1,7 +1,6 @@
import { AuthConfig, ServerType } from '@sasjs/utils/types' import { AuthConfig, ServerType } from '@sasjs/utils/types'
import { SASjsRequest } from '../types'
import { ExtraResponseAttributes } from '@sasjs/utils/types' import { ExtraResponseAttributes } from '@sasjs/utils/types'
import { asyncForEach, parseGeneratedCode, parseSourceCode } from '../utils' import { asyncForEach } from '../utils'
export type ExecuteFunction = () => Promise<any> export type ExecuteFunction = () => Promise<any>
@@ -15,15 +14,12 @@ export interface JobExecutor {
extraResponseAttributes?: ExtraResponseAttributes[] extraResponseAttributes?: ExtraResponseAttributes[]
) => Promise<any> ) => Promise<any>
resendWaitingRequests: () => Promise<void> resendWaitingRequests: () => Promise<void>
getRequests: () => SASjsRequest[]
clearRequests: () => void
} }
export abstract class BaseJobExecutor implements JobExecutor { export abstract class BaseJobExecutor implements JobExecutor {
constructor(protected serverUrl: string, protected serverType: ServerType) {} constructor(protected serverUrl: string, protected serverType: ServerType) {}
private waitingRequests: ExecuteFunction[] = [] private waitingRequests: ExecuteFunction[] = []
private requests: SASjsRequest[] = []
abstract execute( abstract execute(
sasJob: string, sasJob: string,
@@ -46,54 +42,7 @@ export abstract class BaseJobExecutor implements JobExecutor {
return return
} }
getRequests = () => this.requests
clearRequests = () => {
this.requests = []
}
protected appendWaitingRequest(request: ExecuteFunction) { protected appendWaitingRequest(request: ExecuteFunction) {
this.waitingRequests.push(request) this.waitingRequests.push(request)
} }
protected appendRequest(response: any, program: string, debug: boolean) {
let sourceCode = ''
let generatedCode = ''
let sasWork = null
if (debug) {
if (response?.log) {
sourceCode = parseSourceCode(response.log)
generatedCode = parseGeneratedCode(response.log)
if (response?.result) {
sasWork = response.result.WORK
} else {
sasWork = response.log
}
} else if (response?.result) {
sourceCode = parseSourceCode(response.result)
generatedCode = parseGeneratedCode(response.result)
sasWork = response.result.WORK
}
}
const stringifiedResult =
typeof response?.result === 'string'
? response?.result
: JSON.stringify(response?.result, null, 2)
this.requests.push({
logFile: response?.log || stringifiedResult || response,
serviceLink: program,
timestamp: new Date(),
sourceCode,
generatedCode,
SASWORK: sasWork
})
if (this.requests.length > 20) {
this.requests.splice(0, 1)
}
}
} }

View File

@@ -45,7 +45,7 @@ export class Sas9JobExecutor extends BaseJobExecutor {
if (data) { if (data) {
try { try {
formData = generateFileUploadForm(formData, data) formData = generateFileUploadForm(formData, data)
} catch (e) { } catch (e: any) {
return Promise.reject(new ErrorResponse(e?.message, e)) return Promise.reject(new ErrorResponse(e?.message, e))
} }
} }

View File

@@ -90,7 +90,7 @@ export class WebJobExecutor extends BaseJobExecutor {
// file upload approach // file upload approach
try { try {
formData = generateFileUploadForm(formData, data) formData = generateFileUploadForm(formData, data)
} catch (e) { } catch (e: any) {
return Promise.reject(new ErrorResponse(e?.message, e)) return Promise.reject(new ErrorResponse(e?.message, e))
} }
} else { } else {
@@ -100,7 +100,7 @@ export class WebJobExecutor extends BaseJobExecutor {
generateTableUploadForm(formData, data) generateTableUploadForm(formData, data)
formData = newFormData formData = newFormData
requestParams = { ...requestParams, ...params } requestParams = { ...requestParams, ...params }
} catch (e) { } catch (e: any) {
return Promise.reject(new ErrorResponse(e?.message, e)) return Promise.reject(new ErrorResponse(e?.message, e))
} }
} }
@@ -115,6 +115,8 @@ export class WebJobExecutor extends BaseJobExecutor {
const requestPromise = new Promise((resolve, reject) => { const requestPromise = new Promise((resolve, reject) => {
this.requestClient!.post(apiUrl, formData, undefined) this.requestClient!.post(apiUrl, formData, undefined)
.then(async (res: any) => { .then(async (res: any) => {
this.requestClient!.appendRequest(res, sasJob, config.debug)
let jsonResponse = res.result let jsonResponse = res.result
if (config.debug) { if (config.debug) {
@@ -135,8 +137,6 @@ export class WebJobExecutor extends BaseJobExecutor {
} }
} }
this.appendRequest(res, sasJob, config.debug)
const responseObject = appendExtraResponseAttributes( const responseObject = appendExtraResponseAttributes(
{ result: jsonResponse }, { result: jsonResponse },
extraResponseAttributes extraResponseAttributes
@@ -145,8 +145,7 @@ export class WebJobExecutor extends BaseJobExecutor {
}) })
.catch(async (e: Error) => { .catch(async (e: Error) => {
if (e instanceof JobExecutionError) { if (e instanceof JobExecutionError) {
this.appendRequest(e, sasJob, config.debug) this.requestClient!.appendRequest(e, sasJob, config.debug)
reject(new ErrorResponse(e?.message, e)) reject(new ErrorResponse(e?.message, e))
} }

View File

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

View File

@@ -8,10 +8,11 @@ import {
InternalServerError, InternalServerError,
JobExecutionError JobExecutionError
} from '../types/errors' } from '../types/errors'
import { SASjsRequest } from '../types'
import { parseWeboutResponse } from '../utils/parseWeboutResponse' import { parseWeboutResponse } from '../utils/parseWeboutResponse'
import { prefixMessage } from '@sasjs/utils/error' import { prefixMessage } from '@sasjs/utils/error'
import { SAS9AuthError } from '../types/errors/SAS9AuthError' import { SAS9AuthError } from '../types/errors/SAS9AuthError'
import { getValidJson } from '../utils' import { parseGeneratedCode, parseSourceCode } from '../utils'
export interface HttpClient { export interface HttpClient {
get<T>( get<T>(
@@ -47,27 +48,18 @@ export interface HttpClient {
} }
export class RequestClient implements HttpClient { export class RequestClient implements HttpClient {
private requests: SASjsRequest[] = []
protected csrfToken: CsrfToken = { headerName: '', value: '' } protected csrfToken: CsrfToken = { headerName: '', value: '' }
protected fileUploadCsrfToken: CsrfToken | undefined protected fileUploadCsrfToken: CsrfToken | undefined
protected httpClient: AxiosInstance protected httpClient!: AxiosInstance
constructor(protected baseUrl: string, allowInsecure = false) { constructor(protected baseUrl: string, allowInsecure = false) {
const https = require('https') this.createHttpClient(baseUrl, allowInsecure)
if (allowInsecure && https.Agent) { }
this.httpClient = axios.create({
baseURL: baseUrl,
httpsAgent: new https.Agent({
rejectUnauthorized: !allowInsecure
})
})
} else {
this.httpClient = axios.create({
baseURL: baseUrl
})
}
this.httpClient.defaults.validateStatus = (status) => public setConfig(baseUrl: string, allowInsecure = false) {
status >= 200 && status < 305 this.createHttpClient(baseUrl, allowInsecure)
} }
public getCsrfToken(type: 'general' | 'file' = 'general') { public getCsrfToken(type: 'general' | 'file' = 'general') {
@@ -83,6 +75,66 @@ export class RequestClient implements HttpClient {
return this.httpClient.defaults.baseURL || '' return this.httpClient.defaults.baseURL || ''
} }
/**
* this method returns all requests, an array of SASjsRequest type
* @returns SASjsRequest[]
*/
public getRequests = () => this.requests
/**
* this method clears the requests array, i.e set to empty
*/
public clearRequests = () => {
this.requests = []
}
/**
* this method appends the response from sasjs request to requests array
* @param response - response from sasjs request
* @param program - name of program
* @param debug - a boolean that indicates whether debug was enabled or not
*/
public appendRequest(response: any, program: string, debug: boolean) {
let sourceCode = ''
let generatedCode = ''
let sasWork = null
if (debug) {
if (response?.log) {
sourceCode = parseSourceCode(response.log)
generatedCode = parseGeneratedCode(response.log)
if (response?.result) {
sasWork = response.result.WORK
} else {
sasWork = response.log
}
} else if (response?.result) {
sourceCode = parseSourceCode(response.result)
generatedCode = parseGeneratedCode(response.result)
sasWork = response.result.WORK
}
}
const stringifiedResult =
typeof response?.result === 'string'
? response?.result
: JSON.stringify(response?.result, null, 2)
this.requests.push({
logFile: response?.log || stringifiedResult || response,
serviceLink: program,
timestamp: new Date(),
sourceCode,
generatedCode,
SASWORK: sasWork
})
if (this.requests.length > 20) {
this.requests.splice(0, 1)
}
}
public async get<T>( public async get<T>(
url: string, url: string,
accessToken: string | undefined, accessToken: string | undefined,
@@ -130,7 +182,7 @@ export class RequestClient implements HttpClient {
}) })
} }
public post<T>( public async post<T>(
url: string, url: string,
data: any, data: any,
accessToken: string | undefined, accessToken: string | undefined,
@@ -234,7 +286,7 @@ export class RequestClient implements HttpClient {
result: response.data, result: response.data,
etag: response.headers['etag'] as string etag: response.headers['etag'] as string
} }
} catch (e) { } catch (e: any) {
const response = e.response as AxiosResponse const response = e.response as AxiosResponse
if (response?.status === 403 || response?.status === 449) { if (response?.status === 403 || response?.status === 449) {
this.parseAndSetFileUploadCsrfToken(response) this.parseAndSetFileUploadCsrfToken(response)
@@ -454,6 +506,25 @@ export class RequestClient implements HttpClient {
return responseToReturn return responseToReturn
} }
private createHttpClient(baseUrl: string, allowInsecure = false) {
const https = require('https')
if (allowInsecure && https.Agent) {
this.httpClient = axios.create({
baseURL: baseUrl,
httpsAgent: new https.Agent({
rejectUnauthorized: !allowInsecure
})
})
} else {
this.httpClient = axios.create({
baseURL: baseUrl
})
}
this.httpClient.defaults.validateStatus = (status) =>
status >= 200 && status < 305
}
} }
export const throwIfError = (response: AxiosResponse) => { export const throwIfError = (response: AxiosResponse) => {

View File

@@ -2,7 +2,7 @@
* @jest-environment jsdom * @jest-environment jsdom
*/ */
import { FileUploader } from '../FileUploader' import { FileUploader } from '../job-execution/FileUploader'
import { SASjsConfig, UploadFile } from '../types' import { SASjsConfig, UploadFile } from '../types'
import { RequestClient } from '../request/RequestClient' import { RequestClient } from '../request/RequestClient'
import axios from 'axios' import axios from 'axios'
@@ -34,60 +34,71 @@ const prepareFilesAndParams = () => {
describe('FileUploader', () => { describe('FileUploader', () => {
const config: SASjsConfig = { const config: SASjsConfig = {
...new SASjsConfig(), ...new SASjsConfig(),
appLoc: '/sample/apploc' appLoc: '/sample/apploc',
debug: false
} }
const fileUploader = new FileUploader( const fileUploader = new FileUploader(
config, config.serverUrl,
config.serverType!,
'/jobs/path', '/jobs/path',
new RequestClient('https://sample.server.com') new RequestClient('https://sample.server.com')
) )
it('should upload successfully', async () => { it('should upload successfully', async () => {
const sasJob = 'test/upload' const sasJob = 'test/upload'
const { files, params } = prepareFilesAndParams() const data = prepareFilesAndParams()
mockedAxios.post.mockImplementation(() => mockedAxios.post.mockImplementation(() =>
Promise.resolve({ data: sampleResponse }) Promise.resolve({ data: sampleResponse })
) )
const res = await fileUploader.uploadFile(sasJob, files, params) const res = await fileUploader.execute(sasJob, data, config)
expect(res).toEqual(JSON.parse(sampleResponse)) expect(res).toEqual(JSON.parse(sampleResponse))
}) })
it('should upload successfully when login is required', async () => {
mockedAxios.post
.mockImplementationOnce(() =>
Promise.resolve({ data: '<form action="Logon">' })
)
.mockImplementationOnce(() => Promise.resolve({ data: sampleResponse }))
const loginCallback = jest.fn().mockImplementation(async () => {
await fileUploader.resendWaitingRequests()
Promise.resolve()
})
const sasJob = 'test'
const data = prepareFilesAndParams()
const res = await fileUploader.execute(sasJob, data, config, loginCallback)
expect(res).toEqual(JSON.parse(sampleResponse))
expect(mockedAxios.post).toHaveBeenCalledTimes(2)
expect(loginCallback).toHaveBeenCalled()
})
it('should an error when no files are provided', async () => { it('should an error when no files are provided', async () => {
const sasJob = 'test/upload' const sasJob = 'test/upload'
const files: UploadFile[] = [] const files: UploadFile[] = []
const params = { table: 'libtable' } const params = { table: 'libtable' }
const err = await fileUploader const res: any = await fileUploader
.uploadFile(sasJob, files, params) .execute(sasJob, files, params, config)
.catch((err: any) => err) .catch((err: any) => err)
expect(err.error.message).toEqual('At least one file must be provided.') expect(res.error.message).toEqual('At least one file must be provided.')
}) })
it('should throw an error when no sasJob is provided', async () => { it('should throw an error when no sasJob is provided', async () => {
const sasJob = '' const sasJob = ''
const { files, params } = prepareFilesAndParams() const data = prepareFilesAndParams()
const err = await fileUploader const res: any = await fileUploader
.uploadFile(sasJob, files, params) .execute(sasJob, data, config)
.catch((err: any) => err) .catch((err: any) => err)
expect(err.error.message).toEqual('sasJob must be provided.') expect(res.error.message).toEqual('sasJob must be provided.')
})
it('should throw an error when login is required', async () => {
mockedAxios.post.mockImplementation(() =>
Promise.resolve({ data: '<form action="Logon">' })
)
const sasJob = 'test'
const { files, params } = prepareFilesAndParams()
const err = await fileUploader
.uploadFile(sasJob, files, params)
.catch((err: any) => err)
expect(err.error.message).toEqual('You must be logged in to upload a file.')
}) })
it('should throw an error when invalid JSON is returned by the server', async () => { it('should throw an error when invalid JSON is returned by the server', async () => {
@@ -96,12 +107,13 @@ describe('FileUploader', () => {
) )
const sasJob = 'test' const sasJob = 'test'
const { files, params } = prepareFilesAndParams() const data = prepareFilesAndParams()
const err = await fileUploader const res: any = await fileUploader
.uploadFile(sasJob, files, params) .execute(sasJob, data, config)
.catch((err: any) => err) .catch((err: any) => err)
expect(err.error.message).toEqual('File upload request failed.')
expect(res.error.message).toEqual('File upload request failed.')
}) })
it('should throw an error when the server request fails', async () => { it('should throw an error when the server request fails', async () => {
@@ -110,11 +122,11 @@ describe('FileUploader', () => {
) )
const sasJob = 'test' const sasJob = 'test'
const { files, params } = prepareFilesAndParams() const data = prepareFilesAndParams()
const err = await fileUploader const res: any = await fileUploader
.uploadFile(sasJob, files, params) .execute(sasJob, data, config)
.catch((err: any) => err) .catch((err: any) => err)
expect(err.error.message).toEqual('File upload request failed.') expect(res.error.message).toEqual('File upload request failed.')
}) })
}) })

View File

@@ -33,4 +33,18 @@ describe('jsonValidator', () => {
} }
expect(test).toThrow(JsonParseArrayError) expect(test).toThrow(JsonParseArrayError)
}) })
it('should throw an error when null is passed', () => {
const test = () => {
getValidJson(null as any)
}
expect(test).toThrow(InvalidJsonError)
})
it('should throw an error when undefined is passed', () => {
const test = () => {
getValidJson(undefined as any)
}
expect(test).toThrow(InvalidJsonError)
})
}) })

View File

@@ -6,6 +6,8 @@ import { JsonParseArrayError, InvalidJsonError } from '../types/errors'
*/ */
export const getValidJson = (str: string | object) => { export const getValidJson = (str: string | object) => {
try { try {
if (str === null || str === undefined) throw new InvalidJsonError()
if (Array.isArray(str)) throw new JsonParseArrayError() if (Array.isArray(str)) throw new JsonParseArrayError()
if (typeof str === 'object') return str if (typeof str === 'object') return str

View File

@@ -3,6 +3,12 @@ enum domIDs {
overlay = 'sasjsAdapterLoginPromptBG', overlay = 'sasjsAdapterLoginPromptBG',
dialog = 'sasjsAdapterLoginPrompt' dialog = 'sasjsAdapterLoginPrompt'
} }
const cssPrefix = 'sasjs-adapter'
const classes = {
popUp: `${cssPrefix}popUp`,
popUpBG: `${cssPrefix}popUpBG`
}
export const openLoginPrompt = (): Promise<boolean> => { export const openLoginPrompt = (): Promise<boolean> => {
return new Promise(async (resolve) => { return new Promise(async (resolve) => {
@@ -12,11 +18,11 @@ export const openLoginPrompt = (): Promise<boolean> => {
const loginPromptBG = document.createElement('div') const loginPromptBG = document.createElement('div')
loginPromptBG.id = domIDs.overlay loginPromptBG.id = domIDs.overlay
loginPromptBG.classList.add('popUpBG') loginPromptBG.classList.add(classes.popUpBG)
const loginPrompt = document.createElement('div') const loginPrompt = document.createElement('div')
loginPrompt.id = domIDs.dialog loginPrompt.id = domIDs.dialog
loginPrompt.classList.add('popUp') loginPrompt.classList.add(classes.popUp)
const title = document.createElement('h1') const title = document.createElement('h1')
title.innerText = 'Session Expired!' title.innerText = 'Session Expired!'
@@ -63,7 +69,11 @@ const closeLoginPrompt = () => {
} }
const cssContent = ` const cssContent = `
.popUp { .${classes.popUpBG} ,
.${classes.popUp} {
z-index: 10000;
}
.${classes.popUp} {
box-sizing: border-box; box-sizing: border-box;
-webkit-box-sizing: border-box; -webkit-box-sizing: border-box;
-moz-box-sizing: border-box; -moz-box-sizing: border-box;
@@ -86,7 +96,7 @@ const cssContent = `
max-height: 300px; max-height: 300px;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
} }
.popUp > h1 { .${classes.popUp} > h1 {
box-sizing: border-box; box-sizing: border-box;
-webkit-box-sizing: border-box; -webkit-box-sizing: border-box;
-moz-box-sizing: border-box; -moz-box-sizing: border-box;
@@ -101,7 +111,7 @@ const cssContent = `
border-width: 5px; border-width: 5px;
border-color: black; border-color: black;
} }
.popUp > div { .${classes.popUp} > div {
width: 100%; width: 100%;
height: calc(100% -108px); height: calc(100% -108px);
margin: 0; margin: 0;
@@ -116,7 +126,7 @@ const cssContent = `
border-style: none none solid none; border-style: none none solid none;
overflow: auto; overflow: auto;
} }
.popUp > div > span { .${classes.popUp} > div > span {
display: table-cell; display: table-cell;
box-sizing: border-box; box-sizing: border-box;
-webkit-box-sizing: border-box; -webkit-box-sizing: border-box;
@@ -128,13 +138,13 @@ const cssContent = `
vertical-align: middle; vertical-align: middle;
border-style: none; border-style: none;
} }
.popUp .cancel { .${classes.popUp} .cancel {
float: left; float: left;
} }
.popUp .confirm { .${classes.popUp} .confirm {
float: right; float: right;
} }
.popUp > button { .${classes.popUp} > button {
box-sizing: border-box; box-sizing: border-box;
-webkit-box-sizing: border-box; -webkit-box-sizing: border-box;
-moz-box-sizing: border-box; -moz-box-sizing: border-box;
@@ -148,10 +158,10 @@ const cssContent = `
height: 50px; height: 50px;
background: rgba(1, 1, 1, 0.2); background: rgba(1, 1, 1, 0.2);
} }
.popUp > button:hover { .${classes.popUp} > button:hover {
background: rgba(0, 0, 0, 0.2); background: rgba(0, 0, 0, 0.2);
} }
.popUpBG { .${classes.popUpBG} {
display: block; display: block;
position: fixed; position: fixed;
top: 0; top: 0;