@sasjs/adapter 4.1.6 → 4.2.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.
- package/index-dev.js +3 -0
- package/index-dev.js.LICENSE.txt +81 -0
- package/index.js +1 -2
- package/minified/sas9/SASjs.d.ts +100 -0
- package/minified/sas9/WebJobExecutor.d.ts +14 -0
- package/minified/sas9/index.d.ts +3 -0
- package/minified_sas9.js +2 -0
- package/minified_sas9.js.LICENSE.txt +15 -0
- package/node/main.js +2 -0
- package/package.json +3 -3
- package/src/SASjs.ts +1 -3
- package/src/minified/sas9/SASjs.ts +273 -0
- package/src/minified/sas9/WebJobExecutor.ts +157 -0
- package/src/minified/sas9/index.ts +3 -0
- package/node/index.js +0 -3
- /package/node/{index.js.LICENSE.txt → main.js.LICENSE.txt} +0 -0
package/package.json
CHANGED
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"process": "0.11.10",
|
|
65
65
|
"rimraf": "3.0.2",
|
|
66
66
|
"semantic-release": "19.0.3",
|
|
67
|
-
"terser-webpack-plugin": "5.3.
|
|
67
|
+
"terser-webpack-plugin": "5.3.6",
|
|
68
68
|
"ts-jest": "27.1.3",
|
|
69
69
|
"ts-loader": "9.4.0",
|
|
70
70
|
"tslint": "6.1.3",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"typedoc": "0.23.24",
|
|
73
73
|
"typedoc-plugin-rename-defaults": "0.6.4",
|
|
74
74
|
"typescript": "4.8.3",
|
|
75
|
-
"webpack": "5.
|
|
75
|
+
"webpack": "5.76.2",
|
|
76
76
|
"webpack-cli": "4.9.2"
|
|
77
77
|
},
|
|
78
78
|
"main": "index.js",
|
|
@@ -84,5 +84,5 @@
|
|
|
84
84
|
"https": "1.0.0",
|
|
85
85
|
"tough-cookie": "4.0.0"
|
|
86
86
|
},
|
|
87
|
-
"version": "4.
|
|
87
|
+
"version": "4.2.0"
|
|
88
88
|
}
|
package/src/SASjs.ts
CHANGED
|
@@ -4,8 +4,7 @@ import {
|
|
|
4
4
|
UploadFile,
|
|
5
5
|
EditContextInput,
|
|
6
6
|
PollOptions,
|
|
7
|
-
LoginMechanism
|
|
8
|
-
ExecutionQuery
|
|
7
|
+
LoginMechanism
|
|
9
8
|
} from './types'
|
|
10
9
|
import { SASViyaApiClient } from './SASViyaApiClient'
|
|
11
10
|
import { SAS9ApiClient } from './SAS9ApiClient'
|
|
@@ -17,7 +16,6 @@ import {
|
|
|
17
16
|
AuthConfig,
|
|
18
17
|
ExtraResponseAttributes,
|
|
19
18
|
SasAuthResponse,
|
|
20
|
-
ServicePackSASjs,
|
|
21
19
|
AuthConfigSas9
|
|
22
20
|
} from '@sasjs/utils/types'
|
|
23
21
|
import { RequestClient } from './request/RequestClient'
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { validateInput, compareTimestamps } from '../../utils'
|
|
2
|
+
import { SASjsConfig, UploadFile, LoginMechanism } from '../../types'
|
|
3
|
+
import { AuthManager } from '../../auth'
|
|
4
|
+
import {
|
|
5
|
+
ServerType,
|
|
6
|
+
AuthConfig,
|
|
7
|
+
ExtraResponseAttributes
|
|
8
|
+
} from '@sasjs/utils/types'
|
|
9
|
+
import { RequestClient } from '../../request/RequestClient'
|
|
10
|
+
import { FileUploader } from '../../job-execution/FileUploader'
|
|
11
|
+
import { WebJobExecutor } from './WebJobExecutor'
|
|
12
|
+
import { ErrorResponse } from '../../types/errors/ErrorResponse'
|
|
13
|
+
import { LoginOptions, LoginResult } from '../../types/Login'
|
|
14
|
+
|
|
15
|
+
const defaultConfig: SASjsConfig = {
|
|
16
|
+
serverUrl: '',
|
|
17
|
+
pathSASJS: '/SASjsApi/stp/execute',
|
|
18
|
+
pathSAS9: '/SASStoredProcess/do',
|
|
19
|
+
pathSASViya: '/SASJobExecution',
|
|
20
|
+
appLoc: '/Public/seedapp',
|
|
21
|
+
serverType: ServerType.Sas9,
|
|
22
|
+
debug: false,
|
|
23
|
+
contextName: 'SAS Job Execution compute context',
|
|
24
|
+
useComputeApi: null,
|
|
25
|
+
loginMechanism: LoginMechanism.Default
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* SASjs is a JavaScript adapter for SAS.
|
|
30
|
+
*
|
|
31
|
+
*/
|
|
32
|
+
export default class SASjs {
|
|
33
|
+
private sasjsConfig: SASjsConfig = new SASjsConfig()
|
|
34
|
+
private jobsPath: string = ''
|
|
35
|
+
private fileUploader: FileUploader | null = null
|
|
36
|
+
private authManager: AuthManager | null = null
|
|
37
|
+
private requestClient: RequestClient | null = null
|
|
38
|
+
private webJobExecutor: WebJobExecutor | null = null
|
|
39
|
+
|
|
40
|
+
constructor(config?: Partial<SASjsConfig>) {
|
|
41
|
+
this.sasjsConfig = {
|
|
42
|
+
...defaultConfig,
|
|
43
|
+
...config
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
this.setupConfiguration()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Logs into the SAS server with the supplied credentials.
|
|
51
|
+
* @param username - a string representing the username.
|
|
52
|
+
* @param password - a string representing the password.
|
|
53
|
+
* @param clientId - a string representing the client ID.
|
|
54
|
+
*/
|
|
55
|
+
public async logIn(
|
|
56
|
+
username?: string,
|
|
57
|
+
password?: string,
|
|
58
|
+
clientId?: string,
|
|
59
|
+
options: LoginOptions = {}
|
|
60
|
+
): Promise<LoginResult> {
|
|
61
|
+
if (this.sasjsConfig.loginMechanism === LoginMechanism.Default) {
|
|
62
|
+
if (!username || !password)
|
|
63
|
+
throw new Error(
|
|
64
|
+
'A username and password are required when using the default login mechanism.'
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
return this.authManager!.logIn(username, password)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (typeof window === typeof undefined) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
'The redirected login mechanism is only available for use in the browser.'
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return this.authManager!.redirectedLogIn(options)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Logs out of the configured SAS server.
|
|
81
|
+
*/
|
|
82
|
+
public logOut() {
|
|
83
|
+
return this.authManager!.logOut()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Returns the current SASjs configuration.
|
|
88
|
+
*
|
|
89
|
+
*/
|
|
90
|
+
public getSasjsConfig() {
|
|
91
|
+
return this.sasjsConfig
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* this method returns an array of SASjsRequest
|
|
96
|
+
* @returns SASjsRequest[]
|
|
97
|
+
*/
|
|
98
|
+
public getSasRequests() {
|
|
99
|
+
const requests = [...this.requestClient!.getRequests()]
|
|
100
|
+
const sortedRequests = requests.sort(compareTimestamps)
|
|
101
|
+
return sortedRequests
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Sets the debug state. Turning this on will enable additional logging in the adapter.
|
|
106
|
+
* @param value - boolean indicating debug state (on/off).
|
|
107
|
+
*/
|
|
108
|
+
public setDebugState(value: boolean) {
|
|
109
|
+
this.sasjsConfig.debug = value
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Uploads a file to the given service.
|
|
114
|
+
* @param sasJob - the path to the SAS program (ultimately resolves to
|
|
115
|
+
* the SAS `_program` parameter to run a Job Definition or SAS 9 Stored
|
|
116
|
+
* Process). Is prepended at runtime with the value of `appLoc`.
|
|
117
|
+
* @param files - array of files to be uploaded, including File object and file name.
|
|
118
|
+
* @param params - request URL parameters.
|
|
119
|
+
* @param config - provide any changes to the config here, for instance to
|
|
120
|
+
* enable/disable `debug`. Any change provided will override the global config,
|
|
121
|
+
* for that particular function call.
|
|
122
|
+
* @param loginRequiredCallback - a function that is called if the
|
|
123
|
+
* user is not logged in (eg to display a login form). The request will be
|
|
124
|
+
* resubmitted after successful login.
|
|
125
|
+
*/
|
|
126
|
+
public async uploadFile(
|
|
127
|
+
sasJob: string,
|
|
128
|
+
files: UploadFile[],
|
|
129
|
+
params: { [key: string]: any } | null,
|
|
130
|
+
config: { [key: string]: any } = {},
|
|
131
|
+
loginRequiredCallback?: () => any
|
|
132
|
+
) {
|
|
133
|
+
config = {
|
|
134
|
+
...this.sasjsConfig,
|
|
135
|
+
...config
|
|
136
|
+
}
|
|
137
|
+
const data = { files, params }
|
|
138
|
+
|
|
139
|
+
return await this.fileUploader!.execute(
|
|
140
|
+
sasJob,
|
|
141
|
+
data,
|
|
142
|
+
config,
|
|
143
|
+
loginRequiredCallback
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Makes a request to program specified in `SASjob` (could be a Viya Job, a
|
|
149
|
+
* SAS 9 Stored Process, or a SASjs Server Stored Program). The response
|
|
150
|
+
* object will always contain table names in lowercase, and column names in
|
|
151
|
+
* uppercase. Values are returned formatted by default, unformatted
|
|
152
|
+
* values can be configured as an option in the `%webout` macro.
|
|
153
|
+
*
|
|
154
|
+
* @param sasJob - the path to the SAS program (ultimately resolves to
|
|
155
|
+
* the SAS `_program` parameter to run a Job Definition or SAS 9 Stored
|
|
156
|
+
* Process). Is prepended at runtime with the value of `appLoc`.
|
|
157
|
+
* @param data - a JSON object containing one or more tables to be sent to
|
|
158
|
+
* SAS. For an example of the table structure, see the project README. This
|
|
159
|
+
* value can be `null` if no inputs are required.
|
|
160
|
+
* @param config - provide any changes to the config here, for instance to
|
|
161
|
+
* enable/disable `debug`. Any change provided will override the global config,
|
|
162
|
+
* for that particular function call.
|
|
163
|
+
* @param loginRequiredCallback - a function that is called if the
|
|
164
|
+
* user is not logged in (eg to display a login form). The request will be
|
|
165
|
+
* resubmitted after successful login.
|
|
166
|
+
* When using a `loginRequiredCallback`, the call to the request will look, for example, like so:
|
|
167
|
+
* `await request(sasJobPath, data, config, () => setIsLoggedIn(false))`
|
|
168
|
+
* If you are not passing in any data and configuration, it will look like so:
|
|
169
|
+
* `await request(sasJobPath, {}, {}, () => setIsLoggedIn(false))`
|
|
170
|
+
* @param extraResponseAttributes - a array of predefined values that are used
|
|
171
|
+
* to provide extra attributes (same names as those values) to be added in response
|
|
172
|
+
* Supported values are declared in ExtraResponseAttributes type.
|
|
173
|
+
*/
|
|
174
|
+
public async request(
|
|
175
|
+
sasJob: string,
|
|
176
|
+
data: { [key: string]: any } | null,
|
|
177
|
+
config: { [key: string]: any } = {},
|
|
178
|
+
loginRequiredCallback?: () => any,
|
|
179
|
+
authConfig?: AuthConfig,
|
|
180
|
+
extraResponseAttributes: ExtraResponseAttributes[] = []
|
|
181
|
+
) {
|
|
182
|
+
config = {
|
|
183
|
+
...this.sasjsConfig,
|
|
184
|
+
...config
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const validationResult = validateInput(data)
|
|
188
|
+
|
|
189
|
+
// status is true if the data passes validation checks above
|
|
190
|
+
if (validationResult.status) {
|
|
191
|
+
return await this.webJobExecutor!.execute(
|
|
192
|
+
sasJob,
|
|
193
|
+
data,
|
|
194
|
+
config,
|
|
195
|
+
loginRequiredCallback,
|
|
196
|
+
authConfig,
|
|
197
|
+
extraResponseAttributes
|
|
198
|
+
)
|
|
199
|
+
} else {
|
|
200
|
+
return Promise.reject(new ErrorResponse(validationResult.msg))
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Checks whether a session is active, or login is required.
|
|
206
|
+
* @returns - a promise which resolves with an object containing two values - a boolean `isLoggedIn`, and a string `userName`.
|
|
207
|
+
*/
|
|
208
|
+
public async checkSession() {
|
|
209
|
+
return this.authManager!.checkSession()
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private setupConfiguration() {
|
|
213
|
+
if (
|
|
214
|
+
this.sasjsConfig.serverUrl === undefined ||
|
|
215
|
+
this.sasjsConfig.serverUrl === ''
|
|
216
|
+
) {
|
|
217
|
+
if (typeof location !== 'undefined') {
|
|
218
|
+
let url = `${location.protocol}//${location.hostname}`
|
|
219
|
+
|
|
220
|
+
if (location.port) url = `${url}:${location.port}`
|
|
221
|
+
|
|
222
|
+
this.sasjsConfig.serverUrl = url
|
|
223
|
+
} else {
|
|
224
|
+
this.sasjsConfig.serverUrl = ''
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (this.sasjsConfig.serverUrl.slice(-1) === '/') {
|
|
229
|
+
this.sasjsConfig.serverUrl = this.sasjsConfig.serverUrl.slice(0, -1)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (!this.requestClient) {
|
|
233
|
+
this.requestClient = new RequestClient(
|
|
234
|
+
this.sasjsConfig.serverUrl,
|
|
235
|
+
this.sasjsConfig.httpsAgentOptions,
|
|
236
|
+
this.sasjsConfig.requestHistoryLimit
|
|
237
|
+
)
|
|
238
|
+
} else {
|
|
239
|
+
this.requestClient.setConfig(
|
|
240
|
+
this.sasjsConfig.serverUrl,
|
|
241
|
+
this.sasjsConfig.httpsAgentOptions
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
this.jobsPath = this.sasjsConfig.pathSAS9
|
|
246
|
+
|
|
247
|
+
this.authManager = new AuthManager(
|
|
248
|
+
this.sasjsConfig.serverUrl,
|
|
249
|
+
this.sasjsConfig.serverType!,
|
|
250
|
+
this.requestClient,
|
|
251
|
+
this.resendWaitingRequests
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
this.fileUploader = new FileUploader(
|
|
255
|
+
this.sasjsConfig.serverUrl,
|
|
256
|
+
this.sasjsConfig.serverType!,
|
|
257
|
+
this.jobsPath,
|
|
258
|
+
this.requestClient
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
this.webJobExecutor = new WebJobExecutor(
|
|
262
|
+
this.sasjsConfig.serverUrl,
|
|
263
|
+
this.sasjsConfig.serverType!,
|
|
264
|
+
this.jobsPath,
|
|
265
|
+
this.requestClient
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private resendWaitingRequests = async () => {
|
|
270
|
+
await this.webJobExecutor?.resendWaitingRequests()
|
|
271
|
+
await this.fileUploader?.resendWaitingRequests()
|
|
272
|
+
}
|
|
273
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AuthConfig,
|
|
3
|
+
ExtraResponseAttributes,
|
|
4
|
+
ServerType
|
|
5
|
+
} from '@sasjs/utils/types'
|
|
6
|
+
import {
|
|
7
|
+
ErrorResponse,
|
|
8
|
+
JobExecutionError,
|
|
9
|
+
LoginRequiredError
|
|
10
|
+
} from '../../types/errors'
|
|
11
|
+
import { RequestClient } from '../../request/RequestClient'
|
|
12
|
+
import {
|
|
13
|
+
isRelativePath,
|
|
14
|
+
parseSasViyaDebugResponse,
|
|
15
|
+
appendExtraResponseAttributes,
|
|
16
|
+
convertToCSV
|
|
17
|
+
} from '../../utils'
|
|
18
|
+
import { BaseJobExecutor } from '../../job-execution/JobExecutor'
|
|
19
|
+
import { parseWeboutResponse } from '../../utils/parseWeboutResponse'
|
|
20
|
+
|
|
21
|
+
export interface WaitingRequstPromise {
|
|
22
|
+
promise: Promise<any> | null
|
|
23
|
+
resolve: any
|
|
24
|
+
reject: any
|
|
25
|
+
}
|
|
26
|
+
export class WebJobExecutor extends BaseJobExecutor {
|
|
27
|
+
constructor(
|
|
28
|
+
serverUrl: string,
|
|
29
|
+
serverType: ServerType,
|
|
30
|
+
private jobsPath: string,
|
|
31
|
+
private requestClient: RequestClient
|
|
32
|
+
) {
|
|
33
|
+
super(serverUrl, serverType)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async execute(
|
|
37
|
+
sasJob: string,
|
|
38
|
+
data: any,
|
|
39
|
+
config: any,
|
|
40
|
+
loginRequiredCallback?: any,
|
|
41
|
+
authConfig?: AuthConfig,
|
|
42
|
+
extraResponseAttributes: ExtraResponseAttributes[] = []
|
|
43
|
+
) {
|
|
44
|
+
const loginCallback = loginRequiredCallback
|
|
45
|
+
const program = isRelativePath(sasJob)
|
|
46
|
+
? config.appLoc
|
|
47
|
+
? config.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
|
|
48
|
+
: sasJob
|
|
49
|
+
: sasJob
|
|
50
|
+
let apiUrl = `${config.serverUrl}${this.jobsPath}/?${'_program=' + program}`
|
|
51
|
+
|
|
52
|
+
let requestParams = {
|
|
53
|
+
...this.getRequestParams(config)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let formData = new FormData()
|
|
57
|
+
|
|
58
|
+
if (data) {
|
|
59
|
+
try {
|
|
60
|
+
formData = generateFileUploadForm(formData, data)
|
|
61
|
+
} catch (e: any) {
|
|
62
|
+
return Promise.reject(new ErrorResponse(e?.message, e))
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (const key in requestParams) {
|
|
67
|
+
if (requestParams.hasOwnProperty(key)) {
|
|
68
|
+
formData.append(key, requestParams[key])
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const requestPromise = new Promise((resolve, reject) => {
|
|
73
|
+
this.requestClient!.post(apiUrl, formData, authConfig?.access_token)
|
|
74
|
+
.then(async (res: any) => {
|
|
75
|
+
this.requestClient!.appendRequest(res, sasJob, config.debug)
|
|
76
|
+
|
|
77
|
+
const jsonResponse =
|
|
78
|
+
config.debug && typeof res.result === 'string'
|
|
79
|
+
? parseWeboutResponse(res.result, apiUrl)
|
|
80
|
+
: res.result
|
|
81
|
+
|
|
82
|
+
const responseObject = appendExtraResponseAttributes(
|
|
83
|
+
{ result: jsonResponse, log: res.log },
|
|
84
|
+
extraResponseAttributes
|
|
85
|
+
)
|
|
86
|
+
resolve(responseObject)
|
|
87
|
+
})
|
|
88
|
+
.catch(async (e: Error) => {
|
|
89
|
+
if (e instanceof JobExecutionError) {
|
|
90
|
+
this.requestClient!.appendRequest(e, sasJob, config.debug)
|
|
91
|
+
reject(new ErrorResponse(e?.message, e))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (e instanceof LoginRequiredError) {
|
|
95
|
+
if (!loginRequiredCallback) {
|
|
96
|
+
reject(
|
|
97
|
+
new ErrorResponse(
|
|
98
|
+
'Request is not authenticated. Make sure .env file exists with valid credentials.',
|
|
99
|
+
e
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this.appendWaitingRequest(() => {
|
|
105
|
+
return this.execute(
|
|
106
|
+
sasJob,
|
|
107
|
+
data,
|
|
108
|
+
config,
|
|
109
|
+
loginRequiredCallback,
|
|
110
|
+
authConfig,
|
|
111
|
+
extraResponseAttributes
|
|
112
|
+
).then(
|
|
113
|
+
(res: any) => {
|
|
114
|
+
resolve(res)
|
|
115
|
+
},
|
|
116
|
+
(err: any) => {
|
|
117
|
+
reject(err)
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
if (loginCallback) await loginCallback()
|
|
123
|
+
} else reject(new ErrorResponse(e?.message, e))
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
return requestPromise
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* One of the approaches SASjs takes to send tables-formatted JSON (see README)
|
|
133
|
+
* to SAS is as multipart form data, where each table is provided as a specially
|
|
134
|
+
* formatted CSV file.
|
|
135
|
+
*/
|
|
136
|
+
const generateFileUploadForm = (formData: FormData, data: any): FormData => {
|
|
137
|
+
for (const tableName in data) {
|
|
138
|
+
if (!Array.isArray(data[tableName])) continue
|
|
139
|
+
|
|
140
|
+
const name = tableName
|
|
141
|
+
const csv = convertToCSV(data, tableName)
|
|
142
|
+
|
|
143
|
+
if (csv === 'ERROR: LARGE STRING LENGTH') {
|
|
144
|
+
throw new Error(
|
|
145
|
+
'The max length of a string value in SASjs is 32765 characters.'
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const file = new Blob([csv], {
|
|
150
|
+
type: 'application/csv'
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
formData.append(name, file, `${name}.csv`)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return formData
|
|
157
|
+
}
|