diff --git a/src/SASjs.ts b/src/SASjs.ts index ce07b95..f33c1eb 100644 --- a/src/SASjs.ts +++ b/src/SASjs.ts @@ -27,7 +27,6 @@ import { ComputeJobExecutor, JesJobExecutor, Sas9JobExecutor, - SasJsJobExecutor, FileUploader } from './job-execution' import { ErrorResponse } from './types/errors' @@ -63,7 +62,6 @@ export default class SASjs { private computeJobExecutor: JobExecutor | null = null private jesJobExecutor: JobExecutor | null = null private sas9JobExecutor: JobExecutor | null = null - private sasJsJobExecutor: JobExecutor | null = null constructor(config?: Partial) { this.sasjsConfig = { @@ -79,7 +77,8 @@ export default class SASjs { } /** - * Executes the sas code against SAS9 server + * Executes code against a SAS 9 server. Requires a runner to be present in + * the users home directory in metadata. * @param linesOfCode - lines of sas code from the file to run. * @param username - a string representing the username. * @param password - a string representing the password. @@ -99,7 +98,7 @@ export default class SASjs { } /** - * Executes the sas code against SASViya server + * Executes sas code in a SAS Viya compute session. * @param fileName - name of the file to run. It will be converted to path to the file being submitted for execution. * @param linesOfCode - lines of sas code from the file to run. * @param contextName - context name on which code will be run on the server. @@ -643,7 +642,8 @@ export default class SASjs { } /** - * Makes a request to the SAS Service specified in `SASjob`. The response + * Makes a request to program specified in `SASjob` (could be a Viya Job, a + * SAS 9 Stored Process, or a SASjs Server Stored Program). The response * object will always contain table names in lowercase, and column names in * uppercase. Values are returned formatted by default, unformatted * values can be configured as an option in the `%webout` macro. @@ -652,7 +652,8 @@ export default class SASjs { * the SAS `_program` parameter to run a Job Definition or SAS 9 Stored * Process). Is prepended at runtime with the value of `appLoc`. * @param data - a JSON object containing one or more tables to be sent to - * SAS. Can be `null` if no inputs required. + * SAS. For an example of the table structure, see the project README. This + * value can be `null` if no inputs are required. * @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. @@ -682,9 +683,10 @@ export default class SASjs { const validationResult = this.validateInput(data) + // status is true if the data passes validation checks above if (validationResult.status) { if (config.serverType === ServerType.Sasjs) { - return await this.sasJsJobExecutor!.execute( + return await this.webJobExecutor!.execute( sasJob, data, config, @@ -693,7 +695,7 @@ export default class SASjs { extraResponseAttributes ) } else if ( - config.serverType !== ServerType.Sas9 && + config.serverType === ServerType.SasViya && config.useComputeApi !== undefined && config.useComputeApi !== null ) { @@ -1114,13 +1116,6 @@ export default class SASjs { this.sasViyaApiClient! ) - this.sasJsJobExecutor = new SasJsJobExecutor( - this.sasjsConfig.serverUrl, - this.sasjsConfig.serverType!, - this.jobsPath, - this.requestClient - ) - this.sas9JobExecutor = new Sas9JobExecutor( this.sasjsConfig.serverUrl, this.sasjsConfig.serverType!, diff --git a/src/file/generateFileUploadForm.ts b/src/file/generateFileUploadForm.ts index e358c08..dd5661c 100644 --- a/src/file/generateFileUploadForm.ts +++ b/src/file/generateFileUploadForm.ts @@ -1,9 +1,19 @@ +import * as NodeFormData from 'form-data' import { convertToCSV } from '../utils/convertToCsv' +/** + * 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. + * @param formData Different objects are used depending on whether the adapter is + * running in the browser, or in the CLI + * @param data Special, tables-formatted JSON (see README) + * @returns Populated formData + */ export const generateFileUploadForm = ( - formData: FormData, + formData: FormData | NodeFormData, data: any -): FormData => { +): FormData | NodeFormData => { for (const tableName in data) { if (!Array.isArray(data[tableName])) continue diff --git a/src/file/generateTableUploadForm.ts b/src/file/generateTableUploadForm.ts index 140cae4..450333b 100644 --- a/src/file/generateTableUploadForm.ts +++ b/src/file/generateTableUploadForm.ts @@ -1,7 +1,11 @@ +import * as NodeFormData from 'form-data' import { convertToCSV } from '../utils/convertToCsv' import { splitChunks } from '../utils/splitChunks' -export const generateTableUploadForm = (formData: FormData, data: any) => { +export const generateTableUploadForm = ( + formData: FormData | NodeFormData, + data: any +) => { const sasjsTables = [] const requestParams: any = {} let tableCounter = 0 diff --git a/src/job-execution/SasJsJobExecutor.ts b/src/job-execution/SasJsJobExecutor.ts deleted file mode 100644 index fd3e56e..0000000 --- a/src/job-execution/SasJsJobExecutor.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { - AuthConfig, - ExtraResponseAttributes, - ServerType -} from '@sasjs/utils/types' -import { - ErrorResponse, - JobExecutionError, - LoginRequiredError -} from '../types/errors' -import { RequestClient } from '../request/RequestClient' -import { - isRelativePath, - appendExtraResponseAttributes, - getValidJson -} from '../utils' -import { BaseJobExecutor } from './JobExecutor' -import { parseWeboutResponse } from '../utils/parseWeboutResponse' - -export class SasJsJobExecutor 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 || (() => Promise.resolve()) - const program = isRelativePath(sasJob) - ? config.appLoc - ? config.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '') - : sasJob - : sasJob - let apiUrl = `${config.serverUrl}${this.jobsPath}/?${'_program=' + program}` - - const requestParams = this.getRequestParams(config) - - const requestPromise = new Promise((resolve, reject) => { - this.requestClient!.post( - apiUrl, - { ...requestParams, ...data }, - authConfig?.access_token - ) - .then(async (res: any) => { - const parsedSasjsServerLog = res.result.log - .map((logLine: any) => logLine.line) - .join('\n') - - const resObj = { - result: res.result._webout, - log: parsedSasjsServerLog - } - this.requestClient!.appendRequest(resObj, sasJob, config.debug) - - let jsonResponse = res.result - - if (config.debug) { - if (typeof res.result._webout === 'object') { - jsonResponse = res.result._webout - } else { - const webout = parseWeboutResponse(res.result._webout, apiUrl) - jsonResponse = getValidJson(webout) - } - } else { - jsonResponse = getValidJson(res.result._webout) - } - - const responseObject = appendExtraResponseAttributes( - { result: jsonResponse }, - 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) { - this.appendWaitingRequest(() => { - return this.execute( - sasJob, - data, - config, - loginRequiredCallback, - authConfig, - extraResponseAttributes - ).then( - (res: any) => { - resolve(res) - }, - (err: any) => { - reject(err) - } - ) - }) - - await loginCallback() - } else { - reject(new ErrorResponse(e?.message, e)) - } - }) - }) - - return requestPromise - } - - private getRequestParams(config: any): any { - const requestParams: any = {} - - if (config.debug) { - requestParams['_omittextlog'] = 'false' - requestParams['_omitsessionresults'] = 'false' - - requestParams['_debug'] = 131 - } - - return requestParams - } -} diff --git a/src/job-execution/WebJobExecutor.ts b/src/job-execution/WebJobExecutor.ts index 5100da4..1e36855 100644 --- a/src/job-execution/WebJobExecutor.ts +++ b/src/job-execution/WebJobExecutor.ts @@ -1,3 +1,4 @@ +import * as NodeFormData from 'form-data' import { AuthConfig, ExtraResponseAttributes, @@ -108,7 +109,12 @@ export class WebJobExecutor extends BaseJobExecutor { ...this.getRequestParams(config) } - let formData = new FormData() + /** + * Use the available form data object (FormData in Browser, NodeFormData in + * Node) + */ + let formData = + typeof FormData === 'undefined' ? new NodeFormData() : new FormData() if (data) { const stringifiedData = JSON.stringify(data) @@ -143,8 +149,19 @@ export class WebJobExecutor extends BaseJobExecutor { } } + /* The NodeFormData object does not set the request header - so, set it */ + const contentType = + formData instanceof NodeFormData + ? `multipart/form-data; boundary=${formData.getBoundary()}` + : undefined + const requestPromise = new Promise((resolve, reject) => { - this.requestClient!.post(apiUrl, formData, authConfig?.access_token) + this.requestClient!.post( + apiUrl, + formData, + authConfig?.access_token, + contentType + ) .then(async (res: any) => { const parsedSasjsServerLog = this.serverType === ServerType.Sasjs diff --git a/src/job-execution/index.ts b/src/job-execution/index.ts index 0fd44f7..f0025cb 100644 --- a/src/job-execution/index.ts +++ b/src/job-execution/index.ts @@ -3,5 +3,4 @@ export * from './FileUploader' export * from './JesJobExecutor' export * from './JobExecutor' export * from './Sas9JobExecutor' -export * from './SasJsJobExecutor' export * from './WebJobExecutor'