From 99cfb8b2af01a8f1e41f6c03aae81d7fba31cabc Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Thu, 16 Mar 2023 00:26:08 +0500 Subject: [PATCH] feat: created a minified version of adapter for executing web jobs on sas9 --- src/index-sas9-min.ts | 6 - .../sas9/SASjs.ts} | 70 +++----- src/minified/sas9/WebJobExecutor.ts | 157 ++++++++++++++++++ src/minified/sas9/index.ts | 3 + webpack.config.js | 32 +++- 5 files changed, 203 insertions(+), 65 deletions(-) delete mode 100644 src/index-sas9-min.ts rename src/{SASjs-sas9-min.ts => minified/sas9/SASjs.ts} (81%) create mode 100644 src/minified/sas9/WebJobExecutor.ts create mode 100644 src/minified/sas9/index.ts diff --git a/src/index-sas9-min.ts b/src/index-sas9-min.ts deleted file mode 100644 index 0314954..0000000 --- a/src/index-sas9-min.ts +++ /dev/null @@ -1,6 +0,0 @@ -import SASjs from './SASjs-sas9-min' -export * from './types' -export * from './types/errors' -export * from './SAS9ApiClient' -export * from './request/Sas9RequestClient' -export default SASjs diff --git a/src/SASjs-sas9-min.ts b/src/minified/sas9/SASjs.ts similarity index 81% rename from src/SASjs-sas9-min.ts rename to src/minified/sas9/SASjs.ts index 45282fd..5832cb6 100644 --- a/src/SASjs-sas9-min.ts +++ b/src/minified/sas9/SASjs.ts @@ -1,16 +1,16 @@ -import { validateInput, compareTimestamps } from './utils' -import { SASjsConfig, UploadFile, LoginMechanism } from './types' -import { SAS9ApiClient } from './SAS9ApiClient' -import { AuthManager } from './auth/AuthManager-sas9-min' +import { validateInput, compareTimestamps } from '../../utils' +import { SASjsConfig, UploadFile, LoginMechanism } from '../../types' +import { AuthManager } from '../../auth' import { ServerType, AuthConfig, ExtraResponseAttributes } from '@sasjs/utils/types' -import { RequestClient } from './request/RequestClient' -import { JobExecutor, Sas9JobExecutor, FileUploader } from './job-execution' -import { ErrorResponse } from './types/errors' -import { LoginOptions, LoginResult } from './types/Login' +import { RequestClient } from '../../request/RequestClient' +import { FileUploader } from '../../job-execution/FileUploader' +import { WebJobExecutor } from './WebJobExecutor' +import { ErrorResponse } from '../../types/errors/ErrorResponse' +import { LoginOptions, LoginResult } from '../../types/Login' const defaultConfig: SASjsConfig = { serverUrl: '', @@ -32,12 +32,10 @@ const defaultConfig: SASjsConfig = { export default class SASjs { private sasjsConfig: SASjsConfig = new SASjsConfig() private jobsPath: string = '' - private sas9ApiClient: SAS9ApiClient | null = null private fileUploader: FileUploader | null = null private authManager: AuthManager | null = null private requestClient: RequestClient | null = null - private webJobExecutor: JobExecutor | null = null - private sas9JobExecutor: JobExecutor | null = null + private webJobExecutor: WebJobExecutor | null = null constructor(config?: Partial) { this.sasjsConfig = { @@ -190,23 +188,14 @@ export default class SASjs { // status is true if the data passes validation checks above if (validationResult.status) { - if ( - config.serverType === ServerType.Sas9 && - config.username && - config.password - ) { - console.log(`🤖[198]🤖`, 198) - return await this.sas9JobExecutor!.execute(sasJob, data, config) - } else { - return await this.webJobExecutor!.execute( - sasJob, - data, - config, - loginRequiredCallback, - authConfig, - extraResponseAttributes - ) - } + return await this.webJobExecutor!.execute( + sasJob, + data, + config, + loginRequiredCallback, + authConfig, + extraResponseAttributes + ) } else { return Promise.reject(new ErrorResponse(validationResult.msg)) } @@ -241,8 +230,7 @@ export default class SASjs { } if (!this.requestClient) { - const RequestClientClass = RequestClient - this.requestClient = new RequestClientClass( + this.requestClient = new RequestClient( this.sasjsConfig.serverUrl, this.sasjsConfig.httpsAgentOptions, this.sasjsConfig.requestHistoryLimit @@ -263,18 +251,6 @@ export default class SASjs { this.resendWaitingRequests ) - if (this.sasjsConfig.serverType === ServerType.Sas9) { - if (this.sas9ApiClient) { - this.sas9ApiClient!.setConfig(this.sasjsConfig.serverUrl) - } else { - this.sas9ApiClient = new SAS9ApiClient( - this.sasjsConfig.serverUrl, - this.jobsPath, - this.sasjsConfig.httpsAgentOptions - ) - } - } - this.fileUploader = new FileUploader( this.sasjsConfig.serverUrl, this.sasjsConfig.serverType!, @@ -282,17 +258,11 @@ export default class SASjs { this.requestClient ) - this.sas9JobExecutor = new Sas9JobExecutor( + this.webJobExecutor = new WebJobExecutor( this.sasjsConfig.serverUrl, this.sasjsConfig.serverType!, this.jobsPath, - this.requestClient, - this.sasjsConfig.httpsAgentOptions - ) - - console.log( - `🤖[setupConfiguration this.sas9JobExecutor]🤖`, - this.sas9JobExecutor + this.requestClient ) } diff --git a/src/minified/sas9/WebJobExecutor.ts b/src/minified/sas9/WebJobExecutor.ts new file mode 100644 index 0000000..4cf9393 --- /dev/null +++ b/src/minified/sas9/WebJobExecutor.ts @@ -0,0 +1,157 @@ +import { + AuthConfig, + ExtraResponseAttributes, + ServerType +} from '@sasjs/utils/types' +import { + ErrorResponse, + JobExecutionError, + LoginRequiredError +} from '../../types/errors' +import { RequestClient } from '../../request/RequestClient' +import { + isRelativePath, + parseSasViyaDebugResponse, + appendExtraResponseAttributes, + convertToCSV +} from '../../utils' +import { BaseJobExecutor } from '../../job-execution/JobExecutor' +import { parseWeboutResponse } from '../../utils/parseWeboutResponse' + +export interface WaitingRequstPromise { + promise: Promise | null + resolve: any + reject: any +} +export class WebJobExecutor extends BaseJobExecutor { + constructor( + serverUrl: string, + serverType: ServerType, + private jobsPath: string, + private requestClient: RequestClient + ) { + super(serverUrl, serverType) + } + + 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 + : sasJob + let apiUrl = `${config.serverUrl}${this.jobsPath}/?${'_program=' + program}` + + let requestParams = { + ...this.getRequestParams(config) + } + + let formData = new FormData() + + if (data) { + 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]) + } + } + + const requestPromise = new Promise((resolve, reject) => { + this.requestClient!.post(apiUrl, formData, authConfig?.access_token) + .then(async (res: any) => { + this.requestClient!.appendRequest(res, sasJob, config.debug) + + const jsonResponse = + config.debug && typeof res.result === 'string' + ? parseWeboutResponse(res.result, apiUrl) + : res.result + + const responseObject = appendExtraResponseAttributes( + { result: jsonResponse, log: res.log }, + 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 + } +} + +/** + * One of the approaches SASjs takes to send tables-formatted JSON (see README) + * to SAS is as multipart form data, where each table is provided as a specially + * formatted CSV file. + */ +const generateFileUploadForm = (formData: FormData, data: any): FormData => { + for (const tableName in data) { + if (!Array.isArray(data[tableName])) continue + + const name = tableName + const csv = convertToCSV(data, tableName) + + if (csv === 'ERROR: LARGE STRING LENGTH') { + throw new Error( + 'The max length of a string value in SASjs is 32765 characters.' + ) + } + + const file = new Blob([csv], { + type: 'application/csv' + }) + + formData.append(name, file, `${name}.csv`) + } + + return formData +} diff --git a/src/minified/sas9/index.ts b/src/minified/sas9/index.ts new file mode 100644 index 0000000..cb5b539 --- /dev/null +++ b/src/minified/sas9/index.ts @@ -0,0 +1,3 @@ +import SASjs from './SASjs' +export * from '../../types' +export default SASjs diff --git a/webpack.config.js b/webpack.config.js index bfb4d81..ad5ea68 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -23,8 +23,16 @@ const optimization = { } const browserConfig = { - entry: './src/index-sas9-min.ts', - devtool: 'inline-source-map', + entry: { + index: './src/index.ts', + minified_sas9: './src/minified/sas9/index.ts' + }, + output: { + filename: '[name].js', + path: path.resolve(__dirname, 'build'), + libraryTarget: 'umd', + library: 'SASjs' + }, mode: 'production', optimization: optimization, module: { @@ -40,12 +48,6 @@ const browserConfig = { extensions: ['.ts', '.js'], fallback: { https: false, fs: false, readline: false } }, - output: { - filename: 'index-sas9-min.js', - path: path.resolve(__dirname, 'build'), - libraryTarget: 'umd', - library: 'SASjs' - }, plugins: [ ...defaultPlugins, new webpack.ProvidePlugin({ @@ -55,6 +57,18 @@ const browserConfig = { ] } +const browserConfigWithDevTool = { + ...browserConfig, + entry: './src/index.ts', + output: { + filename: 'index-dev.js', + path: path.resolve(__dirname, 'build'), + libraryTarget: 'umd', + library: 'SASjs' + }, + devtool: 'inline-source-map' +} + const browserConfigWithoutProcessPlugin = { entry: browserConfig.entry, devtool: browserConfig.devtool, @@ -76,4 +90,4 @@ const nodeConfig = { } } -module.exports = [browserConfig, nodeConfig] +module.exports = [browserConfig, browserConfigWithDevTool, nodeConfig]