@sasjs/adapter 4.1.5 → 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/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.1",
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.69.0",
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.1.5"
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
+ }
@@ -0,0 +1,3 @@
1
+ import SASjs from './SASjs'
2
+ export * from '../../types'
3
+ export default SASjs
@@ -4,6 +4,7 @@ import axiosCookieJarSupport from 'axios-cookiejar-support'
4
4
  import * as tough from 'tough-cookie'
5
5
  import { prefixMessage } from '@sasjs/utils/error'
6
6
  import { RequestClient, throwIfError } from './RequestClient'
7
+ import { JobExecutionError } from '../types/errors'
7
8
 
8
9
  /**
9
10
  * Specific request client for SAS9 in Node.js environments.
@@ -69,6 +70,8 @@ export class Sas9RequestClient extends RequestClient {
69
70
  return this.parseResponse<T>(response)
70
71
  })
71
72
  .catch(async (e: any) => {
73
+ if (e instanceof JobExecutionError) throw e
74
+
72
75
  return await this.handleError(
73
76
  e,
74
77
  () =>