mirror of
https://github.com/sasjs/adapter.git
synced 2025-12-24 22:41:20 +00:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5dfee30875 | |||
|
|
3a186bc55c | ||
|
|
0359fcb6be | ||
|
|
f2997169cb | ||
|
|
451f2dfaca | ||
|
|
38e11f1771 | ||
|
|
259b6b3ff2 | ||
|
|
5b2d9e675f | ||
|
|
8dd4ab8cec | ||
|
|
34135b889f | ||
|
|
62f4577b64 | ||
|
|
7a4feddd82 | ||
|
|
681abf5b3b | ||
|
|
46c6d3e7f4 | ||
|
|
5731b0f9b1 | ||
|
|
f18a523087 | ||
|
|
8cbd292f13 | ||
|
|
4851f25753 | ||
|
|
5756638dc2 | ||
|
|
e511cd613c | ||
|
|
2119c81ebb | ||
|
|
ea4b30d6ef |
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -14,7 +14,7 @@ What code changes have been made to achieve the intent.
|
|||||||
|
|
||||||
No PR (that involves a non-trivial code change) should be merged, unless all items below are confirmed! If an urgent fix is needed - use a tar file.
|
No PR (that involves a non-trivial code change) should be merged, unless all items below are confirmed! If an urgent fix is needed - use a tar file.
|
||||||
|
|
||||||
|
- [ ] Unit tests coverage has been increased and a new threshold is set.
|
||||||
- [ ] All `sasjs-cli` unit tests are passing (`npm test`).
|
- [ ] All `sasjs-cli` unit tests are passing (`npm test`).
|
||||||
- (CI Runs this) All `sasjs-tests` are passing. If you want to run it manually (instructions available [here](https://github.com/sasjs/adapter/blob/master/sasjs-tests/README.md)).
|
- (CI Runs this) All `sasjs-tests` are passing. If you want to run it manually (instructions available [here](https://github.com/sasjs/adapter/blob/master/sasjs-tests/README.md)).
|
||||||
- [ ] [Data Controller](https://datacontroller.io) builds and is functional on both SAS 9 and Viya
|
- [ ] [Data Controller](https://datacontroller.io) builds and is functional on both SAS 9 and Viya
|
||||||
|
|||||||
2
.github/workflows/assign-reviewer.yml
vendored
2
.github/workflows/assign-reviewer.yml
vendored
@@ -10,4 +10,4 @@ jobs:
|
|||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: uesteibar/reviewer-lottery@v1
|
- uses: uesteibar/reviewer-lottery@v1
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GH_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -80,6 +80,10 @@ jobs:
|
|||||||
npm run update:adapter
|
npm run update:adapter
|
||||||
pm2 start --name sasjs-test npm -- start
|
pm2 start --name sasjs-test npm -- start
|
||||||
|
|
||||||
|
- name: Sleep for 10 seconds
|
||||||
|
run: sleep 10s
|
||||||
|
shell: bash
|
||||||
|
|
||||||
- name: Run cypress on sasjs
|
- name: Run cypress on sasjs
|
||||||
run: |
|
run: |
|
||||||
replace-in-files --regex='"sasjsTestsUrl".*' --replacement='"sasjsTestsUrl":"http://localhost:3000",' ./cypress.json
|
replace-in-files --regex='"sasjsTestsUrl".*' --replacement='"sasjsTestsUrl":"http://localhost:3000",' ./cypress.json
|
||||||
|
|||||||
4
.github/workflows/generateDocs.yml
vendored
4
.github/workflows/generateDocs.yml
vendored
@@ -37,8 +37,8 @@ jobs:
|
|||||||
- name: Push generated docs
|
- name: Push generated docs
|
||||||
uses: peaceiris/actions-gh-pages@v3
|
uses: peaceiris/actions-gh-pages@v3
|
||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.GH_TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
publish_branch: gh-pages
|
publish_branch: gh-pages
|
||||||
publish_dir: ./docs
|
publish_dir: ./docs
|
||||||
cname: adapter.sasjs.io
|
cname: adapter.sasjs.io
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/npmpublish.yml
vendored
2
.github/workflows/npmpublish.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
|||||||
- name: Semantic Release
|
- name: Semantic Release
|
||||||
uses: cycjimmy/semantic-release-action@v3
|
uses: cycjimmy/semantic-release-action@v3
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|
||||||
- name: Send Matrix message
|
- name: Send Matrix message
|
||||||
|
|||||||
@@ -151,7 +151,11 @@ The `request()` method also has optional parameters such as a config object and
|
|||||||
|
|
||||||
The response object will contain returned tables and columns. Table names are always lowercase, and column names uppercase.
|
The response object will contain returned tables and columns. Table names are always lowercase, and column names uppercase.
|
||||||
|
|
||||||
The adapter will also cache the logs (if debug enabled) and even the work tables. For performance, it is best to keep debug mode off.
|
The adapter will also cache the logs (if debug enabled) and even the work tables. For performance, it is best to keep debug mode off.
|
||||||
|
|
||||||
|
### Verbose Mode
|
||||||
|
|
||||||
|
Set `verbose` to `true` to enable verbose mode that logs a summary of every HTTP response. Verbose mode can be disabled by calling `disableVerboseMode` method or enabled by `enableVerboseMode` method. Verbose mode also supports `bleached` mode that disables extra colors in req/res summary. To enable `bleached` verbose mode, pass `verbose` equal to `bleached` while instantiating an instance of `RequestClient` or to `setVerboseMode` method. Verbose mode can also be enabled/disabled by `startComputeJob` method.
|
||||||
|
|
||||||
### Session Manager
|
### Session Manager
|
||||||
|
|
||||||
@@ -273,6 +277,7 @@ Configuration on the client side involves passing an object on startup, which ca
|
|||||||
* `serverType` - either `SAS9`, `SASVIYA` or `SASJS`. The `SASJS` server type is for use with [sasjs/server](https://github.com/sasjs/server).
|
* `serverType` - either `SAS9`, `SASVIYA` or `SASJS`. The `SASJS` server type is for use with [sasjs/server](https://github.com/sasjs/server).
|
||||||
* `serverUrl` - the location (including http protocol and port) of the SAS Server. Can be omitted, eg if serving directly from the SAS Web Server, or in streaming mode.
|
* `serverUrl` - the location (including http protocol and port) of the SAS Server. Can be omitted, eg if serving directly from the SAS Web Server, or in streaming mode.
|
||||||
* `debug` - if `true` then SAS Logs and extra debug information is returned.
|
* `debug` - if `true` then SAS Logs and extra debug information is returned.
|
||||||
|
* `verbose` - optional, if `true` then a summary of every HTTP response is logged.
|
||||||
* `loginMechanism` - either `Default` or `Redirected`. See [SAS Logon](#sas-logon) section.
|
* `loginMechanism` - either `Default` or `Redirected`. See [SAS Logon](#sas-logon) section.
|
||||||
* `useComputeApi` - Only relevant when the serverType is `SASVIYA`. If `true` the [Compute API](#using-the-compute-api) is used. If `false` the [JES API](#using-the-jes-api) is used. If `null` or `undefined` the [Web](#using-jes-web-app) approach is used.
|
* `useComputeApi` - Only relevant when the serverType is `SASVIYA`. If `true` the [Compute API](#using-the-compute-api) is used. If `false` the [JES API](#using-the-jes-api) is used. If `null` or `undefined` the [Web](#using-jes-web-app) approach is used.
|
||||||
* `contextName` - Compute context on which the requests will be called. If missing or not provided, defaults to `Job Execution Compute context`.
|
* `contextName` - Compute context on which the requests will be called. If missing or not provided, defaults to `Job Execution Compute context`.
|
||||||
|
|||||||
@@ -41,7 +41,14 @@ module.exports = {
|
|||||||
// ],
|
// ],
|
||||||
|
|
||||||
// An object that configures minimum threshold enforcement for coverage results
|
// An object that configures minimum threshold enforcement for coverage results
|
||||||
// coverageThreshold: undefined,
|
coverageThreshold: {
|
||||||
|
global: {
|
||||||
|
statements: 64.01,
|
||||||
|
branches: 45.11,
|
||||||
|
functions: 54.1,
|
||||||
|
lines: 64.51
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// A path to a custom dependency extractor
|
// A path to a custom dependency extractor
|
||||||
// dependencyExtractor: undefined,
|
// dependencyExtractor: undefined,
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import { prefixMessage } from '@sasjs/utils/error'
|
|||||||
import { pollJobState } from './api/viya/pollJobState'
|
import { pollJobState } from './api/viya/pollJobState'
|
||||||
import { getTokens } from './auth/getTokens'
|
import { getTokens } from './auth/getTokens'
|
||||||
import { uploadTables } from './api/viya/uploadTables'
|
import { uploadTables } from './api/viya/uploadTables'
|
||||||
import { executeScript } from './api/viya/executeScript'
|
import { executeOnComputeApi } from './api/viya/executeOnComputeApi'
|
||||||
import { getAccessTokenForViya } from './auth/getAccessTokenForViya'
|
import { getAccessTokenForViya } from './auth/getAccessTokenForViya'
|
||||||
import { refreshTokensForViya } from './auth/refreshTokensForViya'
|
import { refreshTokensForViya } from './auth/refreshTokensForViya'
|
||||||
|
|
||||||
@@ -293,7 +293,7 @@ export class SASViyaApiClient {
|
|||||||
printPid = false,
|
printPid = false,
|
||||||
variables?: MacroVar
|
variables?: MacroVar
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
return executeScript(
|
return executeOnComputeApi(
|
||||||
this.requestClient,
|
this.requestClient,
|
||||||
this.sessionManager,
|
this.sessionManager,
|
||||||
this.rootFolderName,
|
this.rootFolderName,
|
||||||
|
|||||||
67
src/SASjs.ts
67
src/SASjs.ts
@@ -4,7 +4,12 @@ import {
|
|||||||
UploadFile,
|
UploadFile,
|
||||||
EditContextInput,
|
EditContextInput,
|
||||||
PollOptions,
|
PollOptions,
|
||||||
LoginMechanism
|
LoginMechanism,
|
||||||
|
VerboseMode,
|
||||||
|
ErrorResponse,
|
||||||
|
LoginOptions,
|
||||||
|
LoginResult,
|
||||||
|
ExecutionQuery
|
||||||
} from './types'
|
} from './types'
|
||||||
import { SASViyaApiClient } from './SASViyaApiClient'
|
import { SASViyaApiClient } from './SASViyaApiClient'
|
||||||
import { SAS9ApiClient } from './SAS9ApiClient'
|
import { SAS9ApiClient } from './SAS9ApiClient'
|
||||||
@@ -29,8 +34,7 @@ import {
|
|||||||
Sas9JobExecutor,
|
Sas9JobExecutor,
|
||||||
FileUploader
|
FileUploader
|
||||||
} from './job-execution'
|
} from './job-execution'
|
||||||
import { ErrorResponse } from './types/errors'
|
import { AxiosResponse, AxiosError } from 'axios'
|
||||||
import { LoginOptions, LoginResult } from './types/Login'
|
|
||||||
|
|
||||||
interface ExecuteScriptParams {
|
interface ExecuteScriptParams {
|
||||||
linesOfCode: string[]
|
linesOfCode: string[]
|
||||||
@@ -157,6 +161,23 @@ export default class SASjs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes job on SASJS server.
|
||||||
|
* @param query - an object containing job path and debug level.
|
||||||
|
* @param appLoc - an application path.
|
||||||
|
* @param authConfig - an object for authentication.
|
||||||
|
* @returns a promise that resolves into job execution result and log.
|
||||||
|
*/
|
||||||
|
public async executeJob(
|
||||||
|
query: ExecutionQuery,
|
||||||
|
appLoc: string,
|
||||||
|
authConfig?: AuthConfig
|
||||||
|
) {
|
||||||
|
this.isMethodSupported('executeScript', [ServerType.Sasjs])
|
||||||
|
|
||||||
|
return await this.sasJSApiClient?.executeJob(query, appLoc, authConfig)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets compute contexts.
|
* Gets compute contexts.
|
||||||
* @param accessToken - an access token for an authorised user.
|
* @param accessToken - an access token for an authorised user.
|
||||||
@@ -854,7 +875,7 @@ export default class SASjs {
|
|||||||
* @param pollOptions - an object that represents poll interval(milliseconds) and maximum amount of attempts. Object example: { maxPollCount: 24 * 60 * 60, pollInterval: 1000 }. More information available at src/api/viya/pollJobState.ts.
|
* @param pollOptions - an object that represents poll interval(milliseconds) and maximum amount of attempts. Object example: { maxPollCount: 24 * 60 * 60, pollInterval: 1000 }. More information available at src/api/viya/pollJobState.ts.
|
||||||
* @param printPid - a boolean that indicates whether the function should print (PID) of the started job.
|
* @param printPid - a boolean that indicates whether the function should print (PID) of the started job.
|
||||||
* @param variables - an object that represents macro variables.
|
* @param variables - an object that represents macro variables.
|
||||||
* @param verboseMode - boolean to enable verbose mode (log every HTTP response).
|
* @param verboseMode - boolean or a string equal to 'bleached' to enable verbose mode (log every HTTP response).
|
||||||
*/
|
*/
|
||||||
public async startComputeJob(
|
public async startComputeJob(
|
||||||
sasJob: string,
|
sasJob: string,
|
||||||
@@ -865,7 +886,7 @@ export default class SASjs {
|
|||||||
pollOptions?: PollOptions,
|
pollOptions?: PollOptions,
|
||||||
printPid = false,
|
printPid = false,
|
||||||
variables?: MacroVar,
|
variables?: MacroVar,
|
||||||
verboseMode?: boolean
|
verboseMode?: VerboseMode
|
||||||
) {
|
) {
|
||||||
config = {
|
config = {
|
||||||
...this.sasjsConfig,
|
...this.sasjsConfig,
|
||||||
@@ -879,8 +900,10 @@ export default class SASjs {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (verboseMode) this.requestClient?.enableVerboseMode()
|
if (verboseMode) {
|
||||||
else this.requestClient?.disableVerboseMode()
|
this.requestClient?.setVerboseMode(verboseMode)
|
||||||
|
this.requestClient?.enableVerboseMode()
|
||||||
|
} else if (verboseMode === false) this.requestClient?.disableVerboseMode()
|
||||||
|
|
||||||
return this.sasViyaApiClient?.executeComputeJob(
|
return this.sasViyaApiClient?.executeComputeJob(
|
||||||
sasJob,
|
sasJob,
|
||||||
@@ -975,7 +998,8 @@ export default class SASjs {
|
|||||||
this.requestClient = new RequestClientClass(
|
this.requestClient = new RequestClientClass(
|
||||||
this.sasjsConfig.serverUrl,
|
this.sasjsConfig.serverUrl,
|
||||||
this.sasjsConfig.httpsAgentOptions,
|
this.sasjsConfig.httpsAgentOptions,
|
||||||
this.sasjsConfig.requestHistoryLimit
|
this.sasjsConfig.requestHistoryLimit,
|
||||||
|
this.sasjsConfig.verbose
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
this.requestClient.setConfig(
|
this.requestClient.setConfig(
|
||||||
@@ -1139,4 +1163,31 @@ export default class SASjs {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables verbose mode that will log a summary of every HTTP response.
|
||||||
|
* @param successCallBack - function that should be triggered on every HTTP response with the status 2**.
|
||||||
|
* @param errorCallBack - function that should be triggered on every HTTP response with the status different from 2**.
|
||||||
|
*/
|
||||||
|
public enableVerboseMode(
|
||||||
|
successCallBack?: (response: AxiosResponse | AxiosError) => AxiosResponse,
|
||||||
|
errorCallBack?: (response: AxiosResponse | AxiosError) => AxiosResponse
|
||||||
|
) {
|
||||||
|
this.requestClient?.enableVerboseMode(successCallBack, errorCallBack)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns off verbose mode to log every HTTP response.
|
||||||
|
*/
|
||||||
|
public disableVerboseMode() {
|
||||||
|
this.requestClient?.disableVerboseMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets verbose mode.
|
||||||
|
* @param verboseMode - value of the verbose mode, can be true, false or bleached(without extra colors).
|
||||||
|
*/
|
||||||
|
public setVerboseMode = (verboseMode: VerboseMode) => {
|
||||||
|
this.requestClient?.setVerboseMode(verboseMode)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Session, Context, SessionVariable } from './types'
|
import { Session, Context, SessionVariable, SessionState } from './types'
|
||||||
import { NoSessionStateError } from './types/errors'
|
import { NoSessionStateError } from './types/errors'
|
||||||
import { asyncForEach, isUrl } from './utils'
|
import { asyncForEach, isUrl } from './utils'
|
||||||
import { prefixMessage } from '@sasjs/utils/error'
|
import { prefixMessage } from '@sasjs/utils/error'
|
||||||
@@ -12,6 +12,7 @@ interface ApiErrorResponse {
|
|||||||
|
|
||||||
export class SessionManager {
|
export class SessionManager {
|
||||||
private loggedErrors: NoSessionStateError[] = []
|
private loggedErrors: NoSessionStateError[] = []
|
||||||
|
private sessionStateLinkError = 'Error while getting session state link. '
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private serverUrl: string,
|
private serverUrl: string,
|
||||||
@@ -28,7 +29,7 @@ export class SessionManager {
|
|||||||
private _debug: boolean = false
|
private _debug: boolean = false
|
||||||
private printedSessionState = {
|
private printedSessionState = {
|
||||||
printed: false,
|
printed: false,
|
||||||
state: ''
|
state: SessionState.NoState
|
||||||
}
|
}
|
||||||
|
|
||||||
public get debug() {
|
public get debug() {
|
||||||
@@ -265,6 +266,18 @@ export class SessionManager {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Add response etag to Session object.
|
||||||
|
createdSession.etag = etag
|
||||||
|
|
||||||
|
// Get session state link.
|
||||||
|
const stateLink = createdSession.links.find((link) => link.rel === 'state')
|
||||||
|
|
||||||
|
// Throw error if session state link is not present.
|
||||||
|
if (!stateLink) throw this.sessionStateLinkError
|
||||||
|
|
||||||
|
// Add session state link to Session object.
|
||||||
|
createdSession.stateUrl = stateLink.href
|
||||||
|
|
||||||
await this.waitForSession(createdSession, etag, accessToken)
|
await this.waitForSession(createdSession, etag, accessToken)
|
||||||
|
|
||||||
this.sessions.push(createdSession)
|
this.sessions.push(createdSession)
|
||||||
@@ -327,32 +340,30 @@ export class SessionManager {
|
|||||||
etag: string | null,
|
etag: string | null,
|
||||||
accessToken?: string
|
accessToken?: string
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
|
let { state: sessionState } = session
|
||||||
|
const { stateUrl } = session
|
||||||
const logger = process.logger || console
|
const logger = process.logger || console
|
||||||
|
|
||||||
let sessionState = session.state
|
|
||||||
|
|
||||||
const stateLink = session.links.find((l: any) => l.rel === 'state')
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
sessionState === 'pending' ||
|
sessionState === SessionState.Pending ||
|
||||||
sessionState === 'running' ||
|
sessionState === SessionState.Running ||
|
||||||
sessionState === ''
|
sessionState === SessionState.NoState
|
||||||
) {
|
) {
|
||||||
if (stateLink) {
|
if (stateUrl) {
|
||||||
if (this.debug && !this.printedSessionState.printed) {
|
if (this.debug && !this.printedSessionState.printed) {
|
||||||
logger.info(`Polling: ${this.serverUrl + stateLink.href}`)
|
logger.info(`Polling: ${this.serverUrl + stateUrl}`)
|
||||||
|
|
||||||
this.printedSessionState.printed = true
|
this.printedSessionState.printed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = `${this.serverUrl}${stateLink.href}?wait=30`
|
const url = `${this.serverUrl}${stateUrl}?wait=30`
|
||||||
|
|
||||||
const { result: state, responseStatus: responseStatus } =
|
const { result: state, responseStatus: responseStatus } =
|
||||||
await this.getSessionState(url, etag!, accessToken).catch((err) => {
|
await this.getSessionState(url, etag!, accessToken).catch((err) => {
|
||||||
throw prefixMessage(err, 'Error while waiting for session. ')
|
throw prefixMessage(err, 'Error while waiting for session. ')
|
||||||
})
|
})
|
||||||
|
|
||||||
sessionState = state.trim()
|
sessionState = state.trim() as SessionState
|
||||||
|
|
||||||
if (this.debug && this.printedSessionState.state !== sessionState) {
|
if (this.debug && this.printedSessionState.state !== sessionState) {
|
||||||
logger.info(`Current session state is '${sessionState}'`)
|
logger.info(`Current session state is '${sessionState}'`)
|
||||||
@@ -364,7 +375,7 @@ export class SessionManager {
|
|||||||
if (!sessionState) {
|
if (!sessionState) {
|
||||||
const stateError = new NoSessionStateError(
|
const stateError = new NoSessionStateError(
|
||||||
responseStatus,
|
responseStatus,
|
||||||
this.serverUrl + stateLink.href,
|
this.serverUrl + stateUrl,
|
||||||
session.links.find((l: any) => l.rel === 'log')?.href as string
|
session.links.find((l: any) => l.rel === 'log')?.href as string
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -386,7 +397,7 @@ export class SessionManager {
|
|||||||
|
|
||||||
return sessionState
|
return sessionState
|
||||||
} else {
|
} else {
|
||||||
throw 'Error while getting session state link. '
|
throw this.sessionStateLinkError
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.loggedErrors = []
|
this.loggedErrors = []
|
||||||
@@ -413,7 +424,7 @@ export class SessionManager {
|
|||||||
return await this.requestClient
|
return await this.requestClient
|
||||||
.get(url, accessToken, 'text/plain', { 'If-None-Match': etag })
|
.get(url, accessToken, 'text/plain', { 'If-None-Match': etag })
|
||||||
.then((res) => ({
|
.then((res) => ({
|
||||||
result: res.result as string,
|
result: res.result as SessionState,
|
||||||
responseStatus: res.status
|
responseStatus: res.status
|
||||||
}))
|
}))
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
|||||||
@@ -15,8 +15,12 @@ import { formatDataForRequest } from '../../utils/formatDataForRequest'
|
|||||||
import { pollJobState, JobState } from './pollJobState'
|
import { pollJobState, JobState } from './pollJobState'
|
||||||
import { uploadTables } from './uploadTables'
|
import { uploadTables } from './uploadTables'
|
||||||
|
|
||||||
|
interface JobRequestBody {
|
||||||
|
[key: string]: number | string | string[]
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes code on the current SAS Viya server.
|
* Executes SAS program on the current SAS Viya server using Compute API.
|
||||||
* @param jobPath - the path to the file being submitted for execution.
|
* @param jobPath - the path to the file being submitted for execution.
|
||||||
* @param linesOfCode - an array of code lines to execute.
|
* @param linesOfCode - an array of code lines to execute.
|
||||||
* @param contextName - the context to execute the code in.
|
* @param contextName - the context to execute the code in.
|
||||||
@@ -29,7 +33,7 @@ import { uploadTables } from './uploadTables'
|
|||||||
* @param printPid - a boolean that indicates whether the function should print (PID) of the started job.
|
* @param printPid - a boolean that indicates whether the function should print (PID) of the started job.
|
||||||
* @param variables - an object that represents macro variables.
|
* @param variables - an object that represents macro variables.
|
||||||
*/
|
*/
|
||||||
export async function executeScript(
|
export async function executeOnComputeApi(
|
||||||
requestClient: RequestClient,
|
requestClient: RequestClient,
|
||||||
sessionManager: SessionManager,
|
sessionManager: SessionManager,
|
||||||
rootFolderName: string,
|
rootFolderName: string,
|
||||||
@@ -46,6 +50,7 @@ export async function executeScript(
|
|||||||
variables?: MacroVar
|
variables?: MacroVar
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
let access_token = (authConfig || {}).access_token
|
let access_token = (authConfig || {}).access_token
|
||||||
|
|
||||||
if (authConfig) {
|
if (authConfig) {
|
||||||
;({ access_token } = await getTokens(requestClient, authConfig))
|
;({ access_token } = await getTokens(requestClient, authConfig))
|
||||||
}
|
}
|
||||||
@@ -78,27 +83,13 @@ export async function executeScript(
|
|||||||
const logger = process.logger || console
|
const logger = process.logger || console
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`Triggered '${relativeJobPath}' with PID ${
|
`Triggering '${relativeJobPath}' with PID ${
|
||||||
jobIdVariable.value
|
jobIdVariable.value
|
||||||
} at ${timestampToYYYYMMDDHHMMSS()}`
|
} at ${timestampToYYYYMMDDHHMMSS()}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const jobArguments: { [key: string]: any } = {
|
|
||||||
_contextName: contextName,
|
|
||||||
_OMITJSONLISTING: true,
|
|
||||||
_OMITJSONLOG: true,
|
|
||||||
_OMITSESSIONRESULTS: true,
|
|
||||||
_OMITTEXTLISTING: true,
|
|
||||||
_OMITTEXTLOG: true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (debug) {
|
|
||||||
jobArguments['_OMITTEXTLOG'] = false
|
|
||||||
jobArguments['_OMITSESSIONRESULTS'] = false
|
|
||||||
}
|
|
||||||
|
|
||||||
let fileName
|
let fileName
|
||||||
|
|
||||||
if (isRelativePath(jobPath)) {
|
if (isRelativePath(jobPath)) {
|
||||||
@@ -107,6 +98,7 @@ export async function executeScript(
|
|||||||
}`
|
}`
|
||||||
} else {
|
} else {
|
||||||
const jobPathParts = jobPath.split('/')
|
const jobPathParts = jobPath.split('/')
|
||||||
|
|
||||||
fileName = jobPathParts.pop()
|
fileName = jobPathParts.pop()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +110,6 @@ export async function executeScript(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (variables) jobVariables = { ...jobVariables, ...variables }
|
if (variables) jobVariables = { ...jobVariables, ...variables }
|
||||||
|
|
||||||
if (debug) jobVariables = { ...jobVariables, _DEBUG: 131 }
|
if (debug) jobVariables = { ...jobVariables, _DEBUG: 131 }
|
||||||
|
|
||||||
let files: any[] = []
|
let files: any[] = []
|
||||||
@@ -145,12 +136,12 @@ export async function executeScript(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Execute job in session
|
// Execute job in session
|
||||||
const jobRequestBody = {
|
const jobRequestBody: JobRequestBody = {
|
||||||
name: fileName,
|
name: fileName || 'Default Job Name',
|
||||||
description: 'Powered by SASjs',
|
description: 'Powered by SASjs',
|
||||||
code: linesOfCode,
|
code: linesOfCode,
|
||||||
variables: jobVariables,
|
variables: jobVariables,
|
||||||
arguments: jobArguments
|
version: 2
|
||||||
}
|
}
|
||||||
|
|
||||||
const { result: postedJob, etag } = await requestClient
|
const { result: postedJob, etag } = await requestClient
|
||||||
@@ -179,16 +170,21 @@ export async function executeScript(
|
|||||||
postedJob,
|
postedJob,
|
||||||
debug,
|
debug,
|
||||||
authConfig,
|
authConfig,
|
||||||
pollOptions
|
pollOptions,
|
||||||
|
{
|
||||||
|
session,
|
||||||
|
sessionManager
|
||||||
|
}
|
||||||
).catch(async (err) => {
|
).catch(async (err) => {
|
||||||
const error = err?.response?.data
|
const error = err?.response?.data
|
||||||
const result = /err=[0-9]*,/.exec(error)
|
const result = /err=[0-9]*,/.exec(error)
|
||||||
|
|
||||||
const errorCode = '5113'
|
const errorCode = '5113'
|
||||||
|
|
||||||
if (result?.[0]?.slice(4, -1) === errorCode) {
|
if (result?.[0]?.slice(4, -1) === errorCode) {
|
||||||
|
const logCount = 1000000
|
||||||
const sessionLogUrl =
|
const sessionLogUrl =
|
||||||
postedJob.links.find((l: any) => l.rel === 'up')!.href + '/log'
|
postedJob.links.find((l: any) => l.rel === 'up')!.href + '/log'
|
||||||
const logCount = 1000000
|
|
||||||
err.log = await fetchLogByChunks(
|
err.log = await fetchLogByChunks(
|
||||||
requestClient,
|
requestClient,
|
||||||
access_token!,
|
access_token!,
|
||||||
@@ -196,6 +192,7 @@ export async function executeScript(
|
|||||||
logCount
|
logCount
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
throw prefixMessage(err, 'Error while polling job status. ')
|
throw prefixMessage(err, 'Error while polling job status. ')
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -214,12 +211,12 @@ export async function executeScript(
|
|||||||
|
|
||||||
let jobResult
|
let jobResult
|
||||||
let log = ''
|
let log = ''
|
||||||
|
|
||||||
const logLink = currentJob.links.find((l) => l.rel === 'log')
|
const logLink = currentJob.links.find((l) => l.rel === 'log')
|
||||||
|
|
||||||
if (debug && logLink) {
|
if (debug && logLink) {
|
||||||
const logUrl = `${logLink.href}/content`
|
const logUrl = `${logLink.href}/content`
|
||||||
const logCount = currentJob.logStatistics?.lineCount ?? 1000000
|
const logCount = currentJob.logStatistics?.lineCount ?? 1000000
|
||||||
|
|
||||||
log = await fetchLogByChunks(
|
log = await fetchLogByChunks(
|
||||||
requestClient,
|
requestClient,
|
||||||
access_token!,
|
access_token!,
|
||||||
@@ -232,9 +229,7 @@ export async function executeScript(
|
|||||||
throw new ComputeJobExecutionError(currentJob, log)
|
throw new ComputeJobExecutionError(currentJob, log)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!expectWebout) {
|
if (!expectWebout) return { job: currentJob, log }
|
||||||
return { job: currentJob, log }
|
|
||||||
}
|
|
||||||
|
|
||||||
const resultLink = `/compute/sessions/${executionSessionId}/filerefs/_webout/content`
|
const resultLink = `/compute/sessions/${executionSessionId}/filerefs/_webout/content`
|
||||||
|
|
||||||
@@ -245,6 +240,7 @@ export async function executeScript(
|
|||||||
if (logLink) {
|
if (logLink) {
|
||||||
const logUrl = `${logLink.href}/content`
|
const logUrl = `${logLink.href}/content`
|
||||||
const logCount = currentJob.logStatistics?.lineCount ?? 1000000
|
const logCount = currentJob.logStatistics?.lineCount ?? 1000000
|
||||||
|
|
||||||
log = await fetchLogByChunks(
|
log = await fetchLogByChunks(
|
||||||
requestClient,
|
requestClient,
|
||||||
access_token!,
|
access_token!,
|
||||||
@@ -279,7 +275,7 @@ export async function executeScript(
|
|||||||
const error = e as HttpError
|
const error = e as HttpError
|
||||||
|
|
||||||
if (error.status === 404) {
|
if (error.status === 404) {
|
||||||
return executeScript(
|
return executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
rootFolderName,
|
rootFolderName,
|
||||||
@@ -3,7 +3,7 @@ import { Job, PollOptions, PollStrategy } from '../..'
|
|||||||
import { getTokens } from '../../auth/getTokens'
|
import { getTokens } from '../../auth/getTokens'
|
||||||
import { RequestClient } from '../../request/RequestClient'
|
import { RequestClient } from '../../request/RequestClient'
|
||||||
import { JobStatePollError } from '../../types/errors'
|
import { JobStatePollError } from '../../types/errors'
|
||||||
import { Link, WriteStream } from '../../types'
|
import { Link, WriteStream, SessionState, JobSessionManager } from '../../types'
|
||||||
import { delay, isNode } from '../../utils'
|
import { delay, isNode } from '../../utils'
|
||||||
|
|
||||||
export enum JobState {
|
export enum JobState {
|
||||||
@@ -37,6 +37,7 @@ export enum JobState {
|
|||||||
* { maxPollCount: 500, pollInterval: 30000 }, // approximately ~50.5 mins (including time to get response (~300ms))
|
* { maxPollCount: 500, pollInterval: 30000 }, // approximately ~50.5 mins (including time to get response (~300ms))
|
||||||
* { maxPollCount: 3400, pollInterval: 60000 } // approximately ~3015 mins (~125 hours) (including time to get response (~300ms))
|
* { maxPollCount: 3400, pollInterval: 60000 } // approximately ~3015 mins (~125 hours) (including time to get response (~300ms))
|
||||||
* ]
|
* ]
|
||||||
|
* @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.
|
||||||
* @returns - a promise which resolves with a job state
|
* @returns - a promise which resolves with a job state
|
||||||
*/
|
*/
|
||||||
export async function pollJobState(
|
export async function pollJobState(
|
||||||
@@ -44,7 +45,8 @@ export async function pollJobState(
|
|||||||
postedJob: Job,
|
postedJob: Job,
|
||||||
debug: boolean,
|
debug: boolean,
|
||||||
authConfig?: AuthConfig,
|
authConfig?: AuthConfig,
|
||||||
pollOptions?: PollOptions
|
pollOptions?: PollOptions,
|
||||||
|
jobSessionManager?: JobSessionManager
|
||||||
): Promise<JobState> {
|
): Promise<JobState> {
|
||||||
const logger = process.logger || console
|
const logger = process.logger || console
|
||||||
|
|
||||||
@@ -127,7 +129,8 @@ export async function pollJobState(
|
|||||||
pollOptions,
|
pollOptions,
|
||||||
authConfig,
|
authConfig,
|
||||||
streamLog,
|
streamLog,
|
||||||
logFileStream
|
logFileStream,
|
||||||
|
jobSessionManager
|
||||||
)
|
)
|
||||||
|
|
||||||
currentState = result.state
|
currentState = result.state
|
||||||
@@ -158,7 +161,8 @@ export async function pollJobState(
|
|||||||
defaultPollOptions,
|
defaultPollOptions,
|
||||||
authConfig,
|
authConfig,
|
||||||
streamLog,
|
streamLog,
|
||||||
logFileStream
|
logFileStream,
|
||||||
|
jobSessionManager
|
||||||
)
|
)
|
||||||
|
|
||||||
currentState = result.state
|
currentState = result.state
|
||||||
@@ -208,7 +212,21 @@ const needsRetry = (state: string) =>
|
|||||||
state === JobState.Pending ||
|
state === JobState.Pending ||
|
||||||
state === JobState.Unavailable
|
state === JobState.Unavailable
|
||||||
|
|
||||||
const doPoll = async (
|
/**
|
||||||
|
* Polls job state.
|
||||||
|
* @param requestClient - the pre-configured HTTP request client.
|
||||||
|
* @param postedJob - the relative or absolute path to the job.
|
||||||
|
* @param currentState - current job state.
|
||||||
|
* @param debug - sets the _debug flag in the job arguments.
|
||||||
|
* @param pollCount - current poll count.
|
||||||
|
* @param pollOptions - an object containing maxPollCount, pollInterval, streamLog and logFolderPath.
|
||||||
|
* @param authConfig - an access token, refresh token, client and secret for an authorized user.
|
||||||
|
* @param streamLog - indicates if job log should be streamed.
|
||||||
|
* @param logStream - job log stream.
|
||||||
|
* @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.
|
||||||
|
* @returns - a promise which resolves with a job state
|
||||||
|
*/
|
||||||
|
export const doPoll = async (
|
||||||
requestClient: RequestClient,
|
requestClient: RequestClient,
|
||||||
postedJob: Job,
|
postedJob: Job,
|
||||||
currentState: JobState,
|
currentState: JobState,
|
||||||
@@ -217,7 +235,8 @@ const doPoll = async (
|
|||||||
pollOptions: PollOptions,
|
pollOptions: PollOptions,
|
||||||
authConfig?: AuthConfig,
|
authConfig?: AuthConfig,
|
||||||
streamLog?: boolean,
|
streamLog?: boolean,
|
||||||
logStream?: WriteStream
|
logStream?: WriteStream,
|
||||||
|
jobSessionManager?: JobSessionManager
|
||||||
): Promise<{ state: JobState; pollCount: number }> => {
|
): Promise<{ state: JobState; pollCount: number }> => {
|
||||||
const { maxPollCount, pollInterval } = pollOptions
|
const { maxPollCount, pollInterval } = pollOptions
|
||||||
const logger = process.logger || console
|
const logger = process.logger || console
|
||||||
@@ -229,6 +248,35 @@ const doPoll = async (
|
|||||||
let startLogLine = 0
|
let startLogLine = 0
|
||||||
|
|
||||||
while (needsRetry(state) && pollCount <= maxPollCount) {
|
while (needsRetry(state) && pollCount <= maxPollCount) {
|
||||||
|
// Check parent session state on every 10th job state poll.
|
||||||
|
if (jobSessionManager && pollCount && pollCount % 10 === 0 && authConfig) {
|
||||||
|
const { session, sessionManager } = jobSessionManager
|
||||||
|
const { stateUrl, etag, id: sessionId } = session
|
||||||
|
const { access_token } = authConfig
|
||||||
|
const { id: jobId } = postedJob
|
||||||
|
|
||||||
|
// Get session state.
|
||||||
|
const { result: sessionState, responseStatus } = await sessionManager[
|
||||||
|
'getSessionState'
|
||||||
|
](stateUrl, etag, access_token).catch((err) => {
|
||||||
|
// Handle error while getting session state.
|
||||||
|
throw new JobStatePollError(jobId, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Clear parent session and throw an error if session state is not
|
||||||
|
// 'running' or response status is not 200.
|
||||||
|
if (sessionState !== SessionState.Running || responseStatus !== 200) {
|
||||||
|
sessionManager.clearSession(sessionId, access_token)
|
||||||
|
|
||||||
|
const sessionError =
|
||||||
|
sessionState !== SessionState.Running
|
||||||
|
? `Session state of the job is not 'running'. Session state is '${sessionState}'`
|
||||||
|
: `Session response status is not 200. Session response status is ${responseStatus}.`
|
||||||
|
|
||||||
|
throw new JobStatePollError(jobId, new Error(sessionError))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
state = await getJobState(
|
state = await getJobState(
|
||||||
requestClient,
|
requestClient,
|
||||||
postedJob,
|
postedJob,
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { RequestClient } from '../../../request/RequestClient'
|
import { RequestClient } from '../../../request/RequestClient'
|
||||||
import { SessionManager } from '../../../SessionManager'
|
import { SessionManager } from '../../../SessionManager'
|
||||||
import { executeScript } from '../executeScript'
|
import { executeOnComputeApi } from '../executeOnComputeApi'
|
||||||
import { mockSession, mockAuthConfig, mockJob } from './mockResponses'
|
import { mockSession, mockAuthConfig, mockJob } from './mockResponses'
|
||||||
import * as pollJobStateModule from '../pollJobState'
|
import * as pollJobStateModule from '../pollJobState'
|
||||||
import * as uploadTablesModule from '../uploadTables'
|
import * as uploadTablesModule from '../uploadTables'
|
||||||
import * as getTokensModule from '../../../auth/getTokens'
|
import * as getTokensModule from '../../../auth/getTokens'
|
||||||
import * as formatDataModule from '../../../utils/formatDataForRequest'
|
import * as formatDataModule from '../../../utils/formatDataForRequest'
|
||||||
import * as fetchLogsModule from '../../../utils/fetchLogByChunks'
|
import * as fetchLogsModule from '../../../utils/fetchLogByChunks'
|
||||||
import { PollOptions } from '../../../types'
|
import { PollOptions, JobSessionManager } from '../../../types'
|
||||||
import { ComputeJobExecutionError, NotFoundError } from '../../../types/errors'
|
import { ComputeJobExecutionError, NotFoundError } from '../../../types/errors'
|
||||||
import { Logger, LogLevel } from '@sasjs/utils/logger'
|
import { Logger, LogLevel } from '@sasjs/utils/logger'
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ describe('executeScript', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should not try to get fresh tokens if an authConfig is not provided', async () => {
|
it('should not try to get fresh tokens if an authConfig is not provided', async () => {
|
||||||
await executeScript(
|
await executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
'test',
|
'test',
|
||||||
@@ -38,7 +38,7 @@ describe('executeScript', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should try to get fresh tokens if an authConfig is provided', async () => {
|
it('should try to get fresh tokens if an authConfig is provided', async () => {
|
||||||
await executeScript(
|
await executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
'test',
|
'test',
|
||||||
@@ -55,7 +55,7 @@ describe('executeScript', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should get a session from the session manager before executing', async () => {
|
it('should get a session from the session manager before executing', async () => {
|
||||||
await executeScript(
|
await executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
'test',
|
'test',
|
||||||
@@ -72,7 +72,7 @@ describe('executeScript', () => {
|
|||||||
.spyOn(sessionManager, 'getSession')
|
.spyOn(sessionManager, 'getSession')
|
||||||
.mockImplementation(() => Promise.reject('Test Error'))
|
.mockImplementation(() => Promise.reject('Test Error'))
|
||||||
|
|
||||||
const error = await executeScript(
|
const error = await executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
'test',
|
'test',
|
||||||
@@ -85,7 +85,7 @@ describe('executeScript', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should fetch the PID when printPid is true', async () => {
|
it('should fetch the PID when printPid is true', async () => {
|
||||||
await executeScript(
|
await executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
'test',
|
'test',
|
||||||
@@ -113,7 +113,7 @@ describe('executeScript', () => {
|
|||||||
.spyOn(sessionManager, 'getVariable')
|
.spyOn(sessionManager, 'getVariable')
|
||||||
.mockImplementation(() => Promise.reject('Test Error'))
|
.mockImplementation(() => Promise.reject('Test Error'))
|
||||||
|
|
||||||
const error = await executeScript(
|
const error = await executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
'test',
|
'test',
|
||||||
@@ -139,7 +139,7 @@ describe('executeScript', () => {
|
|||||||
Promise.resolve([{ tableName: 'test', file: { id: 1 } }])
|
Promise.resolve([{ tableName: 'test', file: { id: 1 } }])
|
||||||
)
|
)
|
||||||
|
|
||||||
await executeScript(
|
await executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
'test',
|
'test',
|
||||||
@@ -163,7 +163,7 @@ describe('executeScript', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should format data as CSV when it does not contain semicolons', async () => {
|
it('should format data as CSV when it does not contain semicolons', async () => {
|
||||||
await executeScript(
|
await executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
'test',
|
'test',
|
||||||
@@ -189,7 +189,7 @@ describe('executeScript', () => {
|
|||||||
.spyOn(formatDataModule, 'formatDataForRequest')
|
.spyOn(formatDataModule, 'formatDataForRequest')
|
||||||
.mockImplementation(() => ({ sasjs_tables: 'foo', sasjs0data: 'bar' }))
|
.mockImplementation(() => ({ sasjs_tables: 'foo', sasjs0data: 'bar' }))
|
||||||
|
|
||||||
await executeScript(
|
await executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
'test',
|
'test',
|
||||||
@@ -217,14 +217,7 @@ describe('executeScript', () => {
|
|||||||
sasjs_tables: 'foo',
|
sasjs_tables: 'foo',
|
||||||
sasjs0data: 'bar'
|
sasjs0data: 'bar'
|
||||||
},
|
},
|
||||||
arguments: {
|
version: 2
|
||||||
_contextName: 'test context',
|
|
||||||
_OMITJSONLISTING: true,
|
|
||||||
_OMITJSONLOG: true,
|
|
||||||
_OMITSESSIONRESULTS: true,
|
|
||||||
_OMITTEXTLISTING: true,
|
|
||||||
_OMITTEXTLOG: true
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
mockAuthConfig.access_token
|
mockAuthConfig.access_token
|
||||||
)
|
)
|
||||||
@@ -235,7 +228,7 @@ describe('executeScript', () => {
|
|||||||
.spyOn(formatDataModule, 'formatDataForRequest')
|
.spyOn(formatDataModule, 'formatDataForRequest')
|
||||||
.mockImplementation(() => ({ sasjs_tables: 'foo', sasjs0data: 'bar' }))
|
.mockImplementation(() => ({ sasjs_tables: 'foo', sasjs0data: 'bar' }))
|
||||||
|
|
||||||
await executeScript(
|
await executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
'test',
|
'test',
|
||||||
@@ -264,14 +257,7 @@ describe('executeScript', () => {
|
|||||||
sasjs0data: 'bar',
|
sasjs0data: 'bar',
|
||||||
_DEBUG: 131
|
_DEBUG: 131
|
||||||
},
|
},
|
||||||
arguments: {
|
version: 2
|
||||||
_contextName: 'test context',
|
|
||||||
_OMITJSONLISTING: true,
|
|
||||||
_OMITJSONLOG: true,
|
|
||||||
_OMITSESSIONRESULTS: false,
|
|
||||||
_OMITTEXTLISTING: true,
|
|
||||||
_OMITTEXTLOG: false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
mockAuthConfig.access_token
|
mockAuthConfig.access_token
|
||||||
)
|
)
|
||||||
@@ -282,7 +268,7 @@ describe('executeScript', () => {
|
|||||||
.spyOn(requestClient, 'post')
|
.spyOn(requestClient, 'post')
|
||||||
.mockImplementation(() => Promise.reject('Test Error'))
|
.mockImplementation(() => Promise.reject('Test Error'))
|
||||||
|
|
||||||
const error = await executeScript(
|
const error = await executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
'test',
|
'test',
|
||||||
@@ -302,7 +288,7 @@ describe('executeScript', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should immediately return the session when waitForResult is false', async () => {
|
it('should immediately return the session when waitForResult is false', async () => {
|
||||||
const result = await executeScript(
|
const result = await executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
'test',
|
'test',
|
||||||
@@ -322,7 +308,12 @@ describe('executeScript', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should poll for job completion when waitForResult is true', async () => {
|
it('should poll for job completion when waitForResult is true', async () => {
|
||||||
await executeScript(
|
const jobSessionManager: JobSessionManager = {
|
||||||
|
session: mockSession,
|
||||||
|
sessionManager: sessionManager
|
||||||
|
}
|
||||||
|
|
||||||
|
await executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
'test',
|
'test',
|
||||||
@@ -343,7 +334,8 @@ describe('executeScript', () => {
|
|||||||
mockJob,
|
mockJob,
|
||||||
false,
|
false,
|
||||||
mockAuthConfig,
|
mockAuthConfig,
|
||||||
defaultPollOptions
|
defaultPollOptions,
|
||||||
|
jobSessionManager
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -352,7 +344,7 @@ describe('executeScript', () => {
|
|||||||
.spyOn(pollJobStateModule, 'pollJobState')
|
.spyOn(pollJobStateModule, 'pollJobState')
|
||||||
.mockImplementation(() => Promise.reject('Poll Error'))
|
.mockImplementation(() => Promise.reject('Poll Error'))
|
||||||
|
|
||||||
const error = await executeScript(
|
const error = await executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
'test',
|
'test',
|
||||||
@@ -378,7 +370,7 @@ describe('executeScript', () => {
|
|||||||
Promise.reject({ response: { data: 'err=5113,' } })
|
Promise.reject({ response: { data: 'err=5113,' } })
|
||||||
)
|
)
|
||||||
|
|
||||||
const error = await executeScript(
|
const error = await executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
'test',
|
'test',
|
||||||
@@ -404,7 +396,7 @@ describe('executeScript', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should fetch the logs for the job if debug is true and a log URL is available', async () => {
|
it('should fetch the logs for the job if debug is true and a log URL is available', async () => {
|
||||||
await executeScript(
|
await executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
'test',
|
'test',
|
||||||
@@ -429,7 +421,7 @@ describe('executeScript', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should not fetch the logs for the job if debug is false', async () => {
|
it('should not fetch the logs for the job if debug is false', async () => {
|
||||||
await executeScript(
|
await executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
'test',
|
'test',
|
||||||
@@ -455,7 +447,7 @@ describe('executeScript', () => {
|
|||||||
Promise.resolve(pollJobStateModule.JobState.Failed)
|
Promise.resolve(pollJobStateModule.JobState.Failed)
|
||||||
)
|
)
|
||||||
|
|
||||||
const error: ComputeJobExecutionError = await executeScript(
|
const error: ComputeJobExecutionError = await executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
'test',
|
'test',
|
||||||
@@ -490,7 +482,7 @@ describe('executeScript', () => {
|
|||||||
Promise.resolve(pollJobStateModule.JobState.Error)
|
Promise.resolve(pollJobStateModule.JobState.Error)
|
||||||
)
|
)
|
||||||
|
|
||||||
const error: ComputeJobExecutionError = await executeScript(
|
const error: ComputeJobExecutionError = await executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
'test',
|
'test',
|
||||||
@@ -519,7 +511,7 @@ describe('executeScript', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should fetch the result if expectWebout is true', async () => {
|
it('should fetch the result if expectWebout is true', async () => {
|
||||||
await executeScript(
|
await executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
'test',
|
'test',
|
||||||
@@ -550,7 +542,7 @@ describe('executeScript', () => {
|
|||||||
return Promise.resolve({ result: mockJob, etag: '', status: 200 })
|
return Promise.resolve({ result: mockJob, etag: '', status: 200 })
|
||||||
})
|
})
|
||||||
|
|
||||||
const error = await executeScript(
|
const error = await executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
'test',
|
'test',
|
||||||
@@ -584,7 +576,7 @@ describe('executeScript', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should clear the session after execution is complete', async () => {
|
it('should clear the session after execution is complete', async () => {
|
||||||
await executeScript(
|
await executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
'test',
|
'test',
|
||||||
@@ -611,7 +603,7 @@ describe('executeScript', () => {
|
|||||||
.spyOn(sessionManager, 'clearSession')
|
.spyOn(sessionManager, 'clearSession')
|
||||||
.mockImplementation(() => Promise.reject('Clear Session Error'))
|
.mockImplementation(() => Promise.reject('Clear Session Error'))
|
||||||
|
|
||||||
const error = await executeScript(
|
const error = await executeOnComputeApi(
|
||||||
requestClient,
|
requestClient,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
'test',
|
'test',
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import { AuthConfig } from '@sasjs/utils/types'
|
import { AuthConfig } from '@sasjs/utils/types'
|
||||||
import { Job, Session } from '../../../types'
|
import { Job, Session, SessionState } from '../../../types'
|
||||||
|
|
||||||
export const mockSession: Session = {
|
export const mockSession: Session = {
|
||||||
id: 's35510n',
|
id: 's35510n',
|
||||||
state: 'idle',
|
state: SessionState.Idle,
|
||||||
|
stateUrl: '',
|
||||||
links: [],
|
links: [],
|
||||||
attributes: {
|
attributes: {
|
||||||
sessionInactiveTimeout: 1
|
sessionInactiveTimeout: 1
|
||||||
},
|
},
|
||||||
creationTimeStamp: new Date().valueOf().toString()
|
creationTimeStamp: new Date().valueOf().toString(),
|
||||||
|
etag: 'etag-string'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mockJob: Job = {
|
export const mockJob: Job = {
|
||||||
|
|||||||
@@ -1,17 +1,25 @@
|
|||||||
import { Logger, LogLevel } from '@sasjs/utils/logger'
|
import { Logger, LogLevel } from '@sasjs/utils/logger'
|
||||||
import { RequestClient } from '../../../request/RequestClient'
|
import { RequestClient } from '../../../request/RequestClient'
|
||||||
import { mockAuthConfig, mockJob } from './mockResponses'
|
import { mockAuthConfig, mockJob } from './mockResponses'
|
||||||
import { pollJobState } from '../pollJobState'
|
import { pollJobState, doPoll, JobState } from '../pollJobState'
|
||||||
import * as getTokensModule from '../../../auth/getTokens'
|
import * as getTokensModule from '../../../auth/getTokens'
|
||||||
import * as saveLogModule from '../saveLog'
|
import * as saveLogModule from '../saveLog'
|
||||||
import * as getFileStreamModule from '../getFileStream'
|
import * as getFileStreamModule from '../getFileStream'
|
||||||
import * as isNodeModule from '../../../utils/isNode'
|
import * as isNodeModule from '../../../utils/isNode'
|
||||||
import * as delayModule from '../../../utils/delay'
|
import * as delayModule from '../../../utils/delay'
|
||||||
import { PollOptions, PollStrategy } from '../../../types'
|
import {
|
||||||
|
PollOptions,
|
||||||
|
PollStrategy,
|
||||||
|
SessionState,
|
||||||
|
JobSessionManager
|
||||||
|
} from '../../../types'
|
||||||
import { WriteStream } from 'fs'
|
import { WriteStream } from 'fs'
|
||||||
|
import { SessionManager } from '../../../SessionManager'
|
||||||
|
import { JobStatePollError } from '../../../types'
|
||||||
|
|
||||||
const baseUrl = 'http://localhost'
|
const baseUrl = 'http://localhost'
|
||||||
const requestClient = new (<jest.Mock<RequestClient>>RequestClient)()
|
const requestClient = new (<jest.Mock<RequestClient>>RequestClient)()
|
||||||
|
const sessionManager = new (<jest.Mock<SessionManager>>SessionManager)()
|
||||||
requestClient['httpClient'].defaults.baseURL = baseUrl
|
requestClient['httpClient'].defaults.baseURL = baseUrl
|
||||||
|
|
||||||
const defaultStreamLog = false
|
const defaultStreamLog = false
|
||||||
@@ -276,6 +284,76 @@ describe('pollJobState', () => {
|
|||||||
expect(delays).toEqual([pollIntervals[0], ...pollIntervals])
|
expect(delays).toEqual([pollIntervals[0], ...pollIntervals])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should change default poll strategies after completing provided poll options', async () => {
|
||||||
|
const delays: number[] = []
|
||||||
|
|
||||||
|
jest.spyOn(delayModule, 'delay').mockImplementation((ms: number) => {
|
||||||
|
delays.push(ms)
|
||||||
|
|
||||||
|
return Promise.resolve()
|
||||||
|
})
|
||||||
|
|
||||||
|
const customPollOptions: PollOptions = {
|
||||||
|
maxPollCount: 0,
|
||||||
|
pollInterval: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const requests = [
|
||||||
|
{ maxPollCount: 202, pollInterval: 300 },
|
||||||
|
{ maxPollCount: 300, pollInterval: 3000 },
|
||||||
|
{ maxPollCount: 500, pollInterval: 30000 },
|
||||||
|
{ maxPollCount: 3400, pollInterval: 60000 }
|
||||||
|
]
|
||||||
|
|
||||||
|
// ~200 requests with delay 300ms
|
||||||
|
let request = requests.splice(0, 1)[0]
|
||||||
|
let { maxPollCount, pollInterval } = request
|
||||||
|
|
||||||
|
// should be only one interval because maxPollCount is equal to 0
|
||||||
|
const pollIntervals = [customPollOptions.pollInterval]
|
||||||
|
|
||||||
|
pollIntervals.push(...Array(maxPollCount - 2).fill(pollInterval))
|
||||||
|
|
||||||
|
// ~300 requests with delay 3000
|
||||||
|
request = requests.splice(0, 1)[0]
|
||||||
|
let newAmount = request.maxPollCount
|
||||||
|
pollInterval = request.pollInterval
|
||||||
|
|
||||||
|
pollIntervals.push(...Array(newAmount - maxPollCount).fill(pollInterval))
|
||||||
|
pollIntervals.push(...Array(2).fill(pollInterval))
|
||||||
|
|
||||||
|
// ~500 requests with delay 30000
|
||||||
|
request = requests.splice(0, 1)[0]
|
||||||
|
|
||||||
|
let oldAmount = newAmount
|
||||||
|
newAmount = request.maxPollCount
|
||||||
|
pollInterval = request.pollInterval
|
||||||
|
|
||||||
|
pollIntervals.push(...Array(newAmount - oldAmount - 2).fill(pollInterval))
|
||||||
|
pollIntervals.push(...Array(2).fill(pollInterval))
|
||||||
|
|
||||||
|
// ~3400 requests with delay 60000
|
||||||
|
request = requests.splice(0, 1)[0]
|
||||||
|
|
||||||
|
oldAmount = newAmount
|
||||||
|
newAmount = request.maxPollCount
|
||||||
|
pollInterval = request.pollInterval
|
||||||
|
|
||||||
|
mockSimplePoll(newAmount)
|
||||||
|
|
||||||
|
pollIntervals.push(...Array(newAmount - oldAmount - 2).fill(pollInterval))
|
||||||
|
|
||||||
|
await pollJobState(
|
||||||
|
requestClient,
|
||||||
|
mockJob,
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
customPollOptions
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(delays).toEqual(pollIntervals)
|
||||||
|
})
|
||||||
|
|
||||||
it('should throw an error if not valid poll strategies provided', async () => {
|
it('should throw an error if not valid poll strategies provided', async () => {
|
||||||
// INFO: 'maxPollCount' has to be > 0
|
// INFO: 'maxPollCount' has to be > 0
|
||||||
let invalidPollStrategy = {
|
let invalidPollStrategy = {
|
||||||
@@ -353,6 +431,218 @@ describe('pollJobState', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('doPoll', () => {
|
||||||
|
const sessionStateLink = '/compute/sessions/session-id-ses0000/state'
|
||||||
|
const jobSessionManager: JobSessionManager = {
|
||||||
|
sessionManager,
|
||||||
|
session: {
|
||||||
|
id: ['id', new Date().getTime(), Math.random()].join('-'),
|
||||||
|
state: SessionState.NoState,
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
href: sessionStateLink,
|
||||||
|
method: 'GET',
|
||||||
|
rel: 'state',
|
||||||
|
type: 'text/plain',
|
||||||
|
uri: sessionStateLink
|
||||||
|
}
|
||||||
|
],
|
||||||
|
attributes: {
|
||||||
|
sessionInactiveTimeout: 900
|
||||||
|
},
|
||||||
|
creationTimeStamp: `${new Date(new Date().getTime()).toISOString()}`,
|
||||||
|
stateUrl: '',
|
||||||
|
etag: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
setupMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should check session state on every 10th job state poll', async () => {
|
||||||
|
const mockedGetSessionState = jest
|
||||||
|
.spyOn(sessionManager as any, 'getSessionState')
|
||||||
|
.mockImplementation(() => {
|
||||||
|
return Promise.resolve({
|
||||||
|
result: SessionState.Running,
|
||||||
|
responseStatus: 200
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
let getSessionStateCount = 0
|
||||||
|
jest.spyOn(requestClient, 'get').mockImplementation(() => {
|
||||||
|
getSessionStateCount++
|
||||||
|
|
||||||
|
return Promise.resolve({
|
||||||
|
result:
|
||||||
|
getSessionStateCount < 20 ? JobState.Running : JobState.Completed,
|
||||||
|
etag: 'etag-string',
|
||||||
|
status: 200
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
await doPoll(
|
||||||
|
requestClient,
|
||||||
|
mockJob,
|
||||||
|
JobState.Running,
|
||||||
|
false,
|
||||||
|
1,
|
||||||
|
defaultPollStrategy,
|
||||||
|
mockAuthConfig,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
jobSessionManager
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(mockedGetSessionState).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle error while checking session state', async () => {
|
||||||
|
const sessionStateError = 'Error while getting session state.'
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(sessionManager as any, 'getSessionState')
|
||||||
|
.mockImplementation(() => {
|
||||||
|
return Promise.reject(sessionStateError)
|
||||||
|
})
|
||||||
|
|
||||||
|
jest.spyOn(requestClient, 'get').mockImplementation(() => {
|
||||||
|
return Promise.resolve({
|
||||||
|
result: JobState.Running,
|
||||||
|
etag: 'etag-string',
|
||||||
|
status: 200
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
doPoll(
|
||||||
|
requestClient,
|
||||||
|
mockJob,
|
||||||
|
JobState.Running,
|
||||||
|
false,
|
||||||
|
1,
|
||||||
|
defaultPollStrategy,
|
||||||
|
mockAuthConfig,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
jobSessionManager
|
||||||
|
)
|
||||||
|
).rejects.toEqual(
|
||||||
|
new JobStatePollError(mockJob.id, new Error(sessionStateError))
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw an error if session is not in running state', async () => {
|
||||||
|
const filteredSessionStates = Object.values(SessionState).filter(
|
||||||
|
(state) => state !== SessionState.Running
|
||||||
|
)
|
||||||
|
const randomSessionState =
|
||||||
|
filteredSessionStates[
|
||||||
|
Math.floor(Math.random() * filteredSessionStates.length)
|
||||||
|
]
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(sessionManager as any, 'getSessionState')
|
||||||
|
.mockImplementation(() => {
|
||||||
|
return Promise.resolve({
|
||||||
|
result: randomSessionState,
|
||||||
|
responseStatus: 200
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
jest.spyOn(requestClient, 'get').mockImplementation(() => {
|
||||||
|
return Promise.resolve({
|
||||||
|
result: JobState.Running,
|
||||||
|
etag: 'etag-string',
|
||||||
|
status: 200
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const mockedClearSession = jest
|
||||||
|
.spyOn(sessionManager, 'clearSession')
|
||||||
|
.mockImplementation(() => Promise.resolve())
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
doPoll(
|
||||||
|
requestClient,
|
||||||
|
mockJob,
|
||||||
|
JobState.Running,
|
||||||
|
false,
|
||||||
|
1,
|
||||||
|
defaultPollStrategy,
|
||||||
|
mockAuthConfig,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
jobSessionManager
|
||||||
|
)
|
||||||
|
).rejects.toEqual(
|
||||||
|
new JobStatePollError(
|
||||||
|
mockJob.id,
|
||||||
|
new Error(
|
||||||
|
`Session state of the job is not 'running'. Session state is '${randomSessionState}'`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(mockedClearSession).toHaveBeenCalledWith(
|
||||||
|
jobSessionManager.session.id,
|
||||||
|
mockAuthConfig.access_token
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle throw an error if response status of session state is not 200', async () => {
|
||||||
|
const sessionStateResponseStatus = 500
|
||||||
|
jest
|
||||||
|
.spyOn(sessionManager as any, 'getSessionState')
|
||||||
|
.mockImplementation(() => {
|
||||||
|
return Promise.resolve({
|
||||||
|
result: SessionState.Running,
|
||||||
|
responseStatus: sessionStateResponseStatus
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
jest.spyOn(requestClient, 'get').mockImplementation(() => {
|
||||||
|
return Promise.resolve({
|
||||||
|
result: JobState.Running,
|
||||||
|
etag: 'etag-string',
|
||||||
|
status: 200
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const mockedClearSession = jest
|
||||||
|
.spyOn(sessionManager, 'clearSession')
|
||||||
|
.mockImplementation(() => Promise.resolve())
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
doPoll(
|
||||||
|
requestClient,
|
||||||
|
mockJob,
|
||||||
|
JobState.Running,
|
||||||
|
false,
|
||||||
|
1,
|
||||||
|
defaultPollStrategy,
|
||||||
|
mockAuthConfig,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
jobSessionManager
|
||||||
|
)
|
||||||
|
).rejects.toEqual(
|
||||||
|
new JobStatePollError(
|
||||||
|
mockJob.id,
|
||||||
|
new Error(
|
||||||
|
`Session response status is not 200. Session response status is ${sessionStateResponseStatus}.`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(mockedClearSession).toHaveBeenCalledWith(
|
||||||
|
jobSessionManager.session.id,
|
||||||
|
mockAuthConfig.access_token
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const setupMocks = () => {
|
const setupMocks = () => {
|
||||||
jest.restoreAllMocks()
|
jest.restoreAllMocks()
|
||||||
jest.mock('../../../request/RequestClient')
|
jest.mock('../../../request/RequestClient')
|
||||||
|
|||||||
@@ -233,7 +233,8 @@ export default class SASjs {
|
|||||||
this.requestClient = new RequestClient(
|
this.requestClient = new RequestClient(
|
||||||
this.sasjsConfig.serverUrl,
|
this.sasjsConfig.serverUrl,
|
||||||
this.sasjsConfig.httpsAgentOptions,
|
this.sasjsConfig.httpsAgentOptions,
|
||||||
this.sasjsConfig.requestHistoryLimit
|
this.sasjsConfig.requestHistoryLimit,
|
||||||
|
this.sasjsConfig.verbose
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
this.requestClient.setConfig(
|
this.requestClient.setConfig(
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
import { RequestClient } from '../../request/RequestClient'
|
import { RequestClient } from '../../request/RequestClient'
|
||||||
import {
|
import {
|
||||||
isRelativePath,
|
isRelativePath,
|
||||||
parseSasViyaDebugResponse,
|
|
||||||
appendExtraResponseAttributes,
|
appendExtraResponseAttributes,
|
||||||
convertToCSV
|
convertToCSV
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
import {
|
||||||
|
AxiosError,
|
||||||
|
AxiosInstance,
|
||||||
|
AxiosRequestConfig,
|
||||||
|
AxiosResponse
|
||||||
|
} from 'axios'
|
||||||
|
import axios from 'axios'
|
||||||
import * as https from 'https'
|
import * as https from 'https'
|
||||||
import { CsrfToken } from '..'
|
import { CsrfToken } from '..'
|
||||||
import { isAuthorizeFormRequired, isLogInRequired } from '../auth'
|
import { isAuthorizeFormRequired, isLogInRequired } from '../auth'
|
||||||
@@ -10,7 +16,7 @@ import {
|
|||||||
JobExecutionError,
|
JobExecutionError,
|
||||||
CertificateError
|
CertificateError
|
||||||
} from '../types/errors'
|
} from '../types/errors'
|
||||||
import { SASjsRequest } from '../types'
|
import { SASjsRequest, HttpClient, VerboseMode } from '../types'
|
||||||
import { parseWeboutResponse } from '../utils/parseWeboutResponse'
|
import { parseWeboutResponse } from '../utils/parseWeboutResponse'
|
||||||
import { prefixMessage } from '@sasjs/utils/error'
|
import { prefixMessage } from '@sasjs/utils/error'
|
||||||
import { SAS9AuthError } from '../types/errors/SAS9AuthError'
|
import { SAS9AuthError } from '../types/errors/SAS9AuthError'
|
||||||
@@ -22,45 +28,11 @@ import {
|
|||||||
import { InvalidSASjsCsrfError } from '../types/errors/InvalidSASjsCsrfError'
|
import { InvalidSASjsCsrfError } from '../types/errors/InvalidSASjsCsrfError'
|
||||||
import { inspect } from 'util'
|
import { inspect } from 'util'
|
||||||
|
|
||||||
export interface HttpClient {
|
|
||||||
get<T>(
|
|
||||||
url: string,
|
|
||||||
accessToken: string | undefined,
|
|
||||||
contentType: string,
|
|
||||||
overrideHeaders: { [key: string]: string | number }
|
|
||||||
): Promise<{ result: T; etag: string }>
|
|
||||||
|
|
||||||
post<T>(
|
|
||||||
url: string,
|
|
||||||
data: any,
|
|
||||||
accessToken: string | undefined,
|
|
||||||
contentType: string,
|
|
||||||
overrideHeaders: { [key: string]: string | number }
|
|
||||||
): Promise<{ result: T; etag: string }>
|
|
||||||
|
|
||||||
put<T>(
|
|
||||||
url: string,
|
|
||||||
data: any,
|
|
||||||
accessToken: string | undefined,
|
|
||||||
overrideHeaders: { [key: string]: string | number }
|
|
||||||
): Promise<{ result: T; etag: string }>
|
|
||||||
|
|
||||||
delete<T>(
|
|
||||||
url: string,
|
|
||||||
accessToken: string | undefined
|
|
||||||
): Promise<{ result: T; etag: string }>
|
|
||||||
|
|
||||||
getCsrfToken(type: 'general' | 'file'): CsrfToken | undefined
|
|
||||||
saveLocalStorageToken(accessToken: string, refreshToken: string): void
|
|
||||||
clearCsrfTokens(): void
|
|
||||||
clearLocalStorageTokens(): void
|
|
||||||
getBaseUrl(): string
|
|
||||||
}
|
|
||||||
|
|
||||||
export class RequestClient implements HttpClient {
|
export class RequestClient implements HttpClient {
|
||||||
private requests: SASjsRequest[] = []
|
private requests: SASjsRequest[] = []
|
||||||
private requestsLimit: number = 10
|
private requestsLimit: number = 10
|
||||||
private httpInterceptor?: number
|
private httpInterceptor?: number
|
||||||
|
private verboseMode: VerboseMode = false
|
||||||
|
|
||||||
protected csrfToken: CsrfToken = { headerName: '', value: '' }
|
protected csrfToken: CsrfToken = { headerName: '', value: '' }
|
||||||
protected fileUploadCsrfToken: CsrfToken | undefined
|
protected fileUploadCsrfToken: CsrfToken | undefined
|
||||||
@@ -69,11 +41,17 @@ export class RequestClient implements HttpClient {
|
|||||||
constructor(
|
constructor(
|
||||||
protected baseUrl: string,
|
protected baseUrl: string,
|
||||||
httpsAgentOptions?: https.AgentOptions,
|
httpsAgentOptions?: https.AgentOptions,
|
||||||
requestsLimit?: number
|
requestsLimit?: number,
|
||||||
|
verboseMode?: VerboseMode
|
||||||
) {
|
) {
|
||||||
this.createHttpClient(baseUrl, httpsAgentOptions)
|
this.createHttpClient(baseUrl, httpsAgentOptions)
|
||||||
|
|
||||||
if (requestsLimit) this.requestsLimit = requestsLimit
|
if (requestsLimit) this.requestsLimit = requestsLimit
|
||||||
|
|
||||||
|
if (verboseMode) {
|
||||||
|
this.setVerboseMode(verboseMode)
|
||||||
|
this.enableVerboseMode()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public setConfig(baseUrl: string, httpsAgentOptions?: https.AgentOptions) {
|
public setConfig(baseUrl: string, httpsAgentOptions?: https.AgentOptions) {
|
||||||
@@ -93,6 +71,7 @@ export class RequestClient implements HttpClient {
|
|||||||
this.csrfToken = { headerName: '', value: '' }
|
this.csrfToken = { headerName: '', value: '' }
|
||||||
this.fileUploadCsrfToken = { headerName: '', value: '' }
|
this.fileUploadCsrfToken = { headerName: '', value: '' }
|
||||||
}
|
}
|
||||||
|
|
||||||
public clearLocalStorageTokens() {
|
public clearLocalStorageTokens() {
|
||||||
localStorage.setItem('accessToken', '')
|
localStorage.setItem('accessToken', '')
|
||||||
localStorage.setItem('refreshToken', '')
|
localStorage.setItem('refreshToken', '')
|
||||||
@@ -395,10 +374,12 @@ export class RequestClient implements HttpClient {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds colors to the string.
|
* Adds colors to the string.
|
||||||
|
* If verboseMode is set to 'bleached', colors should be disabled
|
||||||
* @param str - string to be prettified.
|
* @param str - string to be prettified.
|
||||||
* @returns - prettified string
|
* @returns - prettified string
|
||||||
*/
|
*/
|
||||||
private prettifyString = (str: any) => inspect(str, { colors: true })
|
private prettifyString = (str: any) =>
|
||||||
|
inspect(str, { colors: this.verboseMode !== 'bleached' })
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formats HTTP request/response body.
|
* Formats HTTP request/response body.
|
||||||
@@ -432,10 +413,73 @@ export class RequestClient implements HttpClient {
|
|||||||
return bodyLines.join('\n')
|
return bodyLines.join('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
private defaultInterceptionCallBack = (response: AxiosResponse) => {
|
private defaultInterceptionCallBack = (
|
||||||
const { status, config, request, data: resData } = response
|
axiosResponse: AxiosResponse | AxiosError
|
||||||
|
) => {
|
||||||
|
// Message indicating absent value.
|
||||||
|
const noValueMessage = 'Not provided'
|
||||||
|
|
||||||
|
// Fallback request object that can be safely used to form request summary.
|
||||||
|
type FallbackRequest = { _header?: string; res: { rawHeaders: string[] } }
|
||||||
|
// _header is not present in responses with status 1**
|
||||||
|
// rawHeaders are not present in responses with status 1**
|
||||||
|
let fallbackRequest: FallbackRequest = {
|
||||||
|
_header: `${noValueMessage}\n`,
|
||||||
|
res: { rawHeaders: [noValueMessage] }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback response object that can be safely used to form response summary.
|
||||||
|
type FallbackResponse = {
|
||||||
|
status?: number | string
|
||||||
|
request?: FallbackRequest
|
||||||
|
config: { data?: string }
|
||||||
|
data?: unknown
|
||||||
|
}
|
||||||
|
let fallbackResponse: FallbackResponse = axiosResponse
|
||||||
|
|
||||||
|
if (axios.isAxiosError(axiosResponse)) {
|
||||||
|
const { response, request, config } = axiosResponse
|
||||||
|
|
||||||
|
// Try to use axiosResponse.response to form response summary.
|
||||||
|
if (response) {
|
||||||
|
fallbackResponse = response
|
||||||
|
} else {
|
||||||
|
// Try to use axiosResponse.request to form request summary.
|
||||||
|
if (request) {
|
||||||
|
const { _header, _currentRequest } = request
|
||||||
|
|
||||||
|
// Try to use axiosResponse.request._header to form request summary.
|
||||||
|
if (_header) {
|
||||||
|
fallbackRequest._header = _header
|
||||||
|
}
|
||||||
|
// Try to use axiosResponse.request._currentRequest._header to form request summary.
|
||||||
|
else if (_currentRequest && _currentRequest._header) {
|
||||||
|
fallbackRequest._header = _currentRequest._header
|
||||||
|
}
|
||||||
|
|
||||||
|
const { res } = request
|
||||||
|
|
||||||
|
// Try to use axiosResponse.request.res.rawHeaders to form request summary.
|
||||||
|
if (res && res.rawHeaders) {
|
||||||
|
fallbackRequest.res.rawHeaders = res.rawHeaders
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback config that can be safely used to form response summary.
|
||||||
|
const fallbackConfig = { data: noValueMessage }
|
||||||
|
|
||||||
|
fallbackResponse = {
|
||||||
|
status: noValueMessage,
|
||||||
|
request: fallbackRequest,
|
||||||
|
config: config || fallbackConfig,
|
||||||
|
data: noValueMessage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { status, config, request, data: resData } = fallbackResponse
|
||||||
const { data: reqData } = config
|
const { data: reqData } = config
|
||||||
const { _header: reqHeaders, res } = request
|
const { _header: reqHeaders, res } = request || fallbackRequest
|
||||||
const { rawHeaders } = res
|
const { rawHeaders } = res
|
||||||
|
|
||||||
// Converts an array of strings into a single string with the following format:
|
// Converts an array of strings into a single string with the following format:
|
||||||
@@ -465,7 +509,18 @@ HTTP Response (first 50 lines):
|
|||||||
${resHeaders}${parsedResBody ? `\n\n${parsedResBody}` : ''}
|
${resHeaders}${parsedResBody ? `\n\n${parsedResBody}` : ''}
|
||||||
`)
|
`)
|
||||||
|
|
||||||
return response
|
return axiosResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets verbose mode.
|
||||||
|
* @param verboseMode - value of the verbose mode, can be true, false or bleached(without extra colors).
|
||||||
|
*/
|
||||||
|
public setVerboseMode = (verboseMode: VerboseMode) => {
|
||||||
|
this.verboseMode = verboseMode
|
||||||
|
|
||||||
|
if (this.verboseMode) this.enableVerboseMode()
|
||||||
|
else this.disableVerboseMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,19 +1,11 @@
|
|||||||
import { RequestClient } from './RequestClient'
|
import { RequestClient } from './RequestClient'
|
||||||
import { AxiosResponse } from 'axios'
|
import { AxiosResponse } from 'axios'
|
||||||
|
import { SasjsParsedResponse } from '../types'
|
||||||
export interface SasjsParsedResponse<T> {
|
|
||||||
result: T
|
|
||||||
log: string
|
|
||||||
etag: string
|
|
||||||
status: number
|
|
||||||
printOutput?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Specific request client for SASJS.
|
* Specific request client for SASJS.
|
||||||
* Append tokens in headers.
|
* Append tokens in headers.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export class SasjsRequestClient extends RequestClient {
|
export class SasjsRequestClient extends RequestClient {
|
||||||
getHeaders = (accessToken: string | undefined, contentType: string) => {
|
getHeaders = (accessToken: string | undefined, contentType: string) => {
|
||||||
const headers: any = {}
|
const headers: any = {}
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import {
|
import { SASJS_LOGS_SEPARATOR, SasjsRequestClient } from '../SasjsRequestClient'
|
||||||
SASJS_LOGS_SEPARATOR,
|
import { SasjsParsedResponse } from '../../types'
|
||||||
SasjsRequestClient,
|
|
||||||
SasjsParsedResponse
|
|
||||||
} from '../SasjsRequestClient'
|
|
||||||
import { AxiosResponse } from 'axios'
|
import { AxiosResponse } from 'axios'
|
||||||
|
|
||||||
describe('SasjsRequestClient', () => {
|
describe('SasjsRequestClient', () => {
|
||||||
|
|||||||
@@ -5,16 +5,19 @@ import { app, mockedAuthResponse } from './SAS_server_app'
|
|||||||
import { ServerType } from '@sasjs/utils/types'
|
import { ServerType } from '@sasjs/utils/types'
|
||||||
import SASjs from '../SASjs'
|
import SASjs from '../SASjs'
|
||||||
import * as axiosModules from '../utils/createAxiosInstance'
|
import * as axiosModules from '../utils/createAxiosInstance'
|
||||||
|
import axios from 'axios'
|
||||||
import {
|
import {
|
||||||
LoginRequiredError,
|
LoginRequiredError,
|
||||||
AuthorizeError,
|
AuthorizeError,
|
||||||
NotFoundError,
|
NotFoundError,
|
||||||
InternalServerError
|
InternalServerError,
|
||||||
} from '../types/errors'
|
VerboseMode
|
||||||
|
} from '../types'
|
||||||
import { RequestClient } from '../request/RequestClient'
|
import { RequestClient } from '../request/RequestClient'
|
||||||
import { getTokenRequestErrorPrefixResponse } from '../auth/getTokenRequestErrorPrefix'
|
import { getTokenRequestErrorPrefixResponse } from '../auth/getTokenRequestErrorPrefix'
|
||||||
import { AxiosResponse } from 'axios'
|
import { AxiosResponse, AxiosError } from 'axios'
|
||||||
import { Logger, LogLevel } from '@sasjs/utils/logger'
|
import { Logger, LogLevel } from '@sasjs/utils/logger'
|
||||||
|
import * as UtilsModule from 'util'
|
||||||
|
|
||||||
const axiosActual = jest.requireActual('axios')
|
const axiosActual = jest.requireActual('axios')
|
||||||
|
|
||||||
@@ -73,88 +76,7 @@ describe('RequestClient', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('defaultInterceptionCallBack', () => {
|
describe('defaultInterceptionCallBack', () => {
|
||||||
beforeAll(() => {
|
const reqHeaders = `POST https://sas.server.com/compute/sessions/session_id/jobs HTTP/1.1
|
||||||
;(process as any).logger = new Logger(LogLevel.Off)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should log parsed response', () => {
|
|
||||||
jest.spyOn((process as any).logger, 'info')
|
|
||||||
|
|
||||||
const status = 200
|
|
||||||
const reqData = `{
|
|
||||||
name: 'test_job',
|
|
||||||
description: 'Powered by SASjs',
|
|
||||||
code: ['test_code'],
|
|
||||||
variables: {
|
|
||||||
SYS_JES_JOB_URI: '',
|
|
||||||
_program: '/Public/sasjs/jobs/jobs/test_job'
|
|
||||||
},
|
|
||||||
arguments: {
|
|
||||||
_contextName: 'SAS Job Execution compute context',
|
|
||||||
_OMITJSONLISTING: true,
|
|
||||||
_OMITJSONLOG: true,
|
|
||||||
_OMITSESSIONRESULTS: true,
|
|
||||||
_OMITTEXTLISTING: true,
|
|
||||||
_OMITTEXTLOG: true
|
|
||||||
}
|
|
||||||
}`
|
|
||||||
const resData = {
|
|
||||||
id: 'id_string',
|
|
||||||
name: 'name_string',
|
|
||||||
uri: 'uri_string',
|
|
||||||
createdBy: 'createdBy_string',
|
|
||||||
code: 'TEST CODE',
|
|
||||||
links: [
|
|
||||||
{
|
|
||||||
method: 'method_string',
|
|
||||||
rel: 'state',
|
|
||||||
href: 'state_href_string',
|
|
||||||
uri: 'uri_string',
|
|
||||||
type: 'type_string'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'method_string',
|
|
||||||
rel: 'state',
|
|
||||||
href: 'state_href_string',
|
|
||||||
uri: 'uri_string',
|
|
||||||
type: 'type_string'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'method_string',
|
|
||||||
rel: 'state',
|
|
||||||
href: 'state_href_string',
|
|
||||||
uri: 'uri_string',
|
|
||||||
type: 'type_string'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'method_string',
|
|
||||||
rel: 'state',
|
|
||||||
href: 'state_href_string',
|
|
||||||
uri: 'uri_string',
|
|
||||||
type: 'type_string'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'method_string',
|
|
||||||
rel: 'state',
|
|
||||||
href: 'state_href_string',
|
|
||||||
uri: 'uri_string',
|
|
||||||
type: 'type_string'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'method_string',
|
|
||||||
rel: 'self',
|
|
||||||
href: 'self_href_string',
|
|
||||||
uri: 'uri_string',
|
|
||||||
type: 'type_string'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
results: { '_webout.json': '_webout.json_string' },
|
|
||||||
logStatistics: {
|
|
||||||
lineCount: 1,
|
|
||||||
modifiedTimeStamp: 'modifiedTimeStamp_string'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const reqHeaders = `POST https://sas.server.com/compute/sessions/session_id/jobs HTTP/1.1
|
|
||||||
Accept: application/json
|
Accept: application/json
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
User-Agent: axios/0.27.2
|
User-Agent: axios/0.27.2
|
||||||
@@ -162,7 +84,126 @@ Content-Length: 334
|
|||||||
host: sas.server.io
|
host: sas.server.io
|
||||||
Connection: close
|
Connection: close
|
||||||
`
|
`
|
||||||
const resHeaders = ['content-type', 'application/json']
|
const reqData = `{
|
||||||
|
name: 'test_job',
|
||||||
|
description: 'Powered by SASjs',
|
||||||
|
code: ['test_code'],
|
||||||
|
variables: {
|
||||||
|
SYS_JES_JOB_URI: '',
|
||||||
|
_program: '/Public/sasjs/jobs/jobs/test_job'
|
||||||
|
},
|
||||||
|
arguments: {
|
||||||
|
_contextName: 'SAS Job Execution compute context',
|
||||||
|
_OMITJSONLISTING: true,
|
||||||
|
_OMITJSONLOG: true,
|
||||||
|
_OMITSESSIONRESULTS: true,
|
||||||
|
_OMITTEXTLISTING: true,
|
||||||
|
_OMITTEXTLOG: true
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
const resHeaders = ['content-type', 'application/json']
|
||||||
|
const resData = {
|
||||||
|
id: 'id_string',
|
||||||
|
name: 'name_string',
|
||||||
|
uri: 'uri_string',
|
||||||
|
createdBy: 'createdBy_string',
|
||||||
|
code: 'TEST CODE',
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
method: 'method_string',
|
||||||
|
rel: 'state',
|
||||||
|
href: 'state_href_string',
|
||||||
|
uri: 'uri_string',
|
||||||
|
type: 'type_string'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: 'method_string',
|
||||||
|
rel: 'state',
|
||||||
|
href: 'state_href_string',
|
||||||
|
uri: 'uri_string',
|
||||||
|
type: 'type_string'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: 'method_string',
|
||||||
|
rel: 'state',
|
||||||
|
href: 'state_href_string',
|
||||||
|
uri: 'uri_string',
|
||||||
|
type: 'type_string'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: 'method_string',
|
||||||
|
rel: 'state',
|
||||||
|
href: 'state_href_string',
|
||||||
|
uri: 'uri_string',
|
||||||
|
type: 'type_string'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: 'method_string',
|
||||||
|
rel: 'state',
|
||||||
|
href: 'state_href_string',
|
||||||
|
uri: 'uri_string',
|
||||||
|
type: 'type_string'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: 'method_string',
|
||||||
|
rel: 'self',
|
||||||
|
href: 'self_href_string',
|
||||||
|
uri: 'uri_string',
|
||||||
|
type: 'type_string'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
results: { '_webout.json': '_webout.json_string' },
|
||||||
|
logStatistics: {
|
||||||
|
lineCount: 1,
|
||||||
|
modifiedTimeStamp: 'modifiedTimeStamp_string'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
beforeAll(() => {
|
||||||
|
;(process as any).logger = new Logger(LogLevel.Off)
|
||||||
|
jest.spyOn((process as any).logger, 'info')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should log parsed response with status 1**', () => {
|
||||||
|
const spyIsAxiosError = jest
|
||||||
|
.spyOn(axios, 'isAxiosError')
|
||||||
|
.mockImplementation(() => true)
|
||||||
|
|
||||||
|
const mockedAxiosError = {
|
||||||
|
config: {
|
||||||
|
data: reqData
|
||||||
|
},
|
||||||
|
request: {
|
||||||
|
_currentRequest: {
|
||||||
|
_header: reqHeaders
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as AxiosError
|
||||||
|
|
||||||
|
const requestClient = new RequestClient('')
|
||||||
|
requestClient['defaultInterceptionCallBack'](mockedAxiosError)
|
||||||
|
|
||||||
|
const noValueMessage = 'Not provided'
|
||||||
|
const expectedLog = `HTTP Request (first 50 lines):
|
||||||
|
${reqHeaders}${requestClient['parseInterceptedBody'](reqData)}
|
||||||
|
|
||||||
|
HTTP Response Code: ${requestClient['prettifyString'](noValueMessage)}
|
||||||
|
|
||||||
|
HTTP Response (first 50 lines):
|
||||||
|
${noValueMessage}
|
||||||
|
\n${requestClient['parseInterceptedBody'](noValueMessage)}
|
||||||
|
`
|
||||||
|
|
||||||
|
expect((process as any).logger.info).toHaveBeenCalledWith(expectedLog)
|
||||||
|
|
||||||
|
spyIsAxiosError.mockReset()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should log parsed response with status 2**', () => {
|
||||||
|
const status = getRandomStatus([
|
||||||
|
200, 201, 202, 203, 204, 205, 206, 207, 208, 226
|
||||||
|
])
|
||||||
|
|
||||||
const mockedResponse: AxiosResponse = {
|
const mockedResponse: AxiosResponse = {
|
||||||
data: resData,
|
data: resData,
|
||||||
status,
|
status,
|
||||||
@@ -190,6 +231,138 @@ ${resHeaders[0]}: ${resHeaders[1]}${
|
|||||||
|
|
||||||
expect((process as any).logger.info).toHaveBeenCalledWith(expectedLog)
|
expect((process as any).logger.info).toHaveBeenCalledWith(expectedLog)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should log parsed response with status 3**', () => {
|
||||||
|
const status = getRandomStatus([300, 301, 302, 303, 304, 307, 308])
|
||||||
|
|
||||||
|
const mockedResponse: AxiosResponse = {
|
||||||
|
data: resData,
|
||||||
|
status,
|
||||||
|
statusText: '',
|
||||||
|
headers: {},
|
||||||
|
config: { data: reqData },
|
||||||
|
request: { _header: reqHeaders, res: { rawHeaders: resHeaders } }
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestClient = new RequestClient('')
|
||||||
|
requestClient['defaultInterceptionCallBack'](mockedResponse)
|
||||||
|
|
||||||
|
const expectedLog = `HTTP Request (first 50 lines):
|
||||||
|
${reqHeaders}${requestClient['parseInterceptedBody'](reqData)}
|
||||||
|
|
||||||
|
HTTP Response Code: ${requestClient['prettifyString'](status)}
|
||||||
|
|
||||||
|
HTTP Response (first 50 lines):
|
||||||
|
${resHeaders[0]}: ${resHeaders[1]}${
|
||||||
|
requestClient['parseInterceptedBody'](resData)
|
||||||
|
? `\n\n${requestClient['parseInterceptedBody'](resData)}`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
expect((process as any).logger.info).toHaveBeenCalledWith(expectedLog)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should log parsed response with status 4**', () => {
|
||||||
|
const spyIsAxiosError = jest
|
||||||
|
.spyOn(axios, 'isAxiosError')
|
||||||
|
.mockImplementation(() => true)
|
||||||
|
|
||||||
|
const status = getRandomStatus([
|
||||||
|
400, 401, 402, 403, 404, 407, 408, 409, 410, 411, 412, 413, 414, 415,
|
||||||
|
416, 417, 418, 421, 422, 423, 424, 425, 426, 428, 429, 431, 451
|
||||||
|
])
|
||||||
|
|
||||||
|
const mockedResponse: AxiosResponse = {
|
||||||
|
data: resData,
|
||||||
|
status,
|
||||||
|
statusText: '',
|
||||||
|
headers: {},
|
||||||
|
config: { data: reqData },
|
||||||
|
request: { _header: reqHeaders, res: { rawHeaders: resHeaders } }
|
||||||
|
}
|
||||||
|
const mockedAxiosError = {
|
||||||
|
config: {
|
||||||
|
data: reqData
|
||||||
|
},
|
||||||
|
request: {
|
||||||
|
_currentRequest: {
|
||||||
|
_header: reqHeaders
|
||||||
|
}
|
||||||
|
},
|
||||||
|
response: mockedResponse
|
||||||
|
} as AxiosError
|
||||||
|
|
||||||
|
const requestClient = new RequestClient('')
|
||||||
|
requestClient['defaultInterceptionCallBack'](mockedAxiosError)
|
||||||
|
|
||||||
|
const expectedLog = `HTTP Request (first 50 lines):
|
||||||
|
${reqHeaders}${requestClient['parseInterceptedBody'](reqData)}
|
||||||
|
|
||||||
|
HTTP Response Code: ${requestClient['prettifyString'](status)}
|
||||||
|
|
||||||
|
HTTP Response (first 50 lines):
|
||||||
|
${resHeaders[0]}: ${resHeaders[1]}${
|
||||||
|
requestClient['parseInterceptedBody'](resData)
|
||||||
|
? `\n\n${requestClient['parseInterceptedBody'](resData)}`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
expect((process as any).logger.info).toHaveBeenCalledWith(expectedLog)
|
||||||
|
|
||||||
|
spyIsAxiosError.mockReset()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should log parsed response with status 5**', () => {
|
||||||
|
const spyIsAxiosError = jest
|
||||||
|
.spyOn(axios, 'isAxiosError')
|
||||||
|
.mockImplementation(() => true)
|
||||||
|
|
||||||
|
const status = getRandomStatus([
|
||||||
|
500, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511
|
||||||
|
])
|
||||||
|
|
||||||
|
const mockedResponse: AxiosResponse = {
|
||||||
|
data: resData,
|
||||||
|
status,
|
||||||
|
statusText: '',
|
||||||
|
headers: {},
|
||||||
|
config: { data: reqData },
|
||||||
|
request: { _header: reqHeaders, res: { rawHeaders: resHeaders } }
|
||||||
|
}
|
||||||
|
const mockedAxiosError = {
|
||||||
|
config: {
|
||||||
|
data: reqData
|
||||||
|
},
|
||||||
|
request: {
|
||||||
|
_currentRequest: {
|
||||||
|
_header: reqHeaders
|
||||||
|
}
|
||||||
|
},
|
||||||
|
response: mockedResponse
|
||||||
|
} as AxiosError
|
||||||
|
|
||||||
|
const requestClient = new RequestClient('')
|
||||||
|
requestClient['defaultInterceptionCallBack'](mockedAxiosError)
|
||||||
|
|
||||||
|
const expectedLog = `HTTP Request (first 50 lines):
|
||||||
|
${reqHeaders}${requestClient['parseInterceptedBody'](reqData)}
|
||||||
|
|
||||||
|
HTTP Response Code: ${requestClient['prettifyString'](status)}
|
||||||
|
|
||||||
|
HTTP Response (first 50 lines):
|
||||||
|
${resHeaders[0]}: ${resHeaders[1]}${
|
||||||
|
requestClient['parseInterceptedBody'](resData)
|
||||||
|
? `\n\n${requestClient['parseInterceptedBody'](resData)}`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
expect((process as any).logger.info).toHaveBeenCalledWith(expectedLog)
|
||||||
|
|
||||||
|
spyIsAxiosError.mockReset()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('enableVerboseMode', () => {
|
describe('enableVerboseMode', () => {
|
||||||
@@ -215,12 +388,12 @@ ${resHeaders[0]}: ${resHeaders[1]}${
|
|||||||
'use'
|
'use'
|
||||||
)
|
)
|
||||||
|
|
||||||
const successCallback = (response: AxiosResponse) => {
|
const successCallback = (response: AxiosResponse | AxiosError) => {
|
||||||
console.log('success')
|
console.log('success')
|
||||||
|
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
const failureCallback = (response: AxiosResponse) => {
|
const failureCallback = (response: AxiosResponse | AxiosError) => {
|
||||||
console.log('failure')
|
console.log('failure')
|
||||||
|
|
||||||
return response
|
return response
|
||||||
@@ -235,6 +408,60 @@ ${resHeaders[0]}: ${resHeaders[1]}${
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('setVerboseMode', () => {
|
||||||
|
it(`should set verbose mode`, () => {
|
||||||
|
const requestClient = new RequestClient('')
|
||||||
|
let verbose: VerboseMode = false
|
||||||
|
requestClient.setVerboseMode(verbose)
|
||||||
|
|
||||||
|
expect(requestClient['verboseMode']).toEqual(verbose)
|
||||||
|
|
||||||
|
verbose = true
|
||||||
|
requestClient.setVerboseMode(verbose)
|
||||||
|
|
||||||
|
expect(requestClient['verboseMode']).toEqual(verbose)
|
||||||
|
|
||||||
|
verbose = 'bleached'
|
||||||
|
requestClient.setVerboseMode(verbose)
|
||||||
|
|
||||||
|
expect(requestClient['verboseMode']).toEqual(verbose)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('prettifyString', () => {
|
||||||
|
it(`should call inspect without colors when verbose mode is set to 'bleached'`, () => {
|
||||||
|
const requestClient = new RequestClient('')
|
||||||
|
let verbose: VerboseMode = 'bleached'
|
||||||
|
requestClient.setVerboseMode(verbose)
|
||||||
|
|
||||||
|
jest.spyOn(UtilsModule, 'inspect')
|
||||||
|
|
||||||
|
const testStr = JSON.stringify({ test: 'test' })
|
||||||
|
|
||||||
|
requestClient['prettifyString'](testStr)
|
||||||
|
|
||||||
|
expect(UtilsModule.inspect).toHaveBeenCalledWith(testStr, {
|
||||||
|
colors: false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it(`should call inspect with colors when verbose mode is set to 'true'`, () => {
|
||||||
|
const requestClient = new RequestClient('')
|
||||||
|
let verbose: VerboseMode = true
|
||||||
|
requestClient.setVerboseMode(verbose)
|
||||||
|
|
||||||
|
jest.spyOn(UtilsModule, 'inspect')
|
||||||
|
|
||||||
|
const testStr = JSON.stringify({ test: 'test' })
|
||||||
|
|
||||||
|
requestClient['prettifyString'](testStr)
|
||||||
|
|
||||||
|
expect(UtilsModule.inspect).toHaveBeenCalledWith(testStr, {
|
||||||
|
colors: true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('disableVerboseMode', () => {
|
describe('disableVerboseMode', () => {
|
||||||
it('should eject interceptor', () => {
|
it('should eject interceptor', () => {
|
||||||
const requestClient = new RequestClient('')
|
const requestClient = new RequestClient('')
|
||||||
@@ -466,3 +693,11 @@ const createCertificate = async (): Promise<pem.CertificateCreationResult> => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a random status code.
|
||||||
|
* @param statuses - an array of available statuses.
|
||||||
|
* @returns - random item from an array of statuses.
|
||||||
|
*/
|
||||||
|
const getRandomStatus = (statuses: number[]) =>
|
||||||
|
statuses[Math.floor(Math.random() * statuses.length)]
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { RequestClient } from '../request/RequestClient'
|
|||||||
import * as dotenv from 'dotenv'
|
import * as dotenv from 'dotenv'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { Logger, LogLevel } from '@sasjs/utils/logger'
|
import { Logger, LogLevel } from '@sasjs/utils/logger'
|
||||||
import { Session, Context } from '../types'
|
import { Session, SessionState, Context } from '../types'
|
||||||
|
|
||||||
jest.mock('axios')
|
jest.mock('axios')
|
||||||
const mockedAxios = axios as jest.Mocked<typeof axios>
|
const mockedAxios = axios as jest.Mocked<typeof axios>
|
||||||
@@ -11,21 +11,34 @@ const requestClient = new (<jest.Mock<RequestClient>>RequestClient)()
|
|||||||
|
|
||||||
describe('SessionManager', () => {
|
describe('SessionManager', () => {
|
||||||
dotenv.config()
|
dotenv.config()
|
||||||
|
process.env.SERVER_URL = 'https://server.com'
|
||||||
|
|
||||||
const sessionManager = new SessionManager(
|
const sessionManager = new SessionManager(
|
||||||
process.env.SERVER_URL as string,
|
process.env.SERVER_URL as string,
|
||||||
process.env.DEFAULT_COMPUTE_CONTEXT as string,
|
process.env.DEFAULT_COMPUTE_CONTEXT as string,
|
||||||
requestClient
|
requestClient
|
||||||
)
|
)
|
||||||
|
const sessionStateLink = '/compute/sessions/session-id-ses0000/state'
|
||||||
|
const sessionEtag = 'etag-string'
|
||||||
|
|
||||||
const getMockSession = () => ({
|
const getMockSession = (): Session => ({
|
||||||
id: ['id', new Date().getTime(), Math.random()].join('-'),
|
id: ['id', new Date().getTime(), Math.random()].join('-'),
|
||||||
state: '',
|
state: SessionState.NoState,
|
||||||
links: [{ rel: 'state', href: '', uri: '', type: '', method: 'GET' }],
|
links: [
|
||||||
|
{
|
||||||
|
href: sessionStateLink,
|
||||||
|
method: 'GET',
|
||||||
|
rel: 'state',
|
||||||
|
type: 'text/plain',
|
||||||
|
uri: sessionStateLink
|
||||||
|
}
|
||||||
|
],
|
||||||
attributes: {
|
attributes: {
|
||||||
sessionInactiveTimeout: 900
|
sessionInactiveTimeout: 900
|
||||||
},
|
},
|
||||||
creationTimeStamp: `${new Date(new Date().getTime()).toISOString()}`
|
creationTimeStamp: `${new Date(new Date().getTime()).toISOString()}`,
|
||||||
|
stateUrl: sessionStateLink,
|
||||||
|
etag: sessionEtag
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -89,19 +102,21 @@ describe('SessionManager', () => {
|
|||||||
describe('waitForSession', () => {
|
describe('waitForSession', () => {
|
||||||
const session: Session = {
|
const session: Session = {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
state: '',
|
state: SessionState.NoState,
|
||||||
links: [{ rel: 'state', href: '', uri: '', type: '', method: 'GET' }],
|
links: [{ rel: 'state', href: '', uri: '', type: '', method: 'GET' }],
|
||||||
attributes: {
|
attributes: {
|
||||||
sessionInactiveTimeout: 0
|
sessionInactiveTimeout: 0
|
||||||
},
|
},
|
||||||
creationTimeStamp: ''
|
creationTimeStamp: '',
|
||||||
|
stateUrl: sessionStateLink,
|
||||||
|
etag: sessionEtag
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
;(process as any).logger = new Logger(LogLevel.Off)
|
;(process as any).logger = new Logger(LogLevel.Off)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should reject with NoSessionStateError if SAS server did not provide session state', async () => {
|
it('should log http response code and session state if SAS server did not provide session state', async () => {
|
||||||
let requestAttempt = 0
|
let requestAttempt = 0
|
||||||
const requestAttemptLimit = 10
|
const requestAttemptLimit = 10
|
||||||
const sessionState = 'idle'
|
const sessionState = 'idle'
|
||||||
@@ -124,15 +139,17 @@ describe('SessionManager', () => {
|
|||||||
sessionManager['waitForSession'](session, null, 'access_token')
|
sessionManager['waitForSession'](session, null, 'access_token')
|
||||||
).resolves.toEqual(sessionState)
|
).resolves.toEqual(sessionState)
|
||||||
|
|
||||||
|
const sessionStateUrl = process.env.SERVER_URL + session.stateUrl
|
||||||
|
|
||||||
expect(mockedAxios.get).toHaveBeenCalledTimes(requestAttemptLimit)
|
expect(mockedAxios.get).toHaveBeenCalledTimes(requestAttemptLimit)
|
||||||
expect((process as any).logger.info).toHaveBeenCalledTimes(3)
|
expect((process as any).logger.info).toHaveBeenCalledTimes(3)
|
||||||
expect((process as any).logger.info).toHaveBeenNthCalledWith(
|
expect((process as any).logger.info).toHaveBeenNthCalledWith(
|
||||||
1,
|
1,
|
||||||
`Polling: ${process.env.SERVER_URL}`
|
`Polling: ${sessionStateUrl}`
|
||||||
)
|
)
|
||||||
expect((process as any).logger.info).toHaveBeenNthCalledWith(
|
expect((process as any).logger.info).toHaveBeenNthCalledWith(
|
||||||
2,
|
2,
|
||||||
`Could not get session state. Server responded with 304 whilst checking state: ${process.env.SERVER_URL}`
|
`Could not get session state. Server responded with 304 whilst checking state: ${sessionStateUrl}`
|
||||||
)
|
)
|
||||||
expect((process as any).logger.info).toHaveBeenNthCalledWith(
|
expect((process as any).logger.info).toHaveBeenNthCalledWith(
|
||||||
3,
|
3,
|
||||||
@@ -142,7 +159,7 @@ describe('SessionManager', () => {
|
|||||||
|
|
||||||
it('should throw an error if there is no session link', async () => {
|
it('should throw an error if there is no session link', async () => {
|
||||||
const customSession = JSON.parse(JSON.stringify(session))
|
const customSession = JSON.parse(JSON.stringify(session))
|
||||||
customSession.links = []
|
customSession.stateUrl = ''
|
||||||
|
|
||||||
mockedAxios.get.mockImplementation(() =>
|
mockedAxios.get.mockImplementation(() =>
|
||||||
Promise.resolve({ data: customSession.state, status: 200 })
|
Promise.resolve({ data: customSession.state, status: 200 })
|
||||||
@@ -156,6 +173,7 @@ describe('SessionManager', () => {
|
|||||||
it('should throw an error if could not get session state', async () => {
|
it('should throw an error if could not get session state', async () => {
|
||||||
const gettingSessionStatus = 500
|
const gettingSessionStatus = 500
|
||||||
const sessionStatusError = `Getting session status timed out after 60 seconds. Request failed with status code ${gettingSessionStatus}`
|
const sessionStatusError = `Getting session status timed out after 60 seconds. Request failed with status code ${gettingSessionStatus}`
|
||||||
|
const sessionStateUrl = process.env.SERVER_URL + session.stateUrl
|
||||||
|
|
||||||
mockedAxios.get.mockImplementation(() =>
|
mockedAxios.get.mockImplementation(() =>
|
||||||
Promise.reject({
|
Promise.reject({
|
||||||
@@ -168,7 +186,7 @@ describe('SessionManager', () => {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
const expectedError = `Error while waiting for session. Error while getting session state. GET request to ${process.env.SERVER_URL}?wait=30 failed with status code ${gettingSessionStatus}. ${sessionStatusError}`
|
const expectedError = `Error while waiting for session. Error while getting session state. GET request to ${sessionStateUrl}?wait=30 failed with status code ${gettingSessionStatus}. ${sessionStatusError}`
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
sessionManager['waitForSession'](session, null, 'access_token')
|
sessionManager['waitForSession'](session, null, 'access_token')
|
||||||
@@ -427,4 +445,45 @@ describe('SessionManager', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('createAndWaitForSession', () => {
|
||||||
|
it('should create session with etag and stateUrl', async () => {
|
||||||
|
const etag = sessionEtag
|
||||||
|
const customSession: any = getMockSession()
|
||||||
|
delete customSession.etag
|
||||||
|
delete customSession.stateUrl
|
||||||
|
|
||||||
|
jest.spyOn(requestClient, 'post').mockImplementation(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
result: customSession,
|
||||||
|
etag
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(sessionManager as any, 'setCurrentContext')
|
||||||
|
.mockImplementation(() => Promise.resolve())
|
||||||
|
|
||||||
|
sessionManager['currentContext'] = {
|
||||||
|
name: 'context name',
|
||||||
|
id: 'string',
|
||||||
|
createdBy: 'string',
|
||||||
|
version: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(sessionManager as any, 'getSessionState')
|
||||||
|
.mockImplementation(() =>
|
||||||
|
Promise.resolve({ result: SessionState.Idle, responseStatus: 200 })
|
||||||
|
)
|
||||||
|
|
||||||
|
const expectedSession = await sessionManager['createAndWaitForSession']()
|
||||||
|
|
||||||
|
expect(customSession.id).toEqual(expectedSession.id)
|
||||||
|
expect(
|
||||||
|
customSession.links.find((l: any) => l.rel === 'state').href
|
||||||
|
).toEqual(expectedSession.stateUrl)
|
||||||
|
expect(expectedSession.etag).toEqual(etag)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
55
src/types/RequestClient.ts
Normal file
55
src/types/RequestClient.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { CsrfToken } from '..'
|
||||||
|
|
||||||
|
export interface HttpClient {
|
||||||
|
get<T>(
|
||||||
|
url: string,
|
||||||
|
accessToken: string | undefined,
|
||||||
|
contentType: string,
|
||||||
|
overrideHeaders: { [key: string]: string | number }
|
||||||
|
): Promise<{ result: T; etag: string }>
|
||||||
|
|
||||||
|
post<T>(
|
||||||
|
url: string,
|
||||||
|
data: any,
|
||||||
|
accessToken: string | undefined,
|
||||||
|
contentType: string,
|
||||||
|
overrideHeaders: { [key: string]: string | number }
|
||||||
|
): Promise<{ result: T; etag: string }>
|
||||||
|
|
||||||
|
put<T>(
|
||||||
|
url: string,
|
||||||
|
data: any,
|
||||||
|
accessToken: string | undefined,
|
||||||
|
overrideHeaders: { [key: string]: string | number }
|
||||||
|
): Promise<{ result: T; etag: string }>
|
||||||
|
|
||||||
|
delete<T>(
|
||||||
|
url: string,
|
||||||
|
accessToken: string | undefined
|
||||||
|
): Promise<{ result: T; etag: string }>
|
||||||
|
|
||||||
|
getCsrfToken(type: 'general' | 'file'): CsrfToken | undefined
|
||||||
|
saveLocalStorageToken(accessToken: string, refreshToken: string): void
|
||||||
|
clearCsrfTokens(): void
|
||||||
|
clearLocalStorageTokens(): void
|
||||||
|
getBaseUrl(): string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SASjsRequest {
|
||||||
|
serviceLink: string
|
||||||
|
timestamp: Date
|
||||||
|
sourceCode: string
|
||||||
|
generatedCode: string
|
||||||
|
logFile: string
|
||||||
|
SASWORK: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SasjsParsedResponse<T> {
|
||||||
|
result: T
|
||||||
|
log: string
|
||||||
|
etag: string
|
||||||
|
status: number
|
||||||
|
printOutput?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type VerboseMode = boolean | 'bleached'
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as https from 'https'
|
import * as https from 'https'
|
||||||
import { ServerType } from '@sasjs/utils/types'
|
import { ServerType } from '@sasjs/utils/types'
|
||||||
|
import { VerboseMode } from '../types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Specifies the configuration for the SASjs instance - eg where and how to
|
* Specifies the configuration for the SASjs instance - eg where and how to
|
||||||
@@ -45,6 +46,10 @@ export class SASjsConfig {
|
|||||||
* Set to `true` to enable additional debugging.
|
* Set to `true` to enable additional debugging.
|
||||||
*/
|
*/
|
||||||
debug: boolean = true
|
debug: boolean = true
|
||||||
|
/**
|
||||||
|
* Set to `true` to enable verbose mode that will log a summary of every HTTP response.
|
||||||
|
*/
|
||||||
|
verbose?: VerboseMode = true
|
||||||
/**
|
/**
|
||||||
* The name of the compute context to use when calling the Viya services directly.
|
* The name of the compute context to use when calling the Viya services directly.
|
||||||
* Example value: 'SAS Job Execution compute context'
|
* Example value: 'SAS Job Execution compute context'
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
/**
|
|
||||||
* Represents a SASjs request, its response and logs.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export interface SASjsRequest {
|
|
||||||
serviceLink: string
|
|
||||||
timestamp: Date
|
|
||||||
sourceCode: string
|
|
||||||
generatedCode: string
|
|
||||||
logFile: string
|
|
||||||
SASWORK: any
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,34 @@
|
|||||||
import { Link } from './Link'
|
import { Link } from './Link'
|
||||||
|
import { SessionManager } from '../SessionManager'
|
||||||
|
|
||||||
|
export enum SessionState {
|
||||||
|
Completed = 'completed',
|
||||||
|
Running = 'running',
|
||||||
|
Pending = 'pending',
|
||||||
|
Idle = 'idle',
|
||||||
|
Unavailable = 'unavailable',
|
||||||
|
NoState = '',
|
||||||
|
Failed = 'failed',
|
||||||
|
Error = 'error'
|
||||||
|
}
|
||||||
|
|
||||||
export interface Session {
|
export interface Session {
|
||||||
id: string
|
id: string
|
||||||
state: string
|
state: SessionState
|
||||||
|
stateUrl: string
|
||||||
links: Link[]
|
links: Link[]
|
||||||
attributes: {
|
attributes: {
|
||||||
sessionInactiveTimeout: number
|
sessionInactiveTimeout: number
|
||||||
}
|
}
|
||||||
creationTimeStamp: string
|
creationTimeStamp: string
|
||||||
|
etag: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SessionVariable {
|
export interface SessionVariable {
|
||||||
value: string
|
value: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface JobSessionManager {
|
||||||
|
session: Session
|
||||||
|
sessionManager: SessionManager
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ export * from './Job'
|
|||||||
export * from './JobDefinition'
|
export * from './JobDefinition'
|
||||||
export * from './JobResult'
|
export * from './JobResult'
|
||||||
export * from './Link'
|
export * from './Link'
|
||||||
|
export * from './Login'
|
||||||
export * from './SASjsConfig'
|
export * from './SASjsConfig'
|
||||||
export * from './SASjsRequest'
|
export * from './RequestClient'
|
||||||
export * from './Session'
|
export * from './Session'
|
||||||
export * from './UploadFile'
|
export * from './UploadFile'
|
||||||
export * from './PollOptions'
|
export * from './PollOptions'
|
||||||
export * from './WriteStream'
|
export * from './WriteStream'
|
||||||
export * from './ExecuteScript'
|
export * from './ExecuteScript'
|
||||||
|
export * from './errors'
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { SASjsRequest } from '../types/SASjsRequest'
|
import { SASjsRequest } from '../types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Comparator for SASjs request timestamps.
|
* Comparator for SASjs request timestamps.
|
||||||
|
|||||||
Reference in New Issue
Block a user