diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8f293d8..40f025b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -80,6 +80,10 @@ 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 diff --git a/README.md b/README.md index 37234c0..d24e0fc 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ The adapter will also cache the logs (if debug enabled) and even the work tables ### 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 can also be enabled/disabled by `startComputeJob` method. +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 diff --git a/src/SASjs.ts b/src/SASjs.ts index e071ecc..1320f5b 100644 --- a/src/SASjs.ts +++ b/src/SASjs.ts @@ -4,7 +4,12 @@ import { UploadFile, EditContextInput, PollOptions, - LoginMechanism + LoginMechanism, + VerboseMode, + ErrorResponse, + LoginOptions, + LoginResult, + ExecutionQuery } from './types' import { SASViyaApiClient } from './SASViyaApiClient' import { SAS9ApiClient } from './SAS9ApiClient' @@ -29,8 +34,6 @@ import { Sas9JobExecutor, FileUploader } from './job-execution' -import { ErrorResponse } from './types/errors' -import { LoginOptions, LoginResult } from './types/Login' import { AxiosResponse } from 'axios' interface ExecuteScriptParams { @@ -158,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. * @param accessToken - an access token for an authorised user. @@ -855,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 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 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( sasJob: string, @@ -866,7 +886,7 @@ export default class SASjs { pollOptions?: PollOptions, printPid = false, variables?: MacroVar, - verboseMode?: boolean + verboseMode?: VerboseMode ) { config = { ...this.sasjsConfig, @@ -880,8 +900,10 @@ export default class SASjs { ) } - if (verboseMode) this.requestClient?.enableVerboseMode() - else if (verboseMode === false) this.requestClient?.disableVerboseMode() + if (verboseMode) { + this.requestClient?.setVerboseMode(verboseMode) + this.requestClient?.enableVerboseMode() + } else if (verboseMode === false) this.requestClient?.disableVerboseMode() return this.sasViyaApiClient?.executeComputeJob( sasJob, @@ -1160,4 +1182,12 @@ export default class SASjs { 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) + } } diff --git a/src/api/viya/spec/pollJobState.spec.ts b/src/api/viya/spec/pollJobState.spec.ts index 4dfb07e..05c5872 100644 --- a/src/api/viya/spec/pollJobState.spec.ts +++ b/src/api/viya/spec/pollJobState.spec.ts @@ -276,6 +276,76 @@ 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 = { diff --git a/src/request/RequestClient.ts b/src/request/RequestClient.ts index 181cbb0..d0fcf4c 100644 --- a/src/request/RequestClient.ts +++ b/src/request/RequestClient.ts @@ -10,7 +10,7 @@ import { JobExecutionError, CertificateError } from '../types/errors' -import { SASjsRequest } from '../types' +import { SASjsRequest, HttpClient, VerboseMode } from '../types' import { parseWeboutResponse } from '../utils/parseWeboutResponse' import { prefixMessage } from '@sasjs/utils/error' import { SAS9AuthError } from '../types/errors/SAS9AuthError' @@ -22,45 +22,11 @@ import { import { InvalidSASjsCsrfError } from '../types/errors/InvalidSASjsCsrfError' import { inspect } from 'util' -export interface HttpClient { - get( - url: string, - accessToken: string | undefined, - contentType: string, - overrideHeaders: { [key: string]: string | number } - ): Promise<{ result: T; etag: string }> - - post( - url: string, - data: any, - accessToken: string | undefined, - contentType: string, - overrideHeaders: { [key: string]: string | number } - ): Promise<{ result: T; etag: string }> - - put( - url: string, - data: any, - accessToken: string | undefined, - overrideHeaders: { [key: string]: string | number } - ): Promise<{ result: T; etag: string }> - - delete( - 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 @@ -70,13 +36,16 @@ export class RequestClient implements HttpClient { protected baseUrl: string, httpsAgentOptions?: https.AgentOptions, requestsLimit?: number, - verboseMode?: boolean + verboseMode?: VerboseMode ) { this.createHttpClient(baseUrl, httpsAgentOptions) if (requestsLimit) this.requestsLimit = requestsLimit - if (verboseMode) this.enableVerboseMode() + if (verboseMode) { + this.setVerboseMode(verboseMode) + this.enableVerboseMode() + } } public setConfig(baseUrl: string, httpsAgentOptions?: https.AgentOptions) { @@ -398,10 +367,12 @@ 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: true }) + private prettifyString = (str: any) => + inspect(str, { colors: this.verboseMode !== 'bleached' }) /** * Formats HTTP request/response body. @@ -471,6 +442,17 @@ ${resHeaders}${parsedResBody ? `\n\n${parsedResBody}` : ''} return response } + /** + * 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**. diff --git a/src/request/SasjsRequestClient.ts b/src/request/SasjsRequestClient.ts index 760c5c8..175c68c 100644 --- a/src/request/SasjsRequestClient.ts +++ b/src/request/SasjsRequestClient.ts @@ -1,19 +1,11 @@ import { RequestClient } from './RequestClient' import { AxiosResponse } from 'axios' - -export interface SasjsParsedResponse { - result: T - log: string - etag: string - status: number - printOutput?: string -} +import { SasjsParsedResponse } from '../types' /** * Specific request client for SASJS. * Append tokens in headers. */ - export class SasjsRequestClient extends RequestClient { getHeaders = (accessToken: string | undefined, contentType: string) => { const headers: any = {} diff --git a/src/request/spec/SasjsRequestClient.spec.ts b/src/request/spec/SasjsRequestClient.spec.ts index b70007b..1018fcb 100644 --- a/src/request/spec/SasjsRequestClient.spec.ts +++ b/src/request/spec/SasjsRequestClient.spec.ts @@ -1,8 +1,5 @@ -import { - SASJS_LOGS_SEPARATOR, - SasjsRequestClient, - SasjsParsedResponse -} from '../SasjsRequestClient' +import { SASJS_LOGS_SEPARATOR, SasjsRequestClient } from '../SasjsRequestClient' +import { SasjsParsedResponse } from '../../types' import { AxiosResponse } from 'axios' describe('SasjsRequestClient', () => { diff --git a/src/test/RequestClient.spec.ts b/src/test/RequestClient.spec.ts index bef18bc..d592797 100644 --- a/src/test/RequestClient.spec.ts +++ b/src/test/RequestClient.spec.ts @@ -9,12 +9,14 @@ import { LoginRequiredError, AuthorizeError, NotFoundError, - InternalServerError -} from '../types/errors' + InternalServerError, + VerboseMode +} from '../types' import { RequestClient } from '../request/RequestClient' import { getTokenRequestErrorPrefixResponse } from '../auth/getTokenRequestErrorPrefix' import { AxiosResponse } from 'axios' import { Logger, LogLevel } from '@sasjs/utils/logger' +import * as UtilsModule from 'util' const axiosActual = jest.requireActual('axios') @@ -235,6 +237,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', () => { it('should eject interceptor', () => { const requestClient = new RequestClient('') diff --git a/src/types/RequestClient.ts b/src/types/RequestClient.ts new file mode 100644 index 0000000..08072c3 --- /dev/null +++ b/src/types/RequestClient.ts @@ -0,0 +1,55 @@ +import { CsrfToken } from '..' + +export interface HttpClient { + get( + url: string, + accessToken: string | undefined, + contentType: string, + overrideHeaders: { [key: string]: string | number } + ): Promise<{ result: T; etag: string }> + + post( + url: string, + data: any, + accessToken: string | undefined, + contentType: string, + overrideHeaders: { [key: string]: string | number } + ): Promise<{ result: T; etag: string }> + + put( + url: string, + data: any, + accessToken: string | undefined, + overrideHeaders: { [key: string]: string | number } + ): Promise<{ result: T; etag: string }> + + delete( + 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 { + result: T + log: string + etag: string + status: number + printOutput?: string +} + +export type VerboseMode = boolean | 'bleached' diff --git a/src/types/SASjsConfig.ts b/src/types/SASjsConfig.ts index d2c0536..92a0198 100644 --- a/src/types/SASjsConfig.ts +++ b/src/types/SASjsConfig.ts @@ -1,5 +1,6 @@ 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 @@ -48,7 +49,7 @@ export class SASjsConfig { /** * Set to `true` to enable verbose mode that will log a summary of every HTTP response. */ - verbose?: boolean = true + verbose?: VerboseMode = true /** * The name of the compute context to use when calling the Viya services directly. * Example value: 'SAS Job Execution compute context' diff --git a/src/types/SASjsRequest.ts b/src/types/SASjsRequest.ts deleted file mode 100644 index f0646bb..0000000 --- a/src/types/SASjsRequest.ts +++ /dev/null @@ -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 -} diff --git a/src/types/index.ts b/src/types/index.ts index c5994f7..a3a6978 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -6,10 +6,12 @@ export * from './Job' export * from './JobDefinition' export * from './JobResult' export * from './Link' +export * from './Login' export * from './SASjsConfig' -export * from './SASjsRequest' +export * from './RequestClient' export * from './Session' export * from './UploadFile' export * from './PollOptions' export * from './WriteStream' export * from './ExecuteScript' +export * from './errors' diff --git a/src/utils/compareTimestamps.ts b/src/utils/compareTimestamps.ts index 4e6f943..d08f9cd 100644 --- a/src/utils/compareTimestamps.ts +++ b/src/utils/compareTimestamps.ts @@ -1,4 +1,4 @@ -import { SASjsRequest } from '../types/SASjsRequest' +import { SASjsRequest } from '../types' /** * Comparator for SASjs request timestamps.