1
0
mirror of https://github.com/sasjs/adapter.git synced 2026-01-04 19:20:05 +00:00

Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
65ae198c27 chore(deps-dev): bump word-wrap from 1.2.3 to 1.2.4
Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.4.
- [Release notes](https://github.com/jonschlinkert/word-wrap/releases)
- [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.4)

---
updated-dependencies:
- dependency-name: word-wrap
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-19 01:03:33 +00:00
27 changed files with 168 additions and 1079 deletions

View File

@@ -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.
- [ ] Unit tests coverage has been increased and a new threshold is set.
- [ ] 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)).
- [ ] [Data Controller](https://datacontroller.io) builds and is functional on both SAS 9 and Viya

View File

@@ -10,4 +10,4 @@ jobs:
- uses: actions/checkout@v2
- uses: uesteibar/reviewer-lottery@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
repo-token: ${{ secrets.GH_TOKEN }}

View File

@@ -80,10 +80,6 @@ jobs:
npm run update:adapter
pm2 start --name sasjs-test npm -- start
- name: Sleep for 10 seconds
run: sleep 10s
shell: bash
- name: Run cypress on sasjs
run: |
replace-in-files --regex='"sasjsTestsUrl".*' --replacement='"sasjsTestsUrl":"http://localhost:3000",' ./cypress.json

View File

@@ -37,7 +37,7 @@ jobs:
- name: Push generated docs
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
github_token: ${{ secrets.GH_TOKEN }}
publish_branch: gh-pages
publish_dir: ./docs
cname: adapter.sasjs.io

View File

@@ -36,7 +36,7 @@ jobs:
- name: Semantic Release
uses: cycjimmy/semantic-release-action@v3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Send Matrix message

View File

@@ -153,10 +153,6 @@ The response object will contain returned tables and columns. Table names are a
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
To execute a script on Viya a session has to be created first which is time-consuming (~15sec). That is why a Session Manager has been created which is implementing the following logic:
@@ -277,7 +273,6 @@ 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).
* `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.
* `verbose` - optional, if `true` then a summary of every HTTP response is logged.
* `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.
* `contextName` - Compute context on which the requests will be called. If missing or not provided, defaults to `Job Execution Compute context`.

View File

@@ -41,14 +41,7 @@ module.exports = {
// ],
// An object that configures minimum threshold enforcement for coverage results
coverageThreshold: {
global: {
statements: 63.61,
branches: 44.72,
functions: 53.94,
lines: 64.07
}
},
// coverageThreshold: undefined,
// A path to a custom dependency extractor
// dependencyExtractor: undefined,

12
package-lock.json generated
View File

@@ -16800,9 +16800,9 @@
"dev": true
},
"node_modules/word-wrap": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz",
"integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==",
"dev": true,
"engines": {
"node": ">=0.10.0"
@@ -29626,9 +29626,9 @@
"dev": true
},
"word-wrap": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz",
"integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==",
"dev": true
},
"wordwrap": {

View File

@@ -25,7 +25,7 @@ import { prefixMessage } from '@sasjs/utils/error'
import { pollJobState } from './api/viya/pollJobState'
import { getTokens } from './auth/getTokens'
import { uploadTables } from './api/viya/uploadTables'
import { executeOnComputeApi } from './api/viya/executeOnComputeApi'
import { executeScript } from './api/viya/executeScript'
import { getAccessTokenForViya } from './auth/getAccessTokenForViya'
import { refreshTokensForViya } from './auth/refreshTokensForViya'
@@ -293,7 +293,7 @@ export class SASViyaApiClient {
printPid = false,
variables?: MacroVar
): Promise<any> {
return executeOnComputeApi(
return executeScript(
this.requestClient,
this.sessionManager,
this.rootFolderName,

View File

@@ -4,12 +4,7 @@ import {
UploadFile,
EditContextInput,
PollOptions,
LoginMechanism,
VerboseMode,
ErrorResponse,
LoginOptions,
LoginResult,
ExecutionQuery
LoginMechanism
} from './types'
import { SASViyaApiClient } from './SASViyaApiClient'
import { SAS9ApiClient } from './SAS9ApiClient'
@@ -34,7 +29,8 @@ import {
Sas9JobExecutor,
FileUploader
} from './job-execution'
import { AxiosResponse, AxiosError } from 'axios'
import { ErrorResponse } from './types/errors'
import { LoginOptions, LoginResult } from './types/Login'
interface ExecuteScriptParams {
linesOfCode: string[]
@@ -161,23 +157,6 @@ 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.
* @param accessToken - an access token for an authorised user.
@@ -875,7 +854,6 @@ 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 printPid - a boolean that indicates whether the function should print (PID) of the started job.
* @param variables - an object that represents macro variables.
* @param verboseMode - boolean or a string equal to 'bleached' to enable verbose mode (log every HTTP response).
*/
public async startComputeJob(
sasJob: string,
@@ -885,8 +863,7 @@ export default class SASjs {
waitForResult?: boolean,
pollOptions?: PollOptions,
printPid = false,
variables?: MacroVar,
verboseMode?: VerboseMode
variables?: MacroVar
) {
config = {
...this.sasjsConfig,
@@ -900,11 +877,6 @@ export default class SASjs {
)
}
if (verboseMode) {
this.requestClient?.setVerboseMode(verboseMode)
this.requestClient?.enableVerboseMode()
} else if (verboseMode === false) this.requestClient?.disableVerboseMode()
return this.sasViyaApiClient?.executeComputeJob(
sasJob,
config.contextName,
@@ -998,8 +970,7 @@ export default class SASjs {
this.requestClient = new RequestClientClass(
this.sasjsConfig.serverUrl,
this.sasjsConfig.httpsAgentOptions,
this.sasjsConfig.requestHistoryLimit,
this.sasjsConfig.verbose
this.sasjsConfig.requestHistoryLimit
)
} else {
this.requestClient.setConfig(
@@ -1163,31 +1134,4 @@ 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)
}
}

View File

@@ -15,12 +15,8 @@ import { formatDataForRequest } from '../../utils/formatDataForRequest'
import { pollJobState, JobState } from './pollJobState'
import { uploadTables } from './uploadTables'
interface JobRequestBody {
[key: string]: number | string | string[]
}
/**
* Executes SAS program on the current SAS Viya server using Compute API.
* Executes code on the current SAS Viya server.
* @param jobPath - the path to the file being submitted for execution.
* @param linesOfCode - an array of code lines to execute.
* @param contextName - the context to execute the code in.
@@ -33,7 +29,7 @@ interface JobRequestBody {
* @param printPid - a boolean that indicates whether the function should print (PID) of the started job.
* @param variables - an object that represents macro variables.
*/
export async function executeOnComputeApi(
export async function executeScript(
requestClient: RequestClient,
sessionManager: SessionManager,
rootFolderName: string,
@@ -50,7 +46,6 @@ export async function executeOnComputeApi(
variables?: MacroVar
): Promise<any> {
let access_token = (authConfig || {}).access_token
if (authConfig) {
;({ access_token } = await getTokens(requestClient, authConfig))
}
@@ -83,13 +78,27 @@ export async function executeOnComputeApi(
const logger = process.logger || console
logger.info(
`Triggering '${relativeJobPath}' with PID ${
`Triggered '${relativeJobPath}' with PID ${
jobIdVariable.value
} 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
if (isRelativePath(jobPath)) {
@@ -98,7 +107,6 @@ export async function executeOnComputeApi(
}`
} else {
const jobPathParts = jobPath.split('/')
fileName = jobPathParts.pop()
}
@@ -110,6 +118,7 @@ export async function executeOnComputeApi(
}
if (variables) jobVariables = { ...jobVariables, ...variables }
if (debug) jobVariables = { ...jobVariables, _DEBUG: 131 }
let files: any[] = []
@@ -136,12 +145,12 @@ export async function executeOnComputeApi(
}
// Execute job in session
const jobRequestBody: JobRequestBody = {
name: fileName || 'Default Job Name',
const jobRequestBody = {
name: fileName,
description: 'Powered by SASjs',
code: linesOfCode,
variables: jobVariables,
version: 2
arguments: jobArguments
}
const { result: postedJob, etag } = await requestClient
@@ -270,7 +279,7 @@ export async function executeOnComputeApi(
const error = e as HttpError
if (error.status === 404) {
return executeOnComputeApi(
return executeScript(
requestClient,
sessionManager,
rootFolderName,

View File

@@ -1,6 +1,6 @@
import { RequestClient } from '../../../request/RequestClient'
import { SessionManager } from '../../../SessionManager'
import { executeOnComputeApi } from '../executeOnComputeApi'
import { executeScript } from '../executeScript'
import { mockSession, mockAuthConfig, mockJob } from './mockResponses'
import * as pollJobStateModule from '../pollJobState'
import * as uploadTablesModule from '../uploadTables'
@@ -25,7 +25,7 @@ describe('executeScript', () => {
})
it('should not try to get fresh tokens if an authConfig is not provided', async () => {
await executeOnComputeApi(
await executeScript(
requestClient,
sessionManager,
'test',
@@ -38,7 +38,7 @@ describe('executeScript', () => {
})
it('should try to get fresh tokens if an authConfig is provided', async () => {
await executeOnComputeApi(
await executeScript(
requestClient,
sessionManager,
'test',
@@ -55,7 +55,7 @@ describe('executeScript', () => {
})
it('should get a session from the session manager before executing', async () => {
await executeOnComputeApi(
await executeScript(
requestClient,
sessionManager,
'test',
@@ -72,7 +72,7 @@ describe('executeScript', () => {
.spyOn(sessionManager, 'getSession')
.mockImplementation(() => Promise.reject('Test Error'))
const error = await executeOnComputeApi(
const error = await executeScript(
requestClient,
sessionManager,
'test',
@@ -85,7 +85,7 @@ describe('executeScript', () => {
})
it('should fetch the PID when printPid is true', async () => {
await executeOnComputeApi(
await executeScript(
requestClient,
sessionManager,
'test',
@@ -113,7 +113,7 @@ describe('executeScript', () => {
.spyOn(sessionManager, 'getVariable')
.mockImplementation(() => Promise.reject('Test Error'))
const error = await executeOnComputeApi(
const error = await executeScript(
requestClient,
sessionManager,
'test',
@@ -139,7 +139,7 @@ describe('executeScript', () => {
Promise.resolve([{ tableName: 'test', file: { id: 1 } }])
)
await executeOnComputeApi(
await executeScript(
requestClient,
sessionManager,
'test',
@@ -163,7 +163,7 @@ describe('executeScript', () => {
})
it('should format data as CSV when it does not contain semicolons', async () => {
await executeOnComputeApi(
await executeScript(
requestClient,
sessionManager,
'test',
@@ -189,7 +189,7 @@ describe('executeScript', () => {
.spyOn(formatDataModule, 'formatDataForRequest')
.mockImplementation(() => ({ sasjs_tables: 'foo', sasjs0data: 'bar' }))
await executeOnComputeApi(
await executeScript(
requestClient,
sessionManager,
'test',
@@ -217,7 +217,14 @@ describe('executeScript', () => {
sasjs_tables: 'foo',
sasjs0data: 'bar'
},
version: 2
arguments: {
_contextName: 'test context',
_OMITJSONLISTING: true,
_OMITJSONLOG: true,
_OMITSESSIONRESULTS: true,
_OMITTEXTLISTING: true,
_OMITTEXTLOG: true
}
},
mockAuthConfig.access_token
)
@@ -228,7 +235,7 @@ describe('executeScript', () => {
.spyOn(formatDataModule, 'formatDataForRequest')
.mockImplementation(() => ({ sasjs_tables: 'foo', sasjs0data: 'bar' }))
await executeOnComputeApi(
await executeScript(
requestClient,
sessionManager,
'test',
@@ -257,7 +264,14 @@ describe('executeScript', () => {
sasjs0data: 'bar',
_DEBUG: 131
},
version: 2
arguments: {
_contextName: 'test context',
_OMITJSONLISTING: true,
_OMITJSONLOG: true,
_OMITSESSIONRESULTS: false,
_OMITTEXTLISTING: true,
_OMITTEXTLOG: false
}
},
mockAuthConfig.access_token
)
@@ -268,7 +282,7 @@ describe('executeScript', () => {
.spyOn(requestClient, 'post')
.mockImplementation(() => Promise.reject('Test Error'))
const error = await executeOnComputeApi(
const error = await executeScript(
requestClient,
sessionManager,
'test',
@@ -288,7 +302,7 @@ describe('executeScript', () => {
})
it('should immediately return the session when waitForResult is false', async () => {
const result = await executeOnComputeApi(
const result = await executeScript(
requestClient,
sessionManager,
'test',
@@ -308,7 +322,7 @@ describe('executeScript', () => {
})
it('should poll for job completion when waitForResult is true', async () => {
await executeOnComputeApi(
await executeScript(
requestClient,
sessionManager,
'test',
@@ -338,7 +352,7 @@ describe('executeScript', () => {
.spyOn(pollJobStateModule, 'pollJobState')
.mockImplementation(() => Promise.reject('Poll Error'))
const error = await executeOnComputeApi(
const error = await executeScript(
requestClient,
sessionManager,
'test',
@@ -364,7 +378,7 @@ describe('executeScript', () => {
Promise.reject({ response: { data: 'err=5113,' } })
)
const error = await executeOnComputeApi(
const error = await executeScript(
requestClient,
sessionManager,
'test',
@@ -390,7 +404,7 @@ describe('executeScript', () => {
})
it('should fetch the logs for the job if debug is true and a log URL is available', async () => {
await executeOnComputeApi(
await executeScript(
requestClient,
sessionManager,
'test',
@@ -415,7 +429,7 @@ describe('executeScript', () => {
})
it('should not fetch the logs for the job if debug is false', async () => {
await executeOnComputeApi(
await executeScript(
requestClient,
sessionManager,
'test',
@@ -441,7 +455,7 @@ describe('executeScript', () => {
Promise.resolve(pollJobStateModule.JobState.Failed)
)
const error: ComputeJobExecutionError = await executeOnComputeApi(
const error: ComputeJobExecutionError = await executeScript(
requestClient,
sessionManager,
'test',
@@ -476,7 +490,7 @@ describe('executeScript', () => {
Promise.resolve(pollJobStateModule.JobState.Error)
)
const error: ComputeJobExecutionError = await executeOnComputeApi(
const error: ComputeJobExecutionError = await executeScript(
requestClient,
sessionManager,
'test',
@@ -505,7 +519,7 @@ describe('executeScript', () => {
})
it('should fetch the result if expectWebout is true', async () => {
await executeOnComputeApi(
await executeScript(
requestClient,
sessionManager,
'test',
@@ -536,7 +550,7 @@ describe('executeScript', () => {
return Promise.resolve({ result: mockJob, etag: '', status: 200 })
})
const error = await executeOnComputeApi(
const error = await executeScript(
requestClient,
sessionManager,
'test',
@@ -570,7 +584,7 @@ describe('executeScript', () => {
})
it('should clear the session after execution is complete', async () => {
await executeOnComputeApi(
await executeScript(
requestClient,
sessionManager,
'test',
@@ -597,7 +611,7 @@ describe('executeScript', () => {
.spyOn(sessionManager, 'clearSession')
.mockImplementation(() => Promise.reject('Clear Session Error'))
const error = await executeOnComputeApi(
const error = await executeScript(
requestClient,
sessionManager,
'test',

View File

@@ -276,76 +276,6 @@ describe('pollJobState', () => {
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 () => {
// INFO: 'maxPollCount' has to be > 0
let invalidPollStrategy = {

View File

@@ -1,7 +1,8 @@
import {
getValidJson,
parseSasViyaDebugResponse,
parseWeboutResponse
parseWeboutResponse,
SASJS_LOGS_SEPARATOR
} from '../utils'
import { UploadFile } from '../types/UploadFile'
import {

View File

@@ -233,8 +233,7 @@ export default class SASjs {
this.requestClient = new RequestClient(
this.sasjsConfig.serverUrl,
this.sasjsConfig.httpsAgentOptions,
this.sasjsConfig.requestHistoryLimit,
this.sasjsConfig.verbose
this.sasjsConfig.requestHistoryLimit
)
} else {
this.requestClient.setConfig(

View File

@@ -11,6 +11,7 @@ import {
import { RequestClient } from '../../request/RequestClient'
import {
isRelativePath,
parseSasViyaDebugResponse,
appendExtraResponseAttributes,
convertToCSV
} from '../../utils'

View File

@@ -1,10 +1,4 @@
import {
AxiosError,
AxiosInstance,
AxiosRequestConfig,
AxiosResponse
} from 'axios'
import axios from 'axios'
import { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import * as https from 'https'
import { CsrfToken } from '..'
import { isAuthorizeFormRequired, isLogInRequired } from '../auth'
@@ -16,7 +10,7 @@ import {
JobExecutionError,
CertificateError
} from '../types/errors'
import { SASjsRequest, HttpClient, VerboseMode } from '../types'
import { SASjsRequest } from '../types'
import { parseWeboutResponse } from '../utils/parseWeboutResponse'
import { prefixMessage } from '@sasjs/utils/error'
import { SAS9AuthError } from '../types/errors/SAS9AuthError'
@@ -26,13 +20,45 @@ import {
createAxiosInstance
} from '../utils'
import { InvalidSASjsCsrfError } from '../types/errors/InvalidSASjsCsrfError'
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 {
private requests: SASjsRequest[] = []
private requestsLimit: number = 10
private httpInterceptor?: number
private verboseMode: VerboseMode = false
protected csrfToken: CsrfToken = { headerName: '', value: '' }
protected fileUploadCsrfToken: CsrfToken | undefined
@@ -41,17 +67,10 @@ export class RequestClient implements HttpClient {
constructor(
protected baseUrl: string,
httpsAgentOptions?: https.AgentOptions,
requestsLimit?: number,
verboseMode?: VerboseMode
requestsLimit?: number
) {
this.createHttpClient(baseUrl, httpsAgentOptions)
if (requestsLimit) this.requestsLimit = requestsLimit
if (verboseMode) {
this.setVerboseMode(verboseMode)
this.enableVerboseMode()
}
}
public setConfig(baseUrl: string, httpsAgentOptions?: https.AgentOptions) {
@@ -71,7 +90,6 @@ export class RequestClient implements HttpClient {
this.csrfToken = { headerName: '', value: '' }
this.fileUploadCsrfToken = { headerName: '', value: '' }
}
public clearLocalStorageTokens() {
localStorage.setItem('accessToken', '')
localStorage.setItem('refreshToken', '')
@@ -162,7 +180,6 @@ export class RequestClient implements HttpClient {
responseType: contentType === 'text/plain' ? 'text' : 'json',
withCredentials: true
}
if (contentType === 'text/plain') {
requestConfig.transformResponse = undefined
}
@@ -372,181 +389,6 @@ export class RequestClient implements HttpClient {
})
}
/**
* Adds colors to the string.
* If verboseMode is set to 'bleached', colors should be disabled
* @param str - string to be prettified.
* @returns - prettified string
*/
private prettifyString = (str: any) =>
inspect(str, { colors: this.verboseMode !== 'bleached' })
/**
* Formats HTTP request/response body.
* @param body - HTTP request/response body.
* @returns - formatted string.
*/
private parseInterceptedBody = (body: any) => {
if (!body) return ''
let parsedBody
// Tries to parse body into JSON object.
if (typeof body === 'string') {
try {
parsedBody = JSON.parse(body)
} catch (error) {
parsedBody = body
}
} else {
parsedBody = body
}
const bodyLines = this.prettifyString(parsedBody).split('\n')
// Leaves first 50 lines
if (bodyLines.length > 51) {
bodyLines.splice(50)
bodyLines.push('...')
}
return bodyLines.join('\n')
}
private defaultInterceptionCallBack = (
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 { _header: reqHeaders, res } = request || fallbackRequest
const { rawHeaders } = res
// Converts an array of strings into a single string with the following format:
// <headerName>: <headerValue>
const resHeaders = rawHeaders.reduce(
(acc: string, value: string, i: number) => {
if (i % 2 === 0) {
acc += `${i === 0 ? '' : '\n'}${value}`
} else {
acc += `: ${value}`
}
return acc
},
''
)
const parsedResBody = this.parseInterceptedBody(resData)
// HTTP response summary.
process.logger?.info(`HTTP Request (first 50 lines):
${reqHeaders}${this.parseInterceptedBody(reqData)}
HTTP Response Code: ${this.prettifyString(status)}
HTTP Response (first 50 lines):
${resHeaders}${parsedResBody ? `\n\n${parsedResBody}` : ''}
`)
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()
}
/**
* Turns on verbose mode to log 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 = this.defaultInterceptionCallBack,
errorCallBack = this.defaultInterceptionCallBack
) => {
this.httpInterceptor = this.httpClient.interceptors.response.use(
successCallBack,
errorCallBack
)
}
/**
* Turns off verbose mode to log every HTTP response.
*/
public disableVerboseMode = () => {
if (this.httpInterceptor) {
this.httpClient.interceptors.response.eject(this.httpInterceptor)
}
}
protected getHeaders = (
accessToken: string | undefined,
contentType: string

View File

@@ -1,11 +1,20 @@
import { RequestClient } from './RequestClient'
import { AxiosResponse } from 'axios'
import { SasjsParsedResponse } from '../types'
import { SASJS_LOGS_SEPARATOR } from '../utils'
interface SasjsParsedResponse<T> {
result: T
log: string
etag: string
status: number
printOutput?: string
}
/**
* Specific request client for SASJS.
* Append tokens in headers.
*/
export class SasjsRequestClient extends RequestClient {
getHeaders = (accessToken: string | undefined, contentType: string) => {
const headers: any = {}
@@ -36,30 +45,13 @@ export class SasjsRequestClient extends RequestClient {
}
} catch {
if (response.data.includes(SASJS_LOGS_SEPARATOR)) {
const { data } = response
const splittedResponse: string[] = data.split(SASJS_LOGS_SEPARATOR)
const splittedResponse = response.data.split(SASJS_LOGS_SEPARATOR)
webout = splittedResponse.splice(0, 1)[0]
webout = splittedResponse[0]
if (webout !== undefined) parsedResponse = webout
// log can contain nested logs
const logs = splittedResponse.splice(0, splittedResponse.length - 1)
// tests if string ends with SASJS_LOGS_SEPARATOR
const endingWithLogSepRegExp = new RegExp(`${SASJS_LOGS_SEPARATOR}$`)
// at this point splittedResponse can contain only one item
const lastChunk = splittedResponse[0]
if (lastChunk) {
// if the last chunk doesn't end with SASJS_LOGS_SEPARATOR, then it is a printOutput
// else the last chunk is part of the log and has to be joined
if (!endingWithLogSepRegExp.test(data)) printOutput = lastChunk
else if (logs.length > 1) logs.push(lastChunk)
}
// join logs into single log with SASJS_LOGS_SEPARATOR
log = logs.join(SASJS_LOGS_SEPARATOR)
log = splittedResponse[1]
printOutput = splittedResponse[2]
} else {
parsedResponse = response.data
}
@@ -67,7 +59,7 @@ export class SasjsRequestClient extends RequestClient {
const returnResult: SasjsParsedResponse<T> = {
result: parsedResponse as T,
log: log || '',
log,
etag,
status: response.status
}
@@ -77,6 +69,3 @@ export class SasjsRequestClient extends RequestClient {
return returnResult
}
}
export const SASJS_LOGS_SEPARATOR =
'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784'

View File

@@ -1,169 +0,0 @@
import { SASJS_LOGS_SEPARATOR, SasjsRequestClient } from '../SasjsRequestClient'
import { SasjsParsedResponse } from '../../types'
import { AxiosResponse } from 'axios'
describe('SasjsRequestClient', () => {
const requestClient = new SasjsRequestClient('')
const etag = 'etag'
const status = 200
const webout = `hello`
const log = `1 The SAS System Tuesday, 25 July 2023 12:51:00
PROC MIGRATE will preserve current SAS file attributes and is
recommended for converting all your SAS libraries from any
SAS 8 release to SAS 9. For details and examples, please see
http://support.sas.com/rnd/migration/index.html
NOTE: SAS initialization used:
real time 0.01 seconds
cpu time 0.02 seconds
`
const printOutput = 'printOutPut'
describe('parseResponse', () => {})
it('should parse response with 1 log', () => {
const response: AxiosResponse<any> = {
data: `${webout}
${SASJS_LOGS_SEPARATOR}
${log}
${SASJS_LOGS_SEPARATOR}`,
status,
statusText: 'ok',
headers: { etag },
config: {}
}
const expectedParsedResponse: SasjsParsedResponse<string> = {
result: `${webout}
`,
log: `
${log}
`,
etag,
status
}
expect(requestClient['parseResponse'](response)).toEqual(
expectedParsedResponse
)
})
it('should parse response with 1 log and printOutput', () => {
const response: AxiosResponse<any> = {
data: `${webout}
${SASJS_LOGS_SEPARATOR}
${log}
${SASJS_LOGS_SEPARATOR}
${printOutput}`,
status,
statusText: 'ok',
headers: { etag },
config: {}
}
const expectedParsedResponse: SasjsParsedResponse<string> = {
result: `${webout}
`,
log: `
${log}
`,
etag,
status,
printOutput: `
${printOutput}`
}
expect(requestClient['parseResponse'](response)).toEqual(
expectedParsedResponse
)
})
it('should parse response with nested logs', () => {
const logWithNestedLog = `root log start
${SASJS_LOGS_SEPARATOR}
${log}
${SASJS_LOGS_SEPARATOR}
root log end`
const response: AxiosResponse<any> = {
data: `${webout}
${SASJS_LOGS_SEPARATOR}
${logWithNestedLog}
${SASJS_LOGS_SEPARATOR}`,
status,
statusText: 'ok',
headers: { etag },
config: {}
}
const expectedParsedResponse: SasjsParsedResponse<string> = {
result: `${webout}
`,
log: `
${logWithNestedLog}
`,
etag,
status
}
expect(requestClient['parseResponse'](response)).toEqual(
expectedParsedResponse
)
})
it('should parse response with nested logs and printOutput', () => {
const logWithNestedLog = `root log start
${SASJS_LOGS_SEPARATOR}
${log}
${SASJS_LOGS_SEPARATOR}
log with indentation
${SASJS_LOGS_SEPARATOR}
${log}
${SASJS_LOGS_SEPARATOR}
some SAS code containing ${SASJS_LOGS_SEPARATOR}
root log end`
const response: AxiosResponse<any> = {
data: `${webout}
${SASJS_LOGS_SEPARATOR}
${logWithNestedLog}
${SASJS_LOGS_SEPARATOR}
${printOutput}`,
status,
statusText: 'ok',
headers: { etag },
config: {}
}
const expectedParsedResponse: SasjsParsedResponse<string> = {
result: `${webout}
`,
log: `
${logWithNestedLog}
`,
etag,
status,
printOutput: `
${printOutput}`
}
expect(requestClient['parseResponse'](response)).toEqual(
expectedParsedResponse
)
})
})
describe('SASJS_LOGS_SEPARATOR', () => {
it('SASJS_LOGS_SEPARATOR should be hardcoded', () => {
expect(SASJS_LOGS_SEPARATOR).toEqual(
'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784'
)
})
})

View File

@@ -5,19 +5,14 @@ import { app, mockedAuthResponse } from './SAS_server_app'
import { ServerType } from '@sasjs/utils/types'
import SASjs from '../SASjs'
import * as axiosModules from '../utils/createAxiosInstance'
import axios from 'axios'
import {
LoginRequiredError,
AuthorizeError,
NotFoundError,
InternalServerError,
VerboseMode
} from '../types'
InternalServerError
} from '../types/errors'
import { RequestClient } from '../request/RequestClient'
import { getTokenRequestErrorPrefixResponse } from '../auth/getTokenRequestErrorPrefix'
import { AxiosResponse, AxiosError } from 'axios'
import { Logger, LogLevel } from '@sasjs/utils/logger'
import * as UtilsModule from 'util'
const axiosActual = jest.requireActual('axios')
@@ -30,6 +25,16 @@ jest
const PORT = 8000
const SERVER_URL = `https://localhost:${PORT}/`
const ERROR_MESSAGES = {
selfSigned: 'self signed certificate',
CCA: 'unable to verify the first certificate'
}
const incorrectAuthCodeErr = {
error: 'unauthorized',
error_description: 'Bad credentials'
}
describe('RequestClient', () => {
let server: http.Server
@@ -75,411 +80,6 @@ describe('RequestClient', () => {
expect(rejectionErrorMessage).toEqual(expectedError.message)
})
describe('defaultInterceptionCallBack', () => {
const reqHeaders = `POST https://sas.server.com/compute/sessions/session_id/jobs HTTP/1.1
Accept: application/json
Content-Type: application/json
User-Agent: axios/0.27.2
Content-Length: 334
host: sas.server.io
Connection: close
`
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 = {
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 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', () => {
it('should add defaultInterceptionCallBack functions to response interceptors', () => {
const requestClient = new RequestClient('')
const interceptorSpy = jest.spyOn(
requestClient['httpClient'].interceptors.response,
'use'
)
requestClient.enableVerboseMode()
expect(interceptorSpy).toHaveBeenCalledWith(
requestClient['defaultInterceptionCallBack'],
requestClient['defaultInterceptionCallBack']
)
})
it('should add callback functions to response interceptors', () => {
const requestClient = new RequestClient('')
const interceptorSpy = jest.spyOn(
requestClient['httpClient'].interceptors.response,
'use'
)
const successCallback = (response: AxiosResponse | AxiosError) => {
console.log('success')
return response
}
const failureCallback = (response: AxiosResponse | AxiosError) => {
console.log('failure')
return response
}
requestClient.enableVerboseMode(successCallback, failureCallback)
expect(interceptorSpy).toHaveBeenCalledWith(
successCallback,
failureCallback
)
})
})
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', () => {
it('should eject interceptor', () => {
const requestClient = new RequestClient('')
const interceptorSpy = jest.spyOn(
requestClient['httpClient'].interceptors.response,
'eject'
)
const interceptorId = 100
requestClient['httpInterceptor'] = interceptorId
requestClient.disableVerboseMode()
expect(interceptorSpy).toHaveBeenCalledWith(interceptorId)
})
})
describe('handleError', () => {
const requestClient = new RequestClient('https://localhost:8009')
const randomError = 'some error'
@@ -693,11 +293,3 @@ 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)]

View File

@@ -1,55 +0,0 @@
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'

View File

@@ -1,6 +1,5 @@
import * as https from 'https'
import { ServerType } from '@sasjs/utils/types'
import { VerboseMode } from '../types'
/**
* Specifies the configuration for the SASjs instance - eg where and how to
@@ -46,10 +45,6 @@ export class SASjsConfig {
* Set to `true` to enable additional debugging.
*/
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.
* Example value: 'SAS Job Execution compute context'

12
src/types/SASjsRequest.ts Normal file
View File

@@ -0,0 +1,12 @@
/**
* Represents a SASjs request, its response and logs.
*
*/
export interface SASjsRequest {
serviceLink: string
timestamp: Date
sourceCode: string
generatedCode: string
logFile: string
SASWORK: any
}

View File

@@ -6,12 +6,10 @@ export * from './Job'
export * from './JobDefinition'
export * from './JobResult'
export * from './Link'
export * from './Login'
export * from './SASjsConfig'
export * from './RequestClient'
export * from './SASjsRequest'
export * from './Session'
export * from './UploadFile'
export * from './PollOptions'
export * from './WriteStream'
export * from './ExecuteScript'
export * from './errors'

View File

@@ -1,4 +1,4 @@
import { SASjsRequest } from '../types'
import { SASjsRequest } from '../types/SASjsRequest'
/**
* Comparator for SASjs request timestamps.

2
src/utils/constants.ts Normal file
View File

@@ -0,0 +1,2 @@
export const SASJS_LOGS_SEPARATOR =
'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784'

View File

@@ -2,6 +2,7 @@ export * from './appendExtraResponseAttributes'
export * from './asyncForEach'
export * from './compareTimestamps'
export * from './convertToCsv'
export * from './constants'
export * from './createAxiosInstance'
export * from './delay'
export * from './fetchLogByChunks'