From 19adcc311530881700b5d8cd2cfe854a1ccc7e11 Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Mon, 13 Sep 2021 17:42:41 +0500 Subject: [PATCH 1/4] chore: FileUploader extends BaseJobExecutor --- src/FileUploader.ts | 89 ----------------- src/SASjs.ts | 16 ++- src/job-execution/FileUploader.ts | 143 +++++++++++++++++++++++++++ src/job-execution/Sas9JobExecutor.ts | 2 +- src/job-execution/index.ts | 1 + src/test/FileUploader.spec.ts | 76 ++++++++------ 6 files changed, 200 insertions(+), 127 deletions(-) delete mode 100644 src/FileUploader.ts create mode 100644 src/job-execution/FileUploader.ts diff --git a/src/FileUploader.ts b/src/FileUploader.ts deleted file mode 100644 index 5523929..0000000 --- a/src/FileUploader.ts +++ /dev/null @@ -1,89 +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 { SASjsConfig } from './types' - -export class FileUploader { - constructor(private jobsPath: string, private requestClient: RequestClient) {} - - public async uploadFile( - sasJob: string, - files: UploadFile[], - params: any, - config: SASjsConfig - ) { - 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 = 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 - return this.requestClient - .post(uploadUrl, formData, undefined, 'application/json', headers) - .then(async (res) => { - this.requestClient.appendRequest(res, sasJob, config.debug) - if (config.serverType === ServerType.SasViya && config.debug) { - const jsonResponse = await parseSasViyaDebugResponse( - res.result as string, - this.requestClient, - config.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) - ) - }) - } -} diff --git a/src/SASjs.ts b/src/SASjs.ts index b7d3e0b..2c8fbf3 100644 --- a/src/SASjs.ts +++ b/src/SASjs.ts @@ -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' @@ -579,10 +579,11 @@ export default class SASjs { params: any, overrideSasjsConfig?: any ) { - return await this.fileUploader!.uploadFile(sasJob, files, params, { + const config = { ...this.sasjsConfig, ...overrideSasjsConfig - }) + } + return await this.fileUploader!.execute(sasJob, files, params, config) } /** @@ -984,7 +985,12 @@ export default class SASjs { ) } - this.fileUploader = new FileUploader(this.jobsPath, this.requestClient) + this.fileUploader = new FileUploader( + this.sasjsConfig.serverUrl, + this.sasjsConfig.serverType!, + this.jobsPath, + this.requestClient + ) this.webJobExecutor = new WebJobExecutor( this.sasjsConfig.serverUrl, diff --git a/src/job-execution/FileUploader.ts b/src/job-execution/FileUploader.ts new file mode 100644 index 0000000..ecb7c95 --- /dev/null +++ b/src/job-execution/FileUploader.ts @@ -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: any +} + +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 + } +} diff --git a/src/job-execution/Sas9JobExecutor.ts b/src/job-execution/Sas9JobExecutor.ts index 3b1f77f..892be05 100644 --- a/src/job-execution/Sas9JobExecutor.ts +++ b/src/job-execution/Sas9JobExecutor.ts @@ -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)) } } diff --git a/src/job-execution/index.ts b/src/job-execution/index.ts index dc9187c..46237a6 100644 --- a/src/job-execution/index.ts +++ b/src/job-execution/index.ts @@ -3,3 +3,4 @@ export * from './JesJobExecutor' export * from './JobExecutor' export * from './Sas9JobExecutor' export * from './WebJobExecutor' +export * from './FileUploader' diff --git a/src/test/FileUploader.spec.ts b/src/test/FileUploader.spec.ts index 452ddfa..7266aa7 100644 --- a/src/test/FileUploader.spec.ts +++ b/src/test/FileUploader.spec.ts @@ -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,55 +39,66 @@ describe('FileUploader', () => { } const fileUploader = new FileUploader( + 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, config) + 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: '
' }) + ) + .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, config) + 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, config) + 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: '' }) - ) - - const sasJob = 'test' - const { files, params } = prepareFilesAndParams() - - const err = await fileUploader - .uploadFile(sasJob, files, params, config) - .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 () => { @@ -96,12 +107,13 @@ describe('FileUploader', () => { ) const sasJob = 'test' - const { files, params } = prepareFilesAndParams() + const data = prepareFilesAndParams() - const err = await fileUploader - .uploadFile(sasJob, files, params, config) + 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 () => { @@ -110,11 +122,11 @@ describe('FileUploader', () => { ) const sasJob = 'test' - const { files, params } = prepareFilesAndParams() + const data = prepareFilesAndParams() - const err = await fileUploader - .uploadFile(sasJob, files, params, config) + 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.') }) }) From f714f20f299f3d9fc62048182483cb701d13d509 Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Mon, 13 Sep 2021 17:45:35 +0500 Subject: [PATCH 2/4] chore(fileUploader): support loginCallback and re-submit request --- src/SASjs.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/SASjs.ts b/src/SASjs.ts index 2c8fbf3..9f4f143 100644 --- a/src/SASjs.ts +++ b/src/SASjs.ts @@ -869,6 +869,7 @@ export default class SASjs { await this.webJobExecutor?.resendWaitingRequests() await this.computeJobExecutor?.resendWaitingRequests() await this.jesJobExecutor?.resendWaitingRequests() + await this.fileUploader?.resendWaitingRequests() } /** From 10cf4998f5b164fdb5f9e519767e8f04cca14086 Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Mon, 13 Sep 2021 18:02:26 +0500 Subject: [PATCH 3/4] fix(loginPrompt): z-index added --- src/utils/loginPrompt/index.ts | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/src/utils/loginPrompt/index.ts b/src/utils/loginPrompt/index.ts index 0f53a23..2074032 100644 --- a/src/utils/loginPrompt/index.ts +++ b/src/utils/loginPrompt/index.ts @@ -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 => { return new Promise(async (resolve) => { @@ -12,11 +18,11 @@ export const openLoginPrompt = (): Promise => { 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; From 15ff90025aad34c4e17135e396ce31ab58e7361a Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Tue, 14 Sep 2021 05:58:40 +0500 Subject: [PATCH 4/4] fix(fileUploader): added loginCallback --- src/SASjs.ts | 25 +++++++++++++++++++------ src/job-execution/FileUploader.ts | 2 +- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/SASjs.ts b/src/SASjs.ts index 9f4f143..74831a5 100644 --- a/src/SASjs.ts +++ b/src/SASjs.ts @@ -571,19 +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 async uploadFile( sasJob: string, files: UploadFile[], - params: any, - overrideSasjsConfig?: any + params: { [key: string]: any } | null, + config: { [key: string]: any } = {}, + loginRequiredCallback?: () => any ) { - const config = { + config = { ...this.sasjsConfig, - ...overrideSasjsConfig + ...config } - return await this.fileUploader!.execute(sasJob, files, params, config) + const data = { files, params } + + return await this.fileUploader!.execute( + sasJob, + data, + config, + loginRequiredCallback + ) } /** diff --git a/src/job-execution/FileUploader.ts b/src/job-execution/FileUploader.ts index ecb7c95..868cfa6 100644 --- a/src/job-execution/FileUploader.ts +++ b/src/job-execution/FileUploader.ts @@ -15,7 +15,7 @@ import { BaseJobExecutor } from './JobExecutor' interface dataFileUpload { files: UploadFile[] - params: any + params: { [key: string]: any } | null } export class FileUploader extends BaseJobExecutor {