@sasjs/adapter 4.16.7 → 4.17.1

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.
@@ -13,5 +13,6 @@ export declare class WebJobExecutor extends BaseJobExecutor {
13
13
  private sasViyaApiClient;
14
14
  constructor(serverUrl: string, serverType: ServerType, jobsPath: string, requestClient: RequestClient, sasViyaApiClient: SASViyaApiClient);
15
15
  execute(sasJob: string, data: any, config: any, loginRequiredCallback?: any, authConfig?: AuthConfig, extraResponseAttributes?: ExtraResponseAttributes[]): Promise<unknown>;
16
+ protected getRequestParams(config: any): any;
16
17
  private getJobUri;
17
18
  }
@@ -12,12 +12,15 @@ export declare class RequestClient implements HttpClient {
12
12
  protected csrfToken: CsrfToken;
13
13
  protected fileUploadCsrfToken: CsrfToken | undefined;
14
14
  protected httpClient: AxiosInstance;
15
+ private isRecoveringFromNetworkError;
15
16
  constructor(baseUrl: string, httpsAgentOptions?: https.AgentOptions, requestsLimit?: number, verboseMode?: VerboseMode);
16
17
  setConfig(baseUrl: string, httpsAgentOptions?: https.AgentOptions): void;
17
18
  saveLocalStorageToken(accessToken: string, refreshToken: string): void;
18
19
  getCsrfToken(type?: 'general' | 'file'): CsrfToken | undefined;
19
20
  clearCsrfTokens(): void;
20
21
  clearLocalStorageTokens(): void;
22
+ resetInMemoryAuthState(): void;
23
+ private clearAllCookies;
21
24
  getBaseUrl(): string;
22
25
  /**
23
26
  * this method returns all requests, an array of SASjsRequest type
@@ -7,6 +7,7 @@ import { RequestClient } from './RequestClient';
7
7
  */
8
8
  export declare class Sas9RequestClient extends RequestClient {
9
9
  constructor(baseUrl: string, httpsAgentOptions?: https.AgentOptions);
10
+ resetInMemoryAuthState(): void;
10
11
  login(username: string, password: string, jobsPath: string): Promise<void>;
11
12
  get<T>(url: string, accessToken: string | undefined, contentType?: string, overrideHeaders?: {
12
13
  [key: string]: string | number;
@@ -79,6 +79,14 @@ export declare class SASjsConfig {
79
79
  * may affect browser performance, especially with debug (logs) enabled.
80
80
  */
81
81
  requestHistoryLimit?: number;
82
+ /**
83
+ * Optional setting. When `true`, the request runs as a Viya execution task —
84
+ * appends `_executionTasks=true` to the request URL. Only applies to the Viya
85
+ * web jobs path, i.e. when `serverType === SASVIYA` AND
86
+ * `useComputeApi` is `null`/`undefined`. Has no effect when `useComputeApi`
87
+ * is explicitly set to `true` or `false`.
88
+ */
89
+ runAsTask?: boolean;
82
90
  }
83
91
  export declare enum LoginMechanism {
84
92
  Default = "Default",
@@ -15,6 +15,7 @@ export * from './parseGeneratedCode';
15
15
  export * from './parseSasViyaLog';
16
16
  export * from './parseSourceCode';
17
17
  export * from './parseViyaDebugResponse';
18
+ export * from './parseViyaLogDebugResponse';
18
19
  export * from './parseWeboutResponse';
19
20
  export * from './serialize';
20
21
  export * from './splitChunks';
@@ -0,0 +1,10 @@
1
+ /**
2
+ * When querying a Viya job using the Web approach with _DEBUG=128 (used when
3
+ * runAsTask is true), the webout JSON is inlined into the response via:
4
+ * var blob = new Blob([`{...}`], {type: 'application/json'});
5
+ * On abort/error paths the same shape is used but with text/plain and
6
+ * weboutBEGIN/END markers around the JSON:
7
+ * var blob = new Blob([`>>weboutBEGIN<<\n{...}\n>>weboutEND<<\n`], {type: 'text/plain'});
8
+ * No follow-up request is needed — extract and parse the JSON directly.
9
+ */
10
+ export declare const parseSasViyaLogDebugResponse: (response: any) => Promise<any>;
package/package.json CHANGED
@@ -23,10 +23,24 @@
23
23
  },
24
24
  "release": {
25
25
  "plugins": [
26
- "@semantic-release/npm",
27
- {
28
- "pkgRoot": "/build"
29
- }
26
+ "@semantic-release/commit-analyzer",
27
+ "@semantic-release/release-notes-generator",
28
+ [
29
+ "@semantic-release/exec",
30
+ {
31
+ "prepareCmd": "npm version ${nextRelease.version} --no-git-tag-version"
32
+ }
33
+ ],
34
+ [
35
+ "@semantic-release/git",
36
+ {
37
+ "assets": [
38
+ "package.json",
39
+ "package-lock.json"
40
+ ]
41
+ }
42
+ ],
43
+ "@semantic-release/github"
30
44
  ]
31
45
  },
32
46
  "keywords": [
@@ -34,14 +48,16 @@
34
48
  "viya",
35
49
  "sasjs"
36
50
  ],
37
- "author": "Allan Bowe <support@macropeople.com>",
51
+ "author": "support@4gl.io",
38
52
  "repository": {
39
53
  "type": "git",
40
- "url": "https://github.com/sasjs/adapter"
54
+ "url": "git+https://github.com/sasjs/adapter.git"
41
55
  },
42
56
  "license": "ISC",
43
57
  "devDependencies": {
44
58
  "@cypress/webpack-preprocessor": "5.9.1",
59
+ "@semantic-release/exec": "6.0.3",
60
+ "@semantic-release/git": "10.0.1",
45
61
  "@types/cors": "^2.8.17",
46
62
  "@types/express": "4.17.13",
47
63
  "@types/jest": "29.5.14",
@@ -79,9 +95,9 @@
79
95
  "@sasjs/utils": "^3.5.6",
80
96
  "axios": "1.16.0",
81
97
  "axios-cookiejar-support": "5.0.5",
82
- "form-data": "4.0.4",
98
+ "form-data": "4.0.6",
83
99
  "https": "1.0.0",
84
100
  "tough-cookie": "4.1.3"
85
101
  },
86
- "version": "4.16.7"
102
+ "version": "4.17.1"
87
103
  }
@@ -12,12 +12,15 @@ export declare class RequestClient implements HttpClient {
12
12
  protected csrfToken: CsrfToken;
13
13
  protected fileUploadCsrfToken: CsrfToken | undefined;
14
14
  protected httpClient: AxiosInstance;
15
+ private isRecoveringFromNetworkError;
15
16
  constructor(baseUrl: string, httpsAgentOptions?: https.AgentOptions, requestsLimit?: number, verboseMode?: VerboseMode);
16
17
  setConfig(baseUrl: string, httpsAgentOptions?: https.AgentOptions): void;
17
18
  saveLocalStorageToken(accessToken: string, refreshToken: string): void;
18
19
  getCsrfToken(type?: 'general' | 'file'): CsrfToken | undefined;
19
20
  clearCsrfTokens(): void;
20
21
  clearLocalStorageTokens(): void;
22
+ resetInMemoryAuthState(): void;
23
+ private clearAllCookies;
21
24
  getBaseUrl(): string;
22
25
  /**
23
26
  * this method returns all requests, an array of SASjsRequest type
@@ -7,6 +7,7 @@ import { RequestClient } from './RequestClient';
7
7
  */
8
8
  export declare class Sas9RequestClient extends RequestClient {
9
9
  constructor(baseUrl: string, httpsAgentOptions?: https.AgentOptions);
10
+ resetInMemoryAuthState(): void;
10
11
  login(username: string, password: string, jobsPath: string): Promise<void>;
11
12
  get<T>(url: string, accessToken: string | undefined, contentType?: string, overrideHeaders?: {
12
13
  [key: string]: string | number;
@@ -375,7 +375,7 @@ export class AuthManager {
375
375
  *
376
376
  */
377
377
  public async logOut() {
378
- this.requestClient.clearCsrfTokens()
378
+ this.requestClient.resetInMemoryAuthState()
379
379
 
380
380
  return this.requestClient.get(this.logoutUrl, undefined).then(() => true)
381
381
  }
@@ -11,6 +11,13 @@ export const generateTableUploadForm = (
11
11
  let tableCounter = 0
12
12
 
13
13
  for (const tableName in data) {
14
+ if (
15
+ isFormatsTable(tableName) &&
16
+ Object.keys(data).includes(tableName.replace(/^\$/, ''))
17
+ ) {
18
+ continue
19
+ }
20
+
14
21
  tableCounter++
15
22
 
16
23
  // Formats table should not be sent as part of 'sasjs_tables'
@@ -55,7 +55,6 @@ export abstract class BaseJobExecutor implements JobExecutor {
55
55
  if (config.debug) {
56
56
  requestParams['_omittextlog'] = 'false'
57
57
  requestParams['_omitSessionResults'] = 'false'
58
-
59
58
  requestParams['_debug'] = 131
60
59
  }
61
60
 
@@ -16,9 +16,11 @@ import { SASViyaApiClient } from '../SASViyaApiClient'
16
16
  import {
17
17
  isRelativePath,
18
18
  parseSasViyaDebugResponse,
19
+ parseSasViyaLogDebugResponse,
19
20
  appendExtraResponseAttributes,
20
21
  parseWeboutResponse,
21
- getFormData
22
+ getFormData,
23
+ isNode
22
24
  } from '../utils'
23
25
  import { BaseJobExecutor } from './JobExecutor'
24
26
 
@@ -101,6 +103,10 @@ export class WebJobExecutor extends BaseJobExecutor {
101
103
  apiUrl += config.contextName?.trim()
102
104
  ? `&_contextname=${encodeURIComponent(config.contextName)}`
103
105
  : ''
106
+
107
+ if (config.runAsTask === true) {
108
+ apiUrl += '&_executionTasks=true'
109
+ }
104
110
  }
105
111
 
106
112
  let requestParams = {
@@ -113,6 +119,26 @@ export class WebJobExecutor extends BaseJobExecutor {
113
119
  */
114
120
  let formData = getFormData()
115
121
 
122
+ // FIXME(viya - SAS Track CS0409737): remove when Viya stops rejecting empty multipart on
123
+ // _executionTasks=true. Dummy file keeps the body non-empty
124
+ const hasExecutionTasksFlag = config.runAsTask === true
125
+
126
+ // Move debug params to URL for viya; viya seems to honor them more
127
+ // reliably in the query string than the multipart body.
128
+ if (
129
+ hasExecutionTasksFlag &&
130
+ config.debug &&
131
+ config.serverType === ServerType.SasViya
132
+ ) {
133
+ const debugKeys = ['_debug', '_omitSessionResults']
134
+ debugKeys.forEach((key) => {
135
+ if (requestParams[key] !== undefined) {
136
+ apiUrl += `&${key}=${encodeURIComponent(requestParams[key])}`
137
+ delete requestParams[key]
138
+ }
139
+ })
140
+ }
141
+
116
142
  if (data) {
117
143
  const stringifiedData = JSON.stringify(data)
118
144
  if (
@@ -133,10 +159,21 @@ export class WebJobExecutor extends BaseJobExecutor {
133
159
  generateTableUploadForm(formData, data)
134
160
  formData = newFormData
135
161
  requestParams = { ...requestParams, ...params }
162
+
163
+ if (
164
+ config.serverType === ServerType.SasViya &&
165
+ hasExecutionTasksFlag
166
+ ) {
167
+ addDummyFile(formData)
168
+ }
136
169
  } catch (e: any) {
137
170
  return Promise.reject(new ErrorResponse(e?.message, e))
138
171
  }
139
172
  }
173
+ } else {
174
+ if (config.serverType === ServerType.SasViya && hasExecutionTasksFlag) {
175
+ addDummyFile(formData)
176
+ }
140
177
  }
141
178
 
142
179
  for (const key in requestParams) {
@@ -168,11 +205,14 @@ export class WebJobExecutor extends BaseJobExecutor {
168
205
  if (config.debug) {
169
206
  switch (this.serverType) {
170
207
  case ServerType.SasViya:
171
- jsonResponse = await parseSasViyaDebugResponse(
172
- res.result,
173
- this.requestClient,
174
- this.serverUrl
175
- )
208
+ jsonResponse =
209
+ config.useComputeApi === null && config.runAsTask === true
210
+ ? await parseSasViyaLogDebugResponse(res.result)
211
+ : await parseSasViyaDebugResponse(
212
+ res.result,
213
+ this.requestClient,
214
+ this.serverUrl
215
+ )
176
216
  break
177
217
  case ServerType.Sas9:
178
218
  jsonResponse =
@@ -231,6 +271,23 @@ export class WebJobExecutor extends BaseJobExecutor {
231
271
  return requestPromise
232
272
  }
233
273
 
274
+ protected getRequestParams(config: any): any {
275
+ const requestParams = super.getRequestParams(config)
276
+
277
+ // FIXME(viya - possible issue with default debug flags)
278
+ // runAsTask on Viya: use _debug=128 (not 131) and omit _omittextlog
279
+ if (
280
+ config.debug &&
281
+ config.serverType === ServerType.SasViya &&
282
+ config.runAsTask === true
283
+ ) {
284
+ requestParams['_debug'] = 128
285
+ delete requestParams['_omittextlog']
286
+ }
287
+
288
+ return requestParams
289
+ }
290
+
234
291
  private async getJobUri(sasJob: string) {
235
292
  if (!this.sasViyaApiClient) return ''
236
293
  let uri = ''
@@ -263,3 +320,18 @@ export class WebJobExecutor extends BaseJobExecutor {
263
320
  return uri
264
321
  }
265
322
  }
323
+
324
+ function addDummyFile(formData: NodeFormData | FormData) {
325
+ if (isNode()) {
326
+ ;(formData as NodeFormData).append('_sasjs_noop', '', {
327
+ filename: '_sasjs_noop.txt',
328
+ contentType: 'text/plain'
329
+ })
330
+ } else {
331
+ ;(formData as FormData).append(
332
+ '_sasjs_noop',
333
+ new Blob([''], { type: 'text/plain' }),
334
+ '_sasjs_noop.txt'
335
+ )
336
+ }
337
+ }
@@ -28,6 +28,9 @@ import {
28
28
  import { InvalidSASjsCsrfError } from '../types/errors/InvalidSASjsCsrfError'
29
29
  import { inspect } from 'util'
30
30
 
31
+ const getLogger = () =>
32
+ (typeof process !== 'undefined' && process.logger) || console
33
+
31
34
  export class RequestClient implements HttpClient {
32
35
  private requests: SASjsRequest[] = []
33
36
  private requestsLimit: number = 10
@@ -37,6 +40,7 @@ export class RequestClient implements HttpClient {
37
40
  protected csrfToken: CsrfToken = { headerName: '', value: '' }
38
41
  protected fileUploadCsrfToken: CsrfToken | undefined
39
42
  protected httpClient!: AxiosInstance
43
+ private isRecoveringFromNetworkError = false
40
44
 
41
45
  constructor(
42
46
  protected baseUrl: string,
@@ -77,6 +81,25 @@ export class RequestClient implements HttpClient {
77
81
  localStorage.setItem('refreshToken', '')
78
82
  }
79
83
 
84
+ public resetInMemoryAuthState() {
85
+ this.clearCsrfTokens()
86
+ if (typeof localStorage !== 'undefined') {
87
+ this.clearLocalStorageTokens()
88
+ }
89
+ if (typeof document !== 'undefined') {
90
+ this.clearAllCookies()
91
+ }
92
+ }
93
+
94
+ private clearAllCookies() {
95
+ const cookies = document.cookie.split(';')
96
+ for (const cookie of cookies) {
97
+ const name = cookie.split('=')[0].trim()
98
+ if (!name) continue
99
+ document.cookie = `${name}=; Max-Age=0; Path=/;`
100
+ }
101
+ }
102
+
80
103
  public getBaseUrl() {
81
104
  return this.httpClient.defaults.baseURL || ''
82
105
  }
@@ -354,6 +377,7 @@ export class RequestClient implements HttpClient {
354
377
  const csrfTokenKey = Object.keys(params).find((k) =>
355
378
  k?.toLowerCase().includes('csrf')
356
379
  )
380
+
357
381
  if (csrfTokenKey) {
358
382
  this.csrfToken.value = params[csrfTokenKey]
359
383
  this.csrfToken.headerName = this.csrfToken.headerName || 'x-csrf-token'
@@ -378,7 +402,7 @@ export class RequestClient implements HttpClient {
378
402
  })
379
403
  .then((res) => res.data)
380
404
  .catch((error) => {
381
- const logger = process.logger || console
405
+ const logger = getLogger()
382
406
  logger.error(error)
383
407
  })
384
408
  }
@@ -687,6 +711,45 @@ ${resHeaders}${parsedResBody ? `\n\n${parsedResBody}` : ''}
687
711
  throw new CertificateError(e.message)
688
712
  }
689
713
 
714
+ if (
715
+ e.isAxiosError &&
716
+ !response &&
717
+ e.code === 'ERR_NETWORK' &&
718
+ !this.isRecoveringFromNetworkError
719
+ ) {
720
+ // Opaque ERR_NETWORK usually means the server rejected stale credentials.
721
+ // Wipe in-memory auth state, re-establish session via GET /,
722
+ // then retry the original request.
723
+ this.resetInMemoryAuthState()
724
+ this.isRecoveringFromNetworkError = true
725
+ try {
726
+ // Re-establish session and CSRF cookie
727
+ const rootResponse = await this.httpClient
728
+ .get('/', { withXSRFToken: true })
729
+ .catch((err) => err.response)
730
+
731
+ if (rootResponse?.data) {
732
+ const cookie =
733
+ /<script>document.cookie = '(XSRF-TOKEN=.*; Max-Age=86400; SameSite=Strict; Path=\/;)'<\/script>/.exec(
734
+ rootResponse.data
735
+ )?.[1]
736
+
737
+ if (cookie && typeof document !== 'undefined') {
738
+ document.cookie = cookie
739
+ }
740
+
741
+ this.parseAndSetCsrfToken(rootResponse)
742
+ }
743
+
744
+ return await callback()
745
+ } catch {
746
+ // Session could not be recovered — surface LoginRequiredError
747
+ throw new LoginRequiredError()
748
+ } finally {
749
+ this.isRecoveringFromNetworkError = false
750
+ }
751
+ }
752
+
690
753
  if (e.message) throw e
691
754
  else throw prefixMessage(e, 'Error while handling error. ')
692
755
  }
@@ -23,6 +23,13 @@ export class Sas9RequestClient extends RequestClient {
23
23
  }
24
24
  }
25
25
 
26
+ public resetInMemoryAuthState() {
27
+ super.resetInMemoryAuthState()
28
+ if (this.httpClient.defaults.jar) {
29
+ ;(this.httpClient.defaults.jar as tough.CookieJar).removeAllCookiesSync()
30
+ }
31
+ }
32
+
26
33
  public async login(username: string, password: string, jobsPath: string) {
27
34
  const codeInjectorPath = `/User Folders/${username}/My Folder/sasjs/runner`
28
35
  if (this.httpClient.defaults.jar) {
@@ -79,6 +79,14 @@ export class SASjsConfig {
79
79
  * may affect browser performance, especially with debug (logs) enabled.
80
80
  */
81
81
  requestHistoryLimit?: number = 10
82
+ /**
83
+ * Optional setting. When `true`, the request runs as a Viya execution task —
84
+ * appends `_executionTasks=true` to the request URL. Only applies to the Viya
85
+ * web jobs path, i.e. when `serverType === SASVIYA` AND
86
+ * `useComputeApi` is `null`/`undefined`. Has no effect when `useComputeApi`
87
+ * is explicitly set to `true` or `false`.
88
+ */
89
+ runAsTask?: boolean = false
82
90
  }
83
91
 
84
92
  export enum LoginMechanism {
@@ -15,6 +15,7 @@ export * from './parseGeneratedCode'
15
15
  export * from './parseSasViyaLog'
16
16
  export * from './parseSourceCode'
17
17
  export * from './parseViyaDebugResponse'
18
+ export * from './parseViyaLogDebugResponse'
18
19
  export * from './parseWeboutResponse'
19
20
  export * from './serialize'
20
21
  export * from './splitChunks'
@@ -0,0 +1,32 @@
1
+ import { getValidJson } from './getValidJson'
2
+ import { parseWeboutResponse } from './parseWeboutResponse'
3
+
4
+ /**
5
+ * When querying a Viya job using the Web approach with _DEBUG=128 (used when
6
+ * runAsTask is true), the webout JSON is inlined into the response via:
7
+ * var blob = new Blob([`{...}`], {type: 'application/json'});
8
+ * On abort/error paths the same shape is used but with text/plain and
9
+ * weboutBEGIN/END markers around the JSON:
10
+ * var blob = new Blob([`>>weboutBEGIN<<\n{...}\n>>weboutEND<<\n`], {type: 'text/plain'});
11
+ * No follow-up request is needed — extract and parse the JSON directly.
12
+ */
13
+ export const parseSasViyaLogDebugResponse = async (response: any) => {
14
+ // If upstream already parsed the response as JSON (object), pass through.
15
+ if (typeof response !== 'string') {
16
+ return response
17
+ }
18
+
19
+ const blobMatch = response.match(
20
+ /new Blob\(\[`([\s\S]*?)`\],\s*\{type:\s*'(?:application\/json|text\/plain)'\}\)/
21
+ )
22
+ if (!blobMatch) {
23
+ throw new Error('Unable to find webout blob in debug log response.')
24
+ }
25
+
26
+ const blobContent = blobMatch[1]
27
+ const stripped = blobContent.includes('>>weboutBEGIN<<')
28
+ ? parseWeboutResponse(blobContent)
29
+ : blobContent
30
+
31
+ return getValidJson(stripped)
32
+ }
@@ -79,6 +79,14 @@ export declare class SASjsConfig {
79
79
  * may affect browser performance, especially with debug (logs) enabled.
80
80
  */
81
81
  requestHistoryLimit?: number;
82
+ /**
83
+ * Optional setting. When `true`, the request runs as a Viya execution task —
84
+ * appends `_executionTasks=true` to the request URL. Only applies to the Viya
85
+ * web jobs path, i.e. when `serverType === SASVIYA` AND
86
+ * `useComputeApi` is `null`/`undefined`. Has no effect when `useComputeApi`
87
+ * is explicitly set to `true` or `false`.
88
+ */
89
+ runAsTask?: boolean;
82
90
  }
83
91
  export declare enum LoginMechanism {
84
92
  Default = "Default",
package/utils/index.d.ts CHANGED
@@ -15,6 +15,7 @@ export * from './parseGeneratedCode';
15
15
  export * from './parseSasViyaLog';
16
16
  export * from './parseSourceCode';
17
17
  export * from './parseViyaDebugResponse';
18
+ export * from './parseViyaLogDebugResponse';
18
19
  export * from './parseWeboutResponse';
19
20
  export * from './serialize';
20
21
  export * from './splitChunks';
@@ -0,0 +1,10 @@
1
+ /**
2
+ * When querying a Viya job using the Web approach with _DEBUG=128 (used when
3
+ * runAsTask is true), the webout JSON is inlined into the response via:
4
+ * var blob = new Blob([`{...}`], {type: 'application/json'});
5
+ * On abort/error paths the same shape is used but with text/plain and
6
+ * weboutBEGIN/END markers around the JSON:
7
+ * var blob = new Blob([`>>weboutBEGIN<<\n{...}\n>>weboutEND<<\n`], {type: 'text/plain'});
8
+ * No follow-up request is needed — extract and parse the JSON directly.
9
+ */
10
+ export declare const parseSasViyaLogDebugResponse: (response: any) => Promise<any>;
@@ -0,0 +1 @@
1
+ export {};