From f18a523087a6ee3a2bc9fd5f2f5e63e449f5dba5 Mon Sep 17 00:00:00 2001 From: Yury Shkoda Date: Mon, 14 Aug 2023 10:50:11 +0300 Subject: [PATCH 1/6] feat(request-client): added bleached verbose mode --- README.md | 2 +- src/SASjs.ts | 18 ++++++++--- src/request/RequestClient.ts | 22 ++++++++++--- src/test/RequestClient.spec.ts | 56 ++++++++++++++++++++++++++++++++++ src/types/RequestClient.ts | 1 + src/types/SASjsConfig.ts | 3 +- src/types/index.ts | 2 +- 7 files changed, 93 insertions(+), 11 deletions(-) create mode 100644 src/types/RequestClient.ts 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..993922f 100644 --- a/src/SASjs.ts +++ b/src/SASjs.ts @@ -855,7 +855,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 +866,7 @@ export default class SASjs { pollOptions?: PollOptions, printPid = false, variables?: MacroVar, - verboseMode?: boolean + verboseMode?: VerboseMode ) { config = { ...this.sasjsConfig, @@ -880,8 +880,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 +1162,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/request/RequestClient.ts b/src/request/RequestClient.ts index 181cbb0..b9586c7 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 { VerboseMode } from '../types' import { parseWeboutResponse } from '../utils/parseWeboutResponse' import { prefixMessage } from '@sasjs/utils/error' import { SAS9AuthError } from '../types/errors/SAS9AuthError' @@ -61,6 +61,7 @@ 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 +71,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 +402,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 +477,14 @@ ${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 + } + /** * 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/test/RequestClient.spec.ts b/src/test/RequestClient.spec.ts index bef18bc..e9650e9 100644 --- a/src/test/RequestClient.spec.ts +++ b/src/test/RequestClient.spec.ts @@ -11,10 +11,12 @@ import { NotFoundError, InternalServerError } from '../types/errors' +import { 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..4765575 --- /dev/null +++ b/src/types/RequestClient.ts @@ -0,0 +1 @@ +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/index.ts b/src/types/index.ts index c5994f7..5d51e4a 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -7,7 +7,7 @@ export * from './JobDefinition' export * from './JobResult' export * from './Link' export * from './SASjsConfig' -export * from './SASjsRequest' +export * from './RequestClient' export * from './Session' export * from './UploadFile' export * from './PollOptions' From 5731b0f9b15470a73d38301f414da22e41a45294 Mon Sep 17 00:00:00 2001 From: Yury Shkoda Date: Mon, 14 Aug 2023 10:55:09 +0300 Subject: [PATCH 2/6] refactor(request-client): put related types in one file --- src/SASjs.ts | 8 +-- src/request/RequestClient.ts | 37 +------------- src/request/SasjsRequestClient.ts | 10 +--- src/request/spec/SasjsRequestClient.spec.ts | 7 +-- src/test/RequestClient.spec.ts | 6 +-- src/types/RequestClient.ts | 54 +++++++++++++++++++++ src/types/SASjsRequest.ts | 12 ----- src/types/index.ts | 2 + src/utils/compareTimestamps.ts | 2 +- 9 files changed, 69 insertions(+), 69 deletions(-) delete mode 100644 src/types/SASjsRequest.ts diff --git a/src/SASjs.ts b/src/SASjs.ts index 993922f..1d560a1 100644 --- a/src/SASjs.ts +++ b/src/SASjs.ts @@ -4,7 +4,11 @@ import { UploadFile, EditContextInput, PollOptions, - LoginMechanism + LoginMechanism, + VerboseMode, + ErrorResponse, + LoginOptions, + LoginResult } from './types' import { SASViyaApiClient } from './SASViyaApiClient' import { SAS9ApiClient } from './SAS9ApiClient' @@ -29,8 +33,6 @@ import { Sas9JobExecutor, FileUploader } from './job-execution' -import { ErrorResponse } from './types/errors' -import { LoginOptions, LoginResult } from './types/Login' import { AxiosResponse } from 'axios' interface ExecuteScriptParams { diff --git a/src/request/RequestClient.ts b/src/request/RequestClient.ts index b9586c7..2b70d9e 100644 --- a/src/request/RequestClient.ts +++ b/src/request/RequestClient.ts @@ -10,7 +10,7 @@ import { JobExecutionError, CertificateError } from '../types/errors' -import { VerboseMode } 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,41 +22,6 @@ 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 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 e9650e9..d592797 100644 --- a/src/test/RequestClient.spec.ts +++ b/src/test/RequestClient.spec.ts @@ -9,9 +9,9 @@ import { LoginRequiredError, AuthorizeError, NotFoundError, - InternalServerError -} from '../types/errors' -import { VerboseMode } from '../types' + InternalServerError, + VerboseMode +} from '../types' import { RequestClient } from '../request/RequestClient' import { getTokenRequestErrorPrefixResponse } from '../auth/getTokenRequestErrorPrefix' import { AxiosResponse } from 'axios' diff --git a/src/types/RequestClient.ts b/src/types/RequestClient.ts index 4765575..08072c3 100644 --- a/src/types/RequestClient.ts +++ b/src/types/RequestClient.ts @@ -1 +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/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 5d51e4a..a3a6978 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -6,6 +6,7 @@ export * from './Job' export * from './JobDefinition' export * from './JobResult' export * from './Link' +export * from './Login' export * from './SASjsConfig' export * from './RequestClient' export * from './Session' @@ -13,3 +14,4 @@ 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. From 46c6d3e7f4589518d8923dd2e98e1d40084afeed Mon Sep 17 00:00:00 2001 From: Yury Shkoda Date: Mon, 14 Aug 2023 10:55:45 +0300 Subject: [PATCH 3/6] test(poll-job-state): covered changing polling strategies --- src/api/viya/spec/pollJobState.spec.ts | 70 ++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) 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 = { From 681abf5b3bc309176fa9f9eb4355b366084bed83 Mon Sep 17 00:00:00 2001 From: Yury Shkoda Date: Mon, 14 Aug 2023 11:25:57 +0300 Subject: [PATCH 4/6] chore(ci-cd): added sleep step before running cypress --- .github/workflows/build.yml | 4 ++++ 1 file changed, 4 insertions(+) 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 From 7a4feddd82c9d9780dfe89a81db8d433789c1306 Mon Sep 17 00:00:00 2001 From: Yury Shkoda Date: Mon, 14 Aug 2023 17:01:15 +0300 Subject: [PATCH 5/6] feat: added public method executeJob --- src/SASjs.ts | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/SASjs.ts b/src/SASjs.ts index 1d560a1..1320f5b 100644 --- a/src/SASjs.ts +++ b/src/SASjs.ts @@ -8,7 +8,8 @@ import { VerboseMode, ErrorResponse, LoginOptions, - LoginResult + LoginResult, + ExecutionQuery } from './types' import { SASViyaApiClient } from './SASViyaApiClient' import { SAS9ApiClient } from './SAS9ApiClient' @@ -160,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. From 62f4577b64c2f6a48be957484748cabf62ea8c0d Mon Sep 17 00:00:00 2001 From: Yury Shkoda Date: Mon, 14 Aug 2023 17:01:47 +0300 Subject: [PATCH 6/6] fix(request-client): fixed setVerboseMode method --- src/request/RequestClient.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/request/RequestClient.ts b/src/request/RequestClient.ts index 2b70d9e..d0fcf4c 100644 --- a/src/request/RequestClient.ts +++ b/src/request/RequestClient.ts @@ -448,6 +448,9 @@ ${resHeaders}${parsedResBody ? `\n\n${parsedResBody}` : ''} */ public setVerboseMode = (verboseMode: VerboseMode) => { this.verboseMode = verboseMode + + if (this.verboseMode) this.enableVerboseMode() + else this.disableVerboseMode() } /**