@sasjs/adapter 4.9.1 → 4.10.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.
@@ -1,13 +1,30 @@
1
1
  import { Link } from './Link';
2
+ import { SessionManager } from '../SessionManager';
3
+ export declare enum SessionState {
4
+ Completed = "completed",
5
+ Running = "running",
6
+ Pending = "pending",
7
+ Idle = "idle",
8
+ Unavailable = "unavailable",
9
+ NoState = "",
10
+ Failed = "failed",
11
+ Error = "error"
12
+ }
2
13
  export interface Session {
3
14
  id: string;
4
- state: string;
15
+ state: SessionState;
16
+ stateUrl: string;
5
17
  links: Link[];
6
18
  attributes: {
7
19
  sessionInactiveTimeout: number;
8
20
  };
9
21
  creationTimeStamp: string;
22
+ etag: string;
10
23
  }
11
24
  export interface SessionVariable {
12
25
  value: string;
13
26
  }
27
+ export interface JobSessionManager {
28
+ session: Session;
29
+ sessionManager: SessionManager;
30
+ }
package/package.json CHANGED
@@ -84,5 +84,5 @@
84
84
  "https": "1.0.0",
85
85
  "tough-cookie": "4.1.3"
86
86
  },
87
- "version": "4.9.1"
87
+ "version": "4.10.0"
88
88
  }
@@ -25,7 +25,7 @@ import { prefixMessage } from '@sasjs/utils/error'
25
25
  import { pollJobState } from './api/viya/pollJobState'
26
26
  import { getTokens } from './auth/getTokens'
27
27
  import { uploadTables } from './api/viya/uploadTables'
28
- import { executeScript } from './api/viya/executeScript'
28
+ import { executeOnComputeApi } from './api/viya/executeOnComputeApi'
29
29
  import { getAccessTokenForViya } from './auth/getAccessTokenForViya'
30
30
  import { refreshTokensForViya } from './auth/refreshTokensForViya'
31
31
 
@@ -293,7 +293,7 @@ export class SASViyaApiClient {
293
293
  printPid = false,
294
294
  variables?: MacroVar
295
295
  ): Promise<any> {
296
- return executeScript(
296
+ return executeOnComputeApi(
297
297
  this.requestClient,
298
298
  this.sessionManager,
299
299
  this.rootFolderName,
@@ -1,4 +1,4 @@
1
- import { Session, Context, SessionVariable } from './types'
1
+ import { Session, Context, SessionVariable, SessionState } from './types'
2
2
  import { NoSessionStateError } from './types/errors'
3
3
  import { asyncForEach, isUrl } from './utils'
4
4
  import { prefixMessage } from '@sasjs/utils/error'
@@ -12,6 +12,7 @@ interface ApiErrorResponse {
12
12
 
13
13
  export class SessionManager {
14
14
  private loggedErrors: NoSessionStateError[] = []
15
+ private sessionStateLinkError = 'Error while getting session state link. '
15
16
 
16
17
  constructor(
17
18
  private serverUrl: string,
@@ -28,7 +29,7 @@ export class SessionManager {
28
29
  private _debug: boolean = false
29
30
  private printedSessionState = {
30
31
  printed: false,
31
- state: ''
32
+ state: SessionState.NoState
32
33
  }
33
34
 
34
35
  public get debug() {
@@ -265,6 +266,18 @@ export class SessionManager {
265
266
  )
266
267
  })
267
268
 
269
+ // Add response etag to Session object.
270
+ createdSession.etag = etag
271
+
272
+ // Get session state link.
273
+ const stateLink = createdSession.links.find((link) => link.rel === 'state')
274
+
275
+ // Throw error if session state link is not present.
276
+ if (!stateLink) throw this.sessionStateLinkError
277
+
278
+ // Add session state link to Session object.
279
+ createdSession.stateUrl = stateLink.href
280
+
268
281
  await this.waitForSession(createdSession, etag, accessToken)
269
282
 
270
283
  this.sessions.push(createdSession)
@@ -327,32 +340,30 @@ export class SessionManager {
327
340
  etag: string | null,
328
341
  accessToken?: string
329
342
  ): Promise<string> {
343
+ let { state: sessionState } = session
344
+ const { stateUrl } = session
330
345
  const logger = process.logger || console
331
346
 
332
- let sessionState = session.state
333
-
334
- const stateLink = session.links.find((l: any) => l.rel === 'state')
335
-
336
347
  if (
337
- sessionState === 'pending' ||
338
- sessionState === 'running' ||
339
- sessionState === ''
348
+ sessionState === SessionState.Pending ||
349
+ sessionState === SessionState.Running ||
350
+ sessionState === SessionState.NoState
340
351
  ) {
341
- if (stateLink) {
352
+ if (stateUrl) {
342
353
  if (this.debug && !this.printedSessionState.printed) {
343
- logger.info(`Polling: ${this.serverUrl + stateLink.href}`)
354
+ logger.info(`Polling: ${this.serverUrl + stateUrl}`)
344
355
 
345
356
  this.printedSessionState.printed = true
346
357
  }
347
358
 
348
- const url = `${this.serverUrl}${stateLink.href}?wait=30`
359
+ const url = `${this.serverUrl}${stateUrl}?wait=30`
349
360
 
350
361
  const { result: state, responseStatus: responseStatus } =
351
362
  await this.getSessionState(url, etag!, accessToken).catch((err) => {
352
363
  throw prefixMessage(err, 'Error while waiting for session. ')
353
364
  })
354
365
 
355
- sessionState = state.trim()
366
+ sessionState = state.trim() as SessionState
356
367
 
357
368
  if (this.debug && this.printedSessionState.state !== sessionState) {
358
369
  logger.info(`Current session state is '${sessionState}'`)
@@ -364,7 +375,7 @@ export class SessionManager {
364
375
  if (!sessionState) {
365
376
  const stateError = new NoSessionStateError(
366
377
  responseStatus,
367
- this.serverUrl + stateLink.href,
378
+ this.serverUrl + stateUrl,
368
379
  session.links.find((l: any) => l.rel === 'log')?.href as string
369
380
  )
370
381
 
@@ -386,7 +397,7 @@ export class SessionManager {
386
397
 
387
398
  return sessionState
388
399
  } else {
389
- throw 'Error while getting session state link. '
400
+ throw this.sessionStateLinkError
390
401
  }
391
402
  } else {
392
403
  this.loggedErrors = []
@@ -413,7 +424,7 @@ export class SessionManager {
413
424
  return await this.requestClient
414
425
  .get(url, accessToken, 'text/plain', { 'If-None-Match': etag })
415
426
  .then((res) => ({
416
- result: res.result as string,
427
+ result: res.result as SessionState,
417
428
  responseStatus: res.status
418
429
  }))
419
430
  .catch((err) => {
@@ -15,8 +15,12 @@ import { formatDataForRequest } from '../../utils/formatDataForRequest'
15
15
  import { pollJobState, JobState } from './pollJobState'
16
16
  import { uploadTables } from './uploadTables'
17
17
 
18
+ interface JobRequestBody {
19
+ [key: string]: number | string | string[]
20
+ }
21
+
18
22
  /**
19
- * Executes code on the current SAS Viya server.
23
+ * Executes SAS program on the current SAS Viya server using Compute API.
20
24
  * @param jobPath - the path to the file being submitted for execution.
21
25
  * @param linesOfCode - an array of code lines to execute.
22
26
  * @param contextName - the context to execute the code in.
@@ -29,7 +33,7 @@ import { uploadTables } from './uploadTables'
29
33
  * @param printPid - a boolean that indicates whether the function should print (PID) of the started job.
30
34
  * @param variables - an object that represents macro variables.
31
35
  */
32
- export async function executeScript(
36
+ export async function executeOnComputeApi(
33
37
  requestClient: RequestClient,
34
38
  sessionManager: SessionManager,
35
39
  rootFolderName: string,
@@ -46,6 +50,7 @@ export async function executeScript(
46
50
  variables?: MacroVar
47
51
  ): Promise<any> {
48
52
  let access_token = (authConfig || {}).access_token
53
+
49
54
  if (authConfig) {
50
55
  ;({ access_token } = await getTokens(requestClient, authConfig))
51
56
  }
@@ -85,20 +90,6 @@ export async function executeScript(
85
90
  }
86
91
  }
87
92
 
88
- const jobArguments: { [key: string]: any } = {
89
- _contextName: contextName,
90
- _OMITJSONLISTING: true,
91
- _OMITJSONLOG: true,
92
- _OMITSESSIONRESULTS: true,
93
- _OMITTEXTLISTING: true,
94
- _OMITTEXTLOG: true
95
- }
96
-
97
- if (debug) {
98
- jobArguments['_OMITTEXTLOG'] = false
99
- jobArguments['_OMITSESSIONRESULTS'] = false
100
- }
101
-
102
93
  let fileName
103
94
 
104
95
  if (isRelativePath(jobPath)) {
@@ -107,6 +98,7 @@ export async function executeScript(
107
98
  }`
108
99
  } else {
109
100
  const jobPathParts = jobPath.split('/')
101
+
110
102
  fileName = jobPathParts.pop()
111
103
  }
112
104
 
@@ -118,7 +110,6 @@ export async function executeScript(
118
110
  }
119
111
 
120
112
  if (variables) jobVariables = { ...jobVariables, ...variables }
121
-
122
113
  if (debug) jobVariables = { ...jobVariables, _DEBUG: 131 }
123
114
 
124
115
  let files: any[] = []
@@ -145,12 +136,12 @@ export async function executeScript(
145
136
  }
146
137
 
147
138
  // Execute job in session
148
- const jobRequestBody = {
149
- name: fileName,
139
+ const jobRequestBody: JobRequestBody = {
140
+ name: fileName || 'Default Job Name',
150
141
  description: 'Powered by SASjs',
151
142
  code: linesOfCode,
152
143
  variables: jobVariables,
153
- arguments: jobArguments
144
+ version: 2
154
145
  }
155
146
 
156
147
  const { result: postedJob, etag } = await requestClient
@@ -179,16 +170,21 @@ export async function executeScript(
179
170
  postedJob,
180
171
  debug,
181
172
  authConfig,
182
- pollOptions
173
+ pollOptions,
174
+ {
175
+ session,
176
+ sessionManager
177
+ }
183
178
  ).catch(async (err) => {
184
179
  const error = err?.response?.data
185
180
  const result = /err=[0-9]*,/.exec(error)
186
-
187
181
  const errorCode = '5113'
182
+
188
183
  if (result?.[0]?.slice(4, -1) === errorCode) {
184
+ const logCount = 1000000
189
185
  const sessionLogUrl =
190
186
  postedJob.links.find((l: any) => l.rel === 'up')!.href + '/log'
191
- const logCount = 1000000
187
+
192
188
  err.log = await fetchLogByChunks(
193
189
  requestClient,
194
190
  access_token!,
@@ -196,6 +192,7 @@ export async function executeScript(
196
192
  logCount
197
193
  )
198
194
  }
195
+
199
196
  throw prefixMessage(err, 'Error while polling job status. ')
200
197
  })
201
198
 
@@ -214,12 +211,12 @@ export async function executeScript(
214
211
 
215
212
  let jobResult
216
213
  let log = ''
217
-
218
214
  const logLink = currentJob.links.find((l) => l.rel === 'log')
219
215
 
220
216
  if (debug && logLink) {
221
217
  const logUrl = `${logLink.href}/content`
222
218
  const logCount = currentJob.logStatistics?.lineCount ?? 1000000
219
+
223
220
  log = await fetchLogByChunks(
224
221
  requestClient,
225
222
  access_token!,
@@ -232,9 +229,7 @@ export async function executeScript(
232
229
  throw new ComputeJobExecutionError(currentJob, log)
233
230
  }
234
231
 
235
- if (!expectWebout) {
236
- return { job: currentJob, log }
237
- }
232
+ if (!expectWebout) return { job: currentJob, log }
238
233
 
239
234
  const resultLink = `/compute/sessions/${executionSessionId}/filerefs/_webout/content`
240
235
 
@@ -245,6 +240,7 @@ export async function executeScript(
245
240
  if (logLink) {
246
241
  const logUrl = `${logLink.href}/content`
247
242
  const logCount = currentJob.logStatistics?.lineCount ?? 1000000
243
+
248
244
  log = await fetchLogByChunks(
249
245
  requestClient,
250
246
  access_token!,
@@ -279,7 +275,7 @@ export async function executeScript(
279
275
  const error = e as HttpError
280
276
 
281
277
  if (error.status === 404) {
282
- return executeScript(
278
+ return executeOnComputeApi(
283
279
  requestClient,
284
280
  sessionManager,
285
281
  rootFolderName,
@@ -3,7 +3,7 @@ import { Job, PollOptions, PollStrategy } from '../..'
3
3
  import { getTokens } from '../../auth/getTokens'
4
4
  import { RequestClient } from '../../request/RequestClient'
5
5
  import { JobStatePollError } from '../../types/errors'
6
- import { Link, WriteStream } from '../../types'
6
+ import { Link, WriteStream, SessionState, JobSessionManager } from '../../types'
7
7
  import { delay, isNode } from '../../utils'
8
8
 
9
9
  export enum JobState {
@@ -37,6 +37,7 @@ export enum JobState {
37
37
  * { maxPollCount: 500, pollInterval: 30000 }, // approximately ~50.5 mins (including time to get response (~300ms))
38
38
  * { maxPollCount: 3400, pollInterval: 60000 } // approximately ~3015 mins (~125 hours) (including time to get response (~300ms))
39
39
  * ]
40
+ * @param jobSessionManager - job session object containing session object and an instance of Session Manager. Job session object is used to periodically (every 10th job state poll) check parent session state.
40
41
  * @returns - a promise which resolves with a job state
41
42
  */
42
43
  export async function pollJobState(
@@ -44,7 +45,8 @@ export async function pollJobState(
44
45
  postedJob: Job,
45
46
  debug: boolean,
46
47
  authConfig?: AuthConfig,
47
- pollOptions?: PollOptions
48
+ pollOptions?: PollOptions,
49
+ jobSessionManager?: JobSessionManager
48
50
  ): Promise<JobState> {
49
51
  const logger = process.logger || console
50
52
 
@@ -127,7 +129,8 @@ export async function pollJobState(
127
129
  pollOptions,
128
130
  authConfig,
129
131
  streamLog,
130
- logFileStream
132
+ logFileStream,
133
+ jobSessionManager
131
134
  )
132
135
 
133
136
  currentState = result.state
@@ -158,7 +161,8 @@ export async function pollJobState(
158
161
  defaultPollOptions,
159
162
  authConfig,
160
163
  streamLog,
161
- logFileStream
164
+ logFileStream,
165
+ jobSessionManager
162
166
  )
163
167
 
164
168
  currentState = result.state
@@ -208,7 +212,21 @@ const needsRetry = (state: string) =>
208
212
  state === JobState.Pending ||
209
213
  state === JobState.Unavailable
210
214
 
211
- const doPoll = async (
215
+ /**
216
+ * Polls job state.
217
+ * @param requestClient - the pre-configured HTTP request client.
218
+ * @param postedJob - the relative or absolute path to the job.
219
+ * @param currentState - current job state.
220
+ * @param debug - sets the _debug flag in the job arguments.
221
+ * @param pollCount - current poll count.
222
+ * @param pollOptions - an object containing maxPollCount, pollInterval, streamLog and logFolderPath.
223
+ * @param authConfig - an access token, refresh token, client and secret for an authorized user.
224
+ * @param streamLog - indicates if job log should be streamed.
225
+ * @param logStream - job log stream.
226
+ * @param jobSessionManager - job session object containing session object and an instance of Session Manager. Job session object is used to periodically (every 10th job state poll) check parent session state.
227
+ * @returns - a promise which resolves with a job state
228
+ */
229
+ export const doPoll = async (
212
230
  requestClient: RequestClient,
213
231
  postedJob: Job,
214
232
  currentState: JobState,
@@ -217,7 +235,8 @@ const doPoll = async (
217
235
  pollOptions: PollOptions,
218
236
  authConfig?: AuthConfig,
219
237
  streamLog?: boolean,
220
- logStream?: WriteStream
238
+ logStream?: WriteStream,
239
+ jobSessionManager?: JobSessionManager
221
240
  ): Promise<{ state: JobState; pollCount: number }> => {
222
241
  const { maxPollCount, pollInterval } = pollOptions
223
242
  const logger = process.logger || console
@@ -229,6 +248,35 @@ const doPoll = async (
229
248
  let startLogLine = 0
230
249
 
231
250
  while (needsRetry(state) && pollCount <= maxPollCount) {
251
+ // Check parent session state on every 10th job state poll.
252
+ if (jobSessionManager && pollCount && pollCount % 10 === 0 && authConfig) {
253
+ const { session, sessionManager } = jobSessionManager
254
+ const { stateUrl, etag, id: sessionId } = session
255
+ const { access_token } = authConfig
256
+ const { id: jobId } = postedJob
257
+
258
+ // Get session state.
259
+ const { result: sessionState, responseStatus } = await sessionManager[
260
+ 'getSessionState'
261
+ ](stateUrl, etag, access_token).catch((err) => {
262
+ // Handle error while getting session state.
263
+ throw new JobStatePollError(jobId, err)
264
+ })
265
+
266
+ // Clear parent session and throw an error if session state is not
267
+ // 'running' or response status is not 200.
268
+ if (sessionState !== SessionState.Running || responseStatus !== 200) {
269
+ sessionManager.clearSession(sessionId, access_token)
270
+
271
+ const sessionError =
272
+ sessionState !== SessionState.Running
273
+ ? `Session state of the job is not 'running'. Session state is '${sessionState}'`
274
+ : `Session response status is not 200. Session response status is ${responseStatus}.`
275
+
276
+ throw new JobStatePollError(jobId, new Error(sessionError))
277
+ }
278
+ }
279
+
232
280
  state = await getJobState(
233
281
  requestClient,
234
282
  postedJob,
@@ -1,14 +1,16 @@
1
1
  import { AuthConfig } from '@sasjs/utils/types'
2
- import { Job, Session } from '../../../types'
2
+ import { Job, Session, SessionState } from '../../../types'
3
3
 
4
4
  export const mockSession: Session = {
5
5
  id: 's35510n',
6
- state: 'idle',
6
+ state: SessionState.Idle,
7
+ stateUrl: '',
7
8
  links: [],
8
9
  attributes: {
9
10
  sessionInactiveTimeout: 1
10
11
  },
11
- creationTimeStamp: new Date().valueOf().toString()
12
+ creationTimeStamp: new Date().valueOf().toString(),
13
+ etag: 'etag-string'
12
14
  }
13
15
 
14
16
  export const mockJob: Job = {
@@ -1,15 +1,34 @@
1
1
  import { Link } from './Link'
2
+ import { SessionManager } from '../SessionManager'
3
+
4
+ export enum SessionState {
5
+ Completed = 'completed',
6
+ Running = 'running',
7
+ Pending = 'pending',
8
+ Idle = 'idle',
9
+ Unavailable = 'unavailable',
10
+ NoState = '',
11
+ Failed = 'failed',
12
+ Error = 'error'
13
+ }
2
14
 
3
15
  export interface Session {
4
16
  id: string
5
- state: string
17
+ state: SessionState
18
+ stateUrl: string
6
19
  links: Link[]
7
20
  attributes: {
8
21
  sessionInactiveTimeout: number
9
22
  }
10
23
  creationTimeStamp: string
24
+ etag: string
11
25
  }
12
26
 
13
27
  export interface SessionVariable {
14
28
  value: string
15
29
  }
30
+
31
+ export interface JobSessionManager {
32
+ session: Session
33
+ sessionManager: SessionManager
34
+ }
@@ -1,13 +1,30 @@
1
1
  import { Link } from './Link';
2
+ import { SessionManager } from '../SessionManager';
3
+ export declare enum SessionState {
4
+ Completed = "completed",
5
+ Running = "running",
6
+ Pending = "pending",
7
+ Idle = "idle",
8
+ Unavailable = "unavailable",
9
+ NoState = "",
10
+ Failed = "failed",
11
+ Error = "error"
12
+ }
2
13
  export interface Session {
3
14
  id: string;
4
- state: string;
15
+ state: SessionState;
16
+ stateUrl: string;
5
17
  links: Link[];
6
18
  attributes: {
7
19
  sessionInactiveTimeout: number;
8
20
  };
9
21
  creationTimeStamp: string;
22
+ etag: string;
10
23
  }
11
24
  export interface SessionVariable {
12
25
  value: string;
13
26
  }
27
+ export interface JobSessionManager {
28
+ session: Session;
29
+ sessionManager: SessionManager;
30
+ }