@sasjs/adapter 4.11.2 → 4.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/SASViyaApiClient.d.ts +25 -2
  2. package/SASjs.d.ts +20 -2
  3. package/api/viya/writeStream.d.ts +1 -1
  4. package/auth/getTokenRequestErrorPrefix.d.ts +2 -2
  5. package/file/generateFileUploadForm.d.ts +1 -1
  6. package/file/generateTableUploadForm.d.ts +1 -1
  7. package/index-dev.js +77153 -3
  8. package/index.js +77153 -2
  9. package/job-execution/JobExecutor.d.ts +1 -1
  10. package/minified_sas9.js +16558 -2
  11. package/node/SASViyaApiClient.d.ts +25 -2
  12. package/node/SASjs.d.ts +20 -2
  13. package/node/api/viya/writeStream.d.ts +1 -1
  14. package/node/auth/getTokenRequestErrorPrefix.d.ts +2 -2
  15. package/node/file/generateFileUploadForm.d.ts +1 -1
  16. package/node/file/generateTableUploadForm.d.ts +1 -1
  17. package/node/index.js +28247 -2
  18. package/node/job-execution/JobExecutor.d.ts +1 -1
  19. package/node/request/RequestClient.d.ts +11 -2
  20. package/node/types/FileResource.d.ts +32 -0
  21. package/node/types/PollOptions.d.ts +1 -1
  22. package/node/types/RequestClient.d.ts +1 -1
  23. package/node/types/WriteStream.d.ts +6 -3
  24. package/node/utils/getFormData.d.ts +1 -1
  25. package/package.json +12 -9
  26. package/request/RequestClient.d.ts +11 -2
  27. package/spec/SAS9ApiClient.spec.d.ts +1 -0
  28. package/spec/SASjsApiClient.spec.d.ts +1 -0
  29. package/src/SAS9ApiClient.ts +1 -1
  30. package/src/SASViyaApiClient.ts +124 -6
  31. package/src/SASjs.ts +54 -4
  32. package/src/SASjsApiClient.ts +1 -1
  33. package/src/api/viya/writeStream.ts +10 -5
  34. package/src/auth/refreshTokensForViya.ts +1 -1
  35. package/src/file/generateFileUploadForm.ts +1 -1
  36. package/src/file/generateTableUploadForm.ts +1 -1
  37. package/src/job-execution/Sas9JobExecutor.ts +1 -1
  38. package/src/job-execution/SasjsJobExecutor.ts +5 -3
  39. package/src/job-execution/WebJobExecutor.ts +5 -3
  40. package/src/request/RequestClient.ts +73 -81
  41. package/src/request/Sas9RequestClient.ts +5 -5
  42. package/src/test/SAS_server_app.ts +10 -2
  43. package/src/types/FileResource.ts +33 -0
  44. package/src/types/WriteStream.ts +9 -3
  45. package/src/utils/convertToCsv.ts +5 -1
  46. package/src/utils/getFormData.ts +2 -2
  47. package/src/utils/validateInput.ts +16 -13
  48. package/types/FileResource.d.ts +32 -0
  49. package/types/PollOptions.d.ts +1 -1
  50. package/types/RequestClient.d.ts +1 -1
  51. package/types/WriteStream.d.ts +6 -3
  52. package/types/errors/spec/SAS9AuthError.spec.d.ts +1 -0
  53. package/utils/getFormData.d.ts +1 -1
  54. package/utils/spec/parseSasViyaLog.spec.d.ts +1 -0
  55. package/utils/spec/parseViyaDebugResponse.spec.d.ts +1 -0
  56. package/index-dev.js.LICENSE.txt +0 -110
  57. package/index.js.LICENSE.txt +0 -110
  58. package/minified_sas9.js.LICENSE.txt +0 -15
  59. package/node/index.js.LICENSE.txt +0 -106
@@ -1,6 +1,6 @@
1
1
  import { AuthConfig, ServerType } from '@sasjs/utils/types';
2
2
  import { ExtraResponseAttributes } from '@sasjs/utils/types';
3
- export declare type ExecuteFunction = () => Promise<any>;
3
+ export type ExecuteFunction = () => Promise<any>;
4
4
  export interface JobExecutor {
5
5
  execute: (sasJob: string, data: any, config: any, loginRequiredCallback?: any, authConfig?: AuthConfig, extraResponseAttributes?: ExtraResponseAttributes[]) => Promise<any>;
6
6
  resendWaitingRequests: () => Promise<void>;
@@ -42,6 +42,13 @@ export declare class RequestClient implements HttpClient {
42
42
  etag: string;
43
43
  status: number;
44
44
  }>;
45
+ /**
46
+ * @param contentType Newer version of Axios is more strict so if you don't
47
+ * set the contentType to `form data` while sending a FormData object
48
+ * application/json will be used by default, axios won’t treat it as FormData.
49
+ * Instead, it serializes data as JSON—resulting in a payload like
50
+ * {"sometable":{}} and we lose the multipart/form-data formatting.
51
+ */
45
52
  post<T>(url: string, data: any, accessToken: string | undefined, contentType?: string, overrideHeaders?: {
46
53
  [key: string]: string | number;
47
54
  }, additionalSettings?: {
@@ -79,7 +86,9 @@ export declare class RequestClient implements HttpClient {
79
86
  * @returns - formatted string.
80
87
  */
81
88
  private parseInterceptedBody;
82
- private defaultInterceptionCallBack;
89
+ private handleAxiosResponse;
90
+ private handleAxiosError;
91
+ private formatHeaders;
83
92
  /**
84
93
  * Sets verbose mode.
85
94
  * @param verboseMode - value of the verbose mode, can be true, false or bleached(without extra colors).
@@ -90,7 +99,7 @@ export declare class RequestClient implements HttpClient {
90
99
  * @param successCallBack - function that should be triggered on every HTTP response with the status 2**.
91
100
  * @param errorCallBack - function that should be triggered on every HTTP response with the status different from 2**.
92
101
  */
93
- enableVerboseMode: (successCallBack?: (axiosResponse: AxiosResponse | AxiosError) => AxiosResponse<any, any> | AxiosError<unknown, any>, errorCallBack?: (axiosResponse: AxiosResponse | AxiosError) => AxiosResponse<any, any> | AxiosError<unknown, any>) => void;
102
+ enableVerboseMode: (successCallBack?: (response: AxiosResponse) => AxiosResponse<any, any>, errorCallBack?: (error: AxiosError) => AxiosError<unknown, any>) => void;
94
103
  /**
95
104
  * Turns off verbose mode to log every HTTP response.
96
105
  */
@@ -0,0 +1,32 @@
1
+ export interface FileResource {
2
+ creationTimeStamp: string;
3
+ modifiedTimeStamp: string;
4
+ createdBy: string;
5
+ modifiedBy: string;
6
+ id: string;
7
+ properties: Properties;
8
+ contentDisposition: string;
9
+ contentType: string;
10
+ encoding: string;
11
+ links: Link[];
12
+ name: string;
13
+ size: number;
14
+ searchable: boolean;
15
+ fileStatus: string;
16
+ fileVersion: number;
17
+ typeDefName: string;
18
+ version: number;
19
+ virusDetected: boolean;
20
+ urlDetected: boolean;
21
+ quarantine: boolean;
22
+ }
23
+ export interface Link {
24
+ method: string;
25
+ rel: string;
26
+ href: string;
27
+ uri: string;
28
+ type?: string;
29
+ responseType?: string;
30
+ }
31
+ export interface Properties {
32
+ }
@@ -5,4 +5,4 @@ export interface PollOptions {
5
5
  streamLog?: boolean;
6
6
  logFolderPath?: string;
7
7
  }
8
- export declare type PollStrategy = PollOptions[];
8
+ export type PollStrategy = PollOptions[];
@@ -43,4 +43,4 @@ export interface SasjsParsedResponse<T> {
43
43
  status: number;
44
44
  printOutput?: string;
45
45
  }
46
- export declare type VerboseMode = boolean | 'bleached';
46
+ export type VerboseMode = boolean | 'bleached';
@@ -1,4 +1,7 @@
1
- export interface WriteStream {
2
- write: (content: string, callback: (err?: Error) => any) => void;
3
- path: string;
1
+ /// <reference types="node" />
2
+ /// <reference types="node" />
3
+ import { WriteStream as FsWriteStream } from 'fs';
4
+ export interface WriteStream extends FsWriteStream {
5
+ write(chunk: any, encoding?: BufferEncoding | ((error: Error | null | undefined) => void), cb?: (error: Error | null | undefined) => void): boolean;
6
+ path: string | Buffer;
4
7
  }
@@ -1,2 +1,2 @@
1
- import * as NodeFormData from 'form-data';
1
+ import NodeFormData from 'form-data';
2
2
  export declare const getFormData: () => NodeFormData | FormData;
package/package.json CHANGED
@@ -45,18 +45,21 @@
45
45
  "license": "ISC",
46
46
  "devDependencies": {
47
47
  "@cypress/webpack-preprocessor": "5.9.1",
48
+ "@types/cors": "^2.8.17",
48
49
  "@types/express": "4.17.13",
49
- "@types/jest": "27.4.0",
50
+ "@types/jest": "29.5.14",
50
51
  "@types/mime": "2.0.3",
51
52
  "@types/pem": "1.9.6",
52
53
  "@types/tough-cookie": "4.0.2",
53
54
  "copyfiles": "2.4.1",
55
+ "cors": "^2.8.5",
54
56
  "cp": "0.2.0",
55
57
  "cypress": "7.7.0",
56
58
  "dotenv": "16.0.0",
57
59
  "express": "4.17.3",
58
- "jest": "27.4.7",
59
- "jest-extended": "2.0.0",
60
+ "jest": "29.7.0",
61
+ "jest-environment-jsdom": "^29.7.0",
62
+ "jest-extended": "4.0.2",
60
63
  "node-polyfill-webpack-plugin": "1.1.4",
61
64
  "path": "0.12.7",
62
65
  "pem": "1.14.5",
@@ -64,24 +67,24 @@
64
67
  "process": "0.11.10",
65
68
  "semantic-release": "19.0.3",
66
69
  "terser-webpack-plugin": "5.3.6",
67
- "ts-jest": "27.1.3",
70
+ "ts-jest": "29.2.6",
68
71
  "ts-loader": "9.4.0",
69
72
  "tslint": "6.1.3",
70
73
  "tslint-config-prettier": "1.18.0",
71
74
  "typedoc": "0.23.24",
72
75
  "typedoc-plugin-rename-defaults": "0.6.4",
73
- "typescript": "4.8.3",
76
+ "typescript": "4.9.5",
74
77
  "webpack": "5.76.2",
75
78
  "webpack-cli": "4.9.2"
76
79
  },
77
80
  "main": "index.js",
78
81
  "dependencies": {
79
- "@sasjs/utils": "^3.5.1",
80
- "axios": "0.27.2",
81
- "axios-cookiejar-support": "1.0.1",
82
+ "@sasjs/utils": "3.5.2",
83
+ "axios": "1.8.2",
84
+ "axios-cookiejar-support": "5.0.5",
82
85
  "form-data": "4.0.0",
83
86
  "https": "1.0.0",
84
87
  "tough-cookie": "4.1.3"
85
88
  },
86
- "version": "4.11.2"
89
+ "version": "4.12.0"
87
90
  }
@@ -42,6 +42,13 @@ export declare class RequestClient implements HttpClient {
42
42
  etag: string;
43
43
  status: number;
44
44
  }>;
45
+ /**
46
+ * @param contentType Newer version of Axios is more strict so if you don't
47
+ * set the contentType to `form data` while sending a FormData object
48
+ * application/json will be used by default, axios won’t treat it as FormData.
49
+ * Instead, it serializes data as JSON—resulting in a payload like
50
+ * {"sometable":{}} and we lose the multipart/form-data formatting.
51
+ */
45
52
  post<T>(url: string, data: any, accessToken: string | undefined, contentType?: string, overrideHeaders?: {
46
53
  [key: string]: string | number;
47
54
  }, additionalSettings?: {
@@ -79,7 +86,9 @@ export declare class RequestClient implements HttpClient {
79
86
  * @returns - formatted string.
80
87
  */
81
88
  private parseInterceptedBody;
82
- private defaultInterceptionCallBack;
89
+ private handleAxiosResponse;
90
+ private handleAxiosError;
91
+ private formatHeaders;
83
92
  /**
84
93
  * Sets verbose mode.
85
94
  * @param verboseMode - value of the verbose mode, can be true, false or bleached(without extra colors).
@@ -90,7 +99,7 @@ export declare class RequestClient implements HttpClient {
90
99
  * @param successCallBack - function that should be triggered on every HTTP response with the status 2**.
91
100
  * @param errorCallBack - function that should be triggered on every HTTP response with the status different from 2**.
92
101
  */
93
- enableVerboseMode: (successCallBack?: (axiosResponse: AxiosResponse | AxiosError) => AxiosResponse<any, any> | AxiosError<unknown, any>, errorCallBack?: (axiosResponse: AxiosResponse | AxiosError) => AxiosResponse<any, any> | AxiosError<unknown, any>) => void;
102
+ enableVerboseMode: (successCallBack?: (response: AxiosResponse) => AxiosResponse<any, any>, errorCallBack?: (error: AxiosError) => AxiosError<unknown, any>) => void;
94
103
  /**
95
104
  * Turns off verbose mode to log every HTTP response.
96
105
  */
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -1,6 +1,6 @@
1
1
  import * as https from 'https'
2
2
  import { generateTimestamp } from '@sasjs/utils/time'
3
- import * as NodeFormData from 'form-data'
3
+ import NodeFormData from 'form-data'
4
4
  import { Sas9RequestClient } from './request/Sas9RequestClient'
5
5
  import { isUrl } from './utils'
6
6
 
@@ -1,5 +1,5 @@
1
1
  import { isRelativePath, isUri, isUrl } from './utils'
2
- import * as NodeFormData from 'form-data'
2
+ import NodeFormData from 'form-data'
3
3
  import {
4
4
  Job,
5
5
  Session,
@@ -28,6 +28,7 @@ import { uploadTables } from './api/viya/uploadTables'
28
28
  import { executeOnComputeApi } from './api/viya/executeOnComputeApi'
29
29
  import { getAccessTokenForViya } from './auth/getAccessTokenForViya'
30
30
  import { refreshTokensForViya } from './auth/refreshTokensForViya'
31
+ import { FileResource } from './types/FileResource'
31
32
 
32
33
  interface JobExecutionResult {
33
34
  result?: { result: object }
@@ -311,6 +312,84 @@ export class SASViyaApiClient {
311
312
  )
312
313
  }
313
314
 
315
+ /**
316
+ * Fetches the file content for a file in the specified folder.
317
+ *
318
+ * @param folderPath - the full path to the folder containing the file. eg: /Public/folder1/folder2
319
+ * @param fileName - the name of the file in the `folderPath`
320
+ * @param accessToken - an access token for authorizing the request
321
+ */
322
+ public async getFileContent(
323
+ folderPath: string,
324
+ fileName: string,
325
+ accessToken?: string
326
+ ) {
327
+ const fileUri = await this.getFileUri(
328
+ folderPath,
329
+ fileName,
330
+ accessToken
331
+ ).catch((err) => {
332
+ throw prefixMessage(
333
+ err,
334
+ `Error while getting file URI for: ${fileName} in folder: ${folderPath}. `
335
+ )
336
+ })
337
+
338
+ return await this.requestClient
339
+ .get<string>(`${this.serverUrl}${fileUri}/content`, accessToken)
340
+ .then((res) => res.result)
341
+ }
342
+
343
+ /**
344
+ * Updates the file content for a file in the specified folder.
345
+ *
346
+ * @param folderPath - the full path to the folder containing the file. eg: /Public/folder1/folder2
347
+ * @param fileName - the name of the file in the `folderPath`
348
+ * @param content - the new content to be written to the file
349
+ * @param accessToken - an access token for authorizing the request
350
+ */
351
+ public async updateFileContent(
352
+ folderPath: string,
353
+ fileName: string,
354
+ content: string,
355
+ accessToken?: string
356
+ ) {
357
+ const fileUri = await this.getFileUri(
358
+ folderPath,
359
+ fileName,
360
+ accessToken
361
+ ).catch((err) => {
362
+ throw prefixMessage(
363
+ err,
364
+ `Error while getting file URI for: ${fileName} in folder: ${folderPath}. `
365
+ )
366
+ })
367
+
368
+ // Fetch the file resource details to get the Etag and content type
369
+ const { result: originalFileResource, etag } =
370
+ await this.requestClient.get<FileResource>(
371
+ `${this.serverUrl}${fileUri}`,
372
+ accessToken
373
+ )
374
+
375
+ if (!originalFileResource || !etag)
376
+ throw new Error(
377
+ `File ${fileName} does not have an ETag, or request failed.`
378
+ )
379
+
380
+ return await this.requestClient
381
+ .put<FileResource>(
382
+ `${this.serverUrl}${fileUri}/content`,
383
+ content,
384
+ accessToken,
385
+ {
386
+ 'If-Match': etag,
387
+ 'Content-Type': originalFileResource.contentType
388
+ }
389
+ )
390
+ .then((res) => res.result)
391
+ }
392
+
314
393
  /**
315
394
  * Fetches a folder. Path to the folder is required.
316
395
  * @param folderPath - the absolute path to the folder.
@@ -941,6 +1020,7 @@ export class SASViyaApiClient {
941
1020
  })
942
1021
 
943
1022
  if (!folder) return undefined
1023
+
944
1024
  return folder
945
1025
  }
946
1026
 
@@ -952,6 +1032,30 @@ export class SASViyaApiClient {
952
1032
  return `/folders/folders/${folderDetails.id}`
953
1033
  }
954
1034
 
1035
+ private async getFileUri(
1036
+ folderPath: string,
1037
+ fileName: string,
1038
+ accessToken?: string
1039
+ ): Promise<string> {
1040
+ const folderMembers = await this.listFolder(folderPath, accessToken, 1000, {
1041
+ returnDetails: true
1042
+ }).catch((err) => {
1043
+ throw prefixMessage(err, `Error while listing folder: ${folderPath}. `)
1044
+ })
1045
+
1046
+ if (!folderMembers || !folderMembers.length)
1047
+ throw new Error(`No members found in folder: ${folderPath}`)
1048
+
1049
+ const fileUri = folderMembers.find(
1050
+ (member) => member.name === fileName
1051
+ )?.uri
1052
+
1053
+ if (!fileUri)
1054
+ throw new Error(`File ${fileName} not found in folder: ${folderPath}`)
1055
+
1056
+ return fileUri
1057
+ }
1058
+
955
1059
  private async getRecycleBinUri(accessToken?: string) {
956
1060
  const url = '/folders/folders/@myRecycleBin'
957
1061
 
@@ -999,14 +1103,19 @@ export class SASViyaApiClient {
999
1103
  }
1000
1104
 
1001
1105
  /**
1002
- * Lists children folders for given Viya folder.
1106
+ * Lists children folders/files for given Viya folder.
1003
1107
  * @param sourceFolder - the full path (eg `/Public/example/myFolder`) or URI of the source folder listed. Providing URI instead of path will save one extra request.
1004
1108
  * @param accessToken - an access token for authorizing the request.
1109
+ * @param {Object} [options] - Additional options.
1110
+ * @param {boolean} [options.returnDetails=false] - when set to true, the function will return an array of objects with member details, otherwise it will return an array of member names.
1005
1111
  */
1006
1112
  public async listFolder(
1007
1113
  sourceFolder: string,
1008
1114
  accessToken?: string,
1009
- limit: number = 20
1115
+ limit: number = 20,
1116
+ options?: {
1117
+ returnDetails?: boolean
1118
+ }
1010
1119
  ) {
1011
1120
  // checks if 'sourceFolder' is already a URI
1012
1121
  const sourceFolderUri = isUri(sourceFolder)
@@ -1018,11 +1127,20 @@ export class SASViyaApiClient {
1018
1127
  accessToken
1019
1128
  )
1020
1129
 
1130
+ let membersToReturn = []
1131
+
1021
1132
  if (members && members.items) {
1022
- return members.items.map((item: any) => item.name)
1023
- } else {
1024
- return []
1133
+ // If returnDetails is true, return full member details
1134
+ if (options?.returnDetails) {
1135
+ membersToReturn = members.items
1136
+ } else {
1137
+ // If returnDetails is false, return only member names
1138
+ membersToReturn = members.items.map((item: any) => item.name)
1139
+ }
1025
1140
  }
1141
+
1142
+ // Return members without Etag
1143
+ return membersToReturn
1026
1144
  }
1027
1145
 
1028
1146
  /**
package/src/SASjs.ts CHANGED
@@ -411,6 +411,51 @@ export default class SASjs {
411
411
  )
412
412
  }
413
413
 
414
+ /**
415
+ * Fetches the file content for a file in the specified folder.
416
+ *
417
+ * @param folderPath - the full path to the folder containing the file. eg: /Public/folder1/folder2
418
+ * @param fileName - the name of the file in the `folderPath`
419
+ * @param accessToken - an access token for authorizing the request
420
+ */
421
+ public async getFileContent(
422
+ folderPath: string,
423
+ fileName: string,
424
+ accessToken?: string
425
+ ) {
426
+ this.isMethodSupported('getFileContent', [ServerType.SasViya])
427
+
428
+ return await this.sasViyaApiClient!.getFileContent(
429
+ folderPath,
430
+ fileName,
431
+ accessToken
432
+ )
433
+ }
434
+
435
+ /**
436
+ * Updates the file content for a file in the specified folder.
437
+ *
438
+ * @param folderPath - the full path to the folder containing the file. eg: /Public/folder1/folder2
439
+ * @param fileName - the name of the file in the `folderPath`
440
+ * @param content - the new content to be written to the file
441
+ * @param accessToken - an access token for authorizing the request
442
+ */
443
+ public async updateFileContent(
444
+ folderPath: string,
445
+ fileName: string,
446
+ content: string,
447
+ accessToken?: string
448
+ ) {
449
+ this.isMethodSupported('updateFileContent', [ServerType.SasViya])
450
+
451
+ return await this.sasViyaApiClient!.updateFileContent(
452
+ folderPath,
453
+ fileName,
454
+ content,
455
+ accessToken
456
+ )
457
+ }
458
+
414
459
  /**
415
460
  * Fetches a folder from the SAS file system.
416
461
  * @param folderPath - path of the folder to be fetched.
@@ -436,18 +481,23 @@ export default class SASjs {
436
481
  * Lists children folders for given Viya folder.
437
482
  * @param sourceFolder - the full path (eg `/Public/example/myFolder`) or URI of the source folder listed. Providing URI instead of path will save one extra request.
438
483
  * @param accessToken - an access token for authorizing the request.
484
+ * @param returnDetails - when set to true, the function will return an array of objects with member details, otherwise it will return an array of member names.
439
485
  */
440
486
  public async listFolder(
441
487
  sourceFolder: string,
442
488
  accessToken?: string,
443
- limit?: number
489
+ limit?: number,
490
+ returnDetails = false
444
491
  ) {
445
492
  this.isMethodSupported('listFolder', [ServerType.SasViya])
446
493
 
447
494
  return await this.sasViyaApiClient?.listFolder(
448
495
  sourceFolder,
449
496
  accessToken,
450
- limit
497
+ limit,
498
+ {
499
+ returnDetails
500
+ }
451
501
  )
452
502
  }
453
503
 
@@ -1170,8 +1220,8 @@ export default class SASjs {
1170
1220
  * @param errorCallBack - function that should be triggered on every HTTP response with the status different from 2**.
1171
1221
  */
1172
1222
  public enableVerboseMode(
1173
- successCallBack?: (response: AxiosResponse | AxiosError) => AxiosResponse,
1174
- errorCallBack?: (response: AxiosResponse | AxiosError) => AxiosResponse
1223
+ successCallBack?: (response: AxiosResponse) => AxiosResponse,
1224
+ errorCallBack?: (response: AxiosError) => AxiosError
1175
1225
  ) {
1176
1226
  this.requestClient?.enableVerboseMode(successCallBack, errorCallBack)
1177
1227
  }
@@ -1,4 +1,4 @@
1
- import * as NodeFormData from 'form-data'
1
+ import NodeFormData from 'form-data'
2
2
  import { AuthConfig, ServerType, ServicePackSASjs } from '@sasjs/utils/types'
3
3
  import { prefixMessage } from '@sasjs/utils/error'
4
4
  import { ExecutionQuery } from './types'
@@ -3,9 +3,14 @@ import { WriteStream } from '../../types'
3
3
  export const writeStream = async (
4
4
  stream: WriteStream,
5
5
  content: string
6
- ): Promise<void> =>
7
- stream.write(content + '\n', (e: any) => {
8
- if (e) return Promise.reject(e)
9
-
10
- return Promise.resolve()
6
+ ): Promise<boolean> => {
7
+ return new Promise((resolve, reject) => {
8
+ stream.write(content + '\n', (err: Error | null | undefined) => {
9
+ if (err) {
10
+ reject(err) // Reject on write error
11
+ } else {
12
+ resolve(true) // Resolve on successful write
13
+ }
14
+ })
11
15
  })
16
+ }
@@ -1,6 +1,6 @@
1
1
  import { SasAuthResponse, ServerType } from '@sasjs/utils/types'
2
2
  import { prefixMessage } from '@sasjs/utils/error'
3
- import * as NodeFormData from 'form-data'
3
+ import NodeFormData from 'form-data'
4
4
  import { RequestClient } from '../request/RequestClient'
5
5
  import { isNode } from '../utils'
6
6
  import { getTokenRequestErrorPrefix } from './getTokenRequestErrorPrefix'
@@ -1,4 +1,4 @@
1
- import * as NodeFormData from 'form-data'
1
+ import NodeFormData from 'form-data'
2
2
  import { convertToCSV } from '../utils/convertToCsv'
3
3
  import { isNode } from '../utils'
4
4
 
@@ -1,4 +1,4 @@
1
- import * as NodeFormData from 'form-data'
1
+ import NodeFormData from 'form-data'
2
2
  import { convertToCSV, isFormatsTable } from '../utils/convertToCsv'
3
3
  import { splitChunks } from '../utils/splitChunks'
4
4
 
@@ -1,6 +1,6 @@
1
1
  import * as https from 'https'
2
2
  import { ServerType } from '@sasjs/utils/types'
3
- import * as NodeFormData from 'form-data'
3
+ import NodeFormData from 'form-data'
4
4
  import { ErrorResponse } from '../types/errors'
5
5
  import { convertToCSV, isRelativePath } from '../utils'
6
6
  import { BaseJobExecutor } from './JobExecutor'
@@ -1,4 +1,4 @@
1
- import * as NodeFormData from 'form-data'
1
+ import NodeFormData from 'form-data'
2
2
  import {
3
3
  AuthConfig,
4
4
  ExtraResponseAttributes,
@@ -73,8 +73,10 @@ export class SasjsJobExecutor extends BaseJobExecutor {
73
73
  /* The NodeFormData object does not set the request header - so, set it */
74
74
  const contentType =
75
75
  formData instanceof NodeFormData && typeof FormData === 'undefined'
76
- ? `multipart/form-data; boundary=${formData.getBoundary()}`
77
- : undefined
76
+ ? `multipart/form-data; boundary=${
77
+ formData.getHeaders()['content-type']
78
+ }`
79
+ : 'multipart/form-data'
78
80
 
79
81
  const requestPromise = new Promise((resolve, reject) => {
80
82
  this.requestClient!.post(
@@ -1,4 +1,4 @@
1
- import * as NodeFormData from 'form-data'
1
+ import NodeFormData from 'form-data'
2
2
  import {
3
3
  AuthConfig,
4
4
  ExtraResponseAttributes,
@@ -150,8 +150,10 @@ export class WebJobExecutor extends BaseJobExecutor {
150
150
  /* The NodeFormData object does not set the request header - so, set it */
151
151
  const contentType =
152
152
  formData instanceof NodeFormData && typeof FormData === 'undefined'
153
- ? `multipart/form-data; boundary=${formData.getBoundary()}`
154
- : undefined
153
+ ? `multipart/form-data; boundary=${
154
+ formData.getHeaders()['content-type']
155
+ }`
156
+ : 'multipart/form-data'
155
157
 
156
158
  const requestPromise = new Promise((resolve, reject) => {
157
159
  this.requestClient!.post(