1
0
mirror of https://github.com/sasjs/adapter.git synced 2025-12-11 09:24:35 +00:00

Compare commits

...

7 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
10 changed files with 242 additions and 168 deletions

View File

@@ -1,99 +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) => {
this.requestClient!.appendRequest(res, sasJob, this.sasjsConfig.debug)
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

@@ -8,7 +8,6 @@ import {
} from './types'
import { SASViyaApiClient } from './SASViyaApiClient'
import { SAS9ApiClient } from './SAS9ApiClient'
import { FileUploader } from './FileUploader'
import { AuthManager } from './auth'
import {
ServerType,
@@ -22,7 +21,8 @@ import {
WebJobExecutor,
ComputeJobExecutor,
JesJobExecutor,
Sas9JobExecutor
Sas9JobExecutor,
FileUploader
} from './job-execution'
import { ErrorResponse } from './types/errors'
import { LoginOptions, LoginResult } from './types/Login'
@@ -507,7 +507,7 @@ export default class SASjs {
...this.sasjsConfig,
...config
}
await this.setupConfiguration()
this.setupConfiguration()
}
/**
@@ -571,24 +571,32 @@ export default class SASjs {
* 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 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,
files: UploadFile[],
params: any,
overrideSasjsConfig?: any
params: { [key: string]: any } | null,
config: { [key: string]: any } = {},
loginRequiredCallback?: () => any
) {
const fileUploader = overrideSasjsConfig
? new FileUploader(
{ ...this.sasjsConfig, ...overrideSasjsConfig },
this.jobsPath,
this.requestClient!
)
: this.fileUploader ||
new FileUploader(this.sasjsConfig, this.jobsPath, this.requestClient!)
config = {
...this.sasjsConfig,
...config
}
const data = { files, params }
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.computeJobExecutor?.resendWaitingRequests()
await this.jesJobExecutor?.resendWaitingRequests()
await this.fileUploader?.resendWaitingRequests()
}
/**
@@ -991,7 +1000,8 @@ export default class SASjs {
}
this.fileUploader = new FileUploader(
this.sasjsConfig,
this.sasjsConfig.serverUrl,
this.sasjsConfig.serverType!,
this.jobsPath,
this.requestClient
)

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

@@ -1,7 +1,6 @@
import { AuthConfig, ServerType } from '@sasjs/utils/types'
import { SASjsRequest } from '../types'
import { ExtraResponseAttributes } from '@sasjs/utils/types'
import { asyncForEach, parseGeneratedCode, parseSourceCode } from '../utils'
import { asyncForEach } from '../utils'
export type ExecuteFunction = () => Promise<any>
@@ -21,7 +20,6 @@ export abstract class BaseJobExecutor implements JobExecutor {
constructor(protected serverUrl: string, protected serverType: ServerType) {}
private waitingRequests: ExecuteFunction[] = []
private requests: SASjsRequest[] = []
abstract execute(
sasJob: string,

View File

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

View File

@@ -90,7 +90,7 @@ export class WebJobExecutor extends BaseJobExecutor {
// file upload approach
try {
formData = generateFileUploadForm(formData, data)
} catch (e) {
} catch (e: any) {
return Promise.reject(new ErrorResponse(e?.message, e))
}
} else {
@@ -100,7 +100,7 @@ export class WebJobExecutor extends BaseJobExecutor {
generateTableUploadForm(formData, data)
formData = newFormData
requestParams = { ...requestParams, ...params }
} catch (e) {
} catch (e: any) {
return Promise.reject(new ErrorResponse(e?.message, e))
}
}

View File

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

View File

@@ -182,7 +182,7 @@ export class RequestClient implements HttpClient {
})
}
public post<T>(
public async post<T>(
url: string,
data: any,
accessToken: string | undefined,
@@ -286,7 +286,7 @@ export class RequestClient implements HttpClient {
result: response.data,
etag: response.headers['etag'] as string
}
} catch (e) {
} catch (e: any) {
const response = e.response as AxiosResponse
if (response?.status === 403 || response?.status === 449) {
this.parseAndSetFileUploadCsrfToken(response)

View File

@@ -2,7 +2,7 @@
* @jest-environment jsdom
*/
import { FileUploader } from '../FileUploader'
import { FileUploader } from '../job-execution/FileUploader'
import { SASjsConfig, UploadFile } from '../types'
import { RequestClient } from '../request/RequestClient'
import axios from 'axios'
@@ -39,56 +39,66 @@ describe('FileUploader', () => {
}
const fileUploader = new FileUploader(
config,
config.serverUrl,
config.serverType!,
'/jobs/path',
new RequestClient('https://sample.server.com')
)
it('should upload successfully', async () => {
const sasJob = 'test/upload'
const { files, params } = prepareFilesAndParams()
const data = prepareFilesAndParams()
mockedAxios.post.mockImplementation(() =>
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))
})
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 () => {
const sasJob = 'test/upload'
const files: UploadFile[] = []
const params = { table: 'libtable' }
const err = await fileUploader
.uploadFile(sasJob, files, params)
const res: any = await fileUploader
.execute(sasJob, files, params, config)
.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 () => {
const sasJob = ''
const { files, params } = prepareFilesAndParams()
const data = prepareFilesAndParams()
const err = await fileUploader
.uploadFile(sasJob, files, params)
const res: any = await fileUploader
.execute(sasJob, data, config)
.catch((err: any) => err)
expect(err.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.')
expect(res.error.message).toEqual('sasJob must be provided.')
})
it('should throw an error when invalid JSON is returned by the server', async () => {
@@ -97,12 +107,13 @@ describe('FileUploader', () => {
)
const sasJob = 'test'
const { files, params } = prepareFilesAndParams()
const data = prepareFilesAndParams()
const err = await fileUploader
.uploadFile(sasJob, files, params)
const res: any = await fileUploader
.execute(sasJob, data, config)
.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 () => {
@@ -111,11 +122,11 @@ describe('FileUploader', () => {
)
const sasJob = 'test'
const { files, params } = prepareFilesAndParams()
const data = prepareFilesAndParams()
const err = await fileUploader
.uploadFile(sasJob, files, params)
const res: any = await fileUploader
.execute(sasJob, data, config)
.catch((err: any) => err)
expect(err.error.message).toEqual('File upload request failed.')
expect(res.error.message).toEqual('File upload request failed.')
})
})

View File

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