From 69999d8e8bdb6aa3a1014e99396cbe3f673384b3 Mon Sep 17 00:00:00 2001 From: sabhas Date: Sun, 18 Jul 2021 21:34:16 +0500 Subject: [PATCH 01/23] fix: update fileUpload method to override existing config --- src/SASjs.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/SASjs.ts b/src/SASjs.ts index d60ad27..6e7df78 100644 --- a/src/SASjs.ts +++ b/src/SASjs.ts @@ -540,11 +540,22 @@ export default class SASjs { * Process). Is prepended at runtime with the value of `appLoc`. * @param files - array of files to be uploaded, including File object and file name. * @param params - request URL parameters. + * @param config - object to override existing config (optional) */ - public uploadFile(sasJob: string, files: UploadFile[], params: any) { - const fileUploader = - this.fileUploader || - new FileUploader(this.sasjsConfig, this.jobsPath, this.requestClient!) + public uploadFile( + sasJob: string, + files: UploadFile[], + params: any, + config?: any + ) { + const fileUploader = config + ? new FileUploader( + { ...this.sasjsConfig, ...config }, + this.jobsPath, + this.requestClient! + ) + : this.fileUploader || + new FileUploader(this.sasjsConfig, this.jobsPath, this.requestClient!) return fileUploader.uploadFile(sasJob, files, params) } From c69be8ffc3169816761151a0fa0ba3a2bb3c8023 Mon Sep 17 00:00:00 2001 From: sabhas Date: Sun, 18 Jul 2021 21:37:08 +0500 Subject: [PATCH 02/23] fix: move parseSasViyaDebugResponse method to utils folder --- src/job-execution/WebJobExecutor.ts | 26 +++++++++----------------- src/utils/index.ts | 1 + src/utils/parseViyaDebugResponse.ts | 29 +++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 17 deletions(-) create mode 100644 src/utils/parseViyaDebugResponse.ts diff --git a/src/job-execution/WebJobExecutor.ts b/src/job-execution/WebJobExecutor.ts index 30a6e2f..91a0fc8 100644 --- a/src/job-execution/WebJobExecutor.ts +++ b/src/job-execution/WebJobExecutor.ts @@ -8,7 +8,11 @@ import { generateFileUploadForm } from '../file/generateFileUploadForm' import { generateTableUploadForm } from '../file/generateTableUploadForm' import { RequestClient } from '../request/RequestClient' import { SASViyaApiClient } from '../SASViyaApiClient' -import { isRelativePath, isValidJson } from '../utils' +import { + isRelativePath, + isValidJson, + parseSasViyaDebugResponse +} from '../utils' import { BaseJobExecutor } from './JobExecutor' import { parseWeboutResponse } from '../utils/parseWeboutResponse' @@ -95,8 +99,10 @@ export class WebJobExecutor extends BaseJobExecutor { this.requestClient!.post(apiUrl, formData, undefined) .then(async (res) => { if (this.serverType === ServerType.SasViya && config.debug) { - const jsonResponse = await this.parseSasViyaDebugResponse( - res.result as string + const jsonResponse = await parseSasViyaDebugResponse( + res.result as string, + this.requestClient, + this.serverUrl ) this.appendRequest(res, sasJob, config.debug) resolve(jsonResponse) @@ -151,20 +157,6 @@ export class WebJobExecutor extends BaseJobExecutor { return requestPromise } - private parseSasViyaDebugResponse = async (response: string) => { - const iframeStart = response.split( - '')[0] : null - if (!jsonUrl) { - throw new Error('Unable to find webout file URL.') - } - - return this.requestClient - .get(this.serverUrl + jsonUrl, undefined) - .then((res) => res.result) - } - private async getJobUri(sasJob: string) { if (!this.sasViyaApiClient) return '' let uri = '' diff --git a/src/utils/index.ts b/src/utils/index.ts index 3f6ec1d..9cc5244 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -13,3 +13,4 @@ export * from './splitChunks' export * from './parseWeboutResponse' export * from './fetchLogByChunks' export * from './isValidJson' +export * from './parseViyaDebugResponse' diff --git a/src/utils/parseViyaDebugResponse.ts b/src/utils/parseViyaDebugResponse.ts new file mode 100644 index 0000000..3137995 --- /dev/null +++ b/src/utils/parseViyaDebugResponse.ts @@ -0,0 +1,29 @@ +import { RequestClient } from '../request/RequestClient' + +/** + * When querying a Viya job using the Web approach (as opposed to using the APIs) with _DEBUG enabled, + * the first response contains the log with the content in an iframe. Therefore when debug is enabled, + * and the serverType is VIYA, and useComputeApi is null (WEB), we call this function to extract the + * (_webout) content from the iframe. + * @param response - first response from viya job + * @param requestClient + * @param serverUrl + * @returns + */ +export const parseSasViyaDebugResponse = async ( + response: string, + requestClient: RequestClient, + serverUrl: string +) => { + const iframeStart = response.split( + '')[0] : null + if (!jsonUrl) { + throw new Error('Unable to find webout file URL.') + } + + return requestClient + .get(serverUrl + jsonUrl, undefined) + .then((res) => res.result) +} From 5098342dfed942cb658b502347d2635f9a2c8839 Mon Sep 17 00:00:00 2001 From: sabhas Date: Sun, 18 Jul 2021 21:39:57 +0500 Subject: [PATCH 03/23] fix: retrieve content from the iframe in first response when viya Web approach used with debug enabled --- src/FileUploader.ts | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/FileUploader.ts b/src/FileUploader.ts index 148f534..922571a 100644 --- a/src/FileUploader.ts +++ b/src/FileUploader.ts @@ -1,4 +1,4 @@ -import { isUrl } from './utils' +import { isUrl, isValidJson, parseSasViyaDebugResponse } from './utils' import { UploadFile } from './types/UploadFile' import { ErrorResponse, LoginRequiredError } from './types/errors' import { RequestClient } from './request/RequestClient' @@ -63,13 +63,28 @@ export class FileUploader { return this.requestClient .post(uploadUrl, formData, undefined, 'application/json', headers) - .then((res) => { - let result + .then(async (res) => { + // for web approach on Viya + if ( + this.sasjsConfig.debug && + (this.sasjsConfig.useComputeApi === null || + this.sasjsConfig.useComputeApi === undefined) && + this.sasjsConfig.serverType === ServerType.SasViya + ) { + const jsonResponse = await parseSasViyaDebugResponse( + res.result as string, + this.requestClient, + this.sasjsConfig.serverUrl + ) + return typeof jsonResponse === 'string' + ? isValidJson(jsonResponse) + : jsonResponse + } - result = - typeof res.result === 'string' ? JSON.parse(res.result) : res.result + return typeof res.result === 'string' + ? isValidJson(res.result) + : res.result - return result //TODO: append to SASjs requests }) .catch((err: Error) => { From 5347aeba09e42195d40a34effdce9f5022da947e Mon Sep 17 00:00:00 2001 From: sabhas Date: Sun, 18 Jul 2021 23:24:22 +0500 Subject: [PATCH 04/23] fix: replace isValidJson with getValidJson --- src/FileUploader.ts | 6 +++--- src/job-execution/WebJobExecutor.ts | 6 +++--- src/request/RequestClient.ts | 4 ++-- src/test/utils/isValidJson.spec.ts | 8 ++++---- src/utils/{isValidJson.ts => getValidJson.ts} | 2 +- src/utils/index.ts | 2 +- 6 files changed, 14 insertions(+), 14 deletions(-) rename src/utils/{isValidJson.ts => getValidJson.ts} (81%) diff --git a/src/FileUploader.ts b/src/FileUploader.ts index 922571a..7001bcb 100644 --- a/src/FileUploader.ts +++ b/src/FileUploader.ts @@ -1,4 +1,4 @@ -import { isUrl, isValidJson, parseSasViyaDebugResponse } from './utils' +import { isUrl, getValidJson, parseSasViyaDebugResponse } from './utils' import { UploadFile } from './types/UploadFile' import { ErrorResponse, LoginRequiredError } from './types/errors' import { RequestClient } from './request/RequestClient' @@ -77,12 +77,12 @@ export class FileUploader { this.sasjsConfig.serverUrl ) return typeof jsonResponse === 'string' - ? isValidJson(jsonResponse) + ? getValidJson(jsonResponse) : jsonResponse } return typeof res.result === 'string' - ? isValidJson(res.result) + ? getValidJson(res.result) : res.result //TODO: append to SASjs requests diff --git a/src/job-execution/WebJobExecutor.ts b/src/job-execution/WebJobExecutor.ts index 91a0fc8..98063fa 100644 --- a/src/job-execution/WebJobExecutor.ts +++ b/src/job-execution/WebJobExecutor.ts @@ -10,7 +10,7 @@ import { RequestClient } from '../request/RequestClient' import { SASViyaApiClient } from '../SASViyaApiClient' import { isRelativePath, - isValidJson, + getValidJson, parseSasViyaDebugResponse } from '../utils' import { BaseJobExecutor } from './JobExecutor' @@ -115,11 +115,11 @@ export class WebJobExecutor extends BaseJobExecutor { ) } - isValidJson(jsonResponse) + getValidJson(jsonResponse) this.appendRequest(res, sasJob, config.debug) resolve(res.result) } - isValidJson(res.result as string) + getValidJson(res.result as string) this.appendRequest(res, sasJob, config.debug) resolve(res.result) }) diff --git a/src/request/RequestClient.ts b/src/request/RequestClient.ts index 5bf7414..d1946f7 100644 --- a/src/request/RequestClient.ts +++ b/src/request/RequestClient.ts @@ -11,7 +11,7 @@ import { import { parseWeboutResponse } from '../utils/parseWeboutResponse' import { prefixMessage } from '@sasjs/utils/error' import { SAS9AuthError } from '../types/errors/SAS9AuthError' -import { isValidJson } from '../utils' +import { getValidJson } from '../utils' export interface HttpClient { get( @@ -429,7 +429,7 @@ export class RequestClient implements HttpClient { throw new Error('Valid JSON could not be extracted from response.') } - const jsonResponse = isValidJson(weboutResponse) + const jsonResponse = getValidJson(weboutResponse) parsedResponse = jsonResponse } catch { parsedResponse = response.data diff --git a/src/test/utils/isValidJson.spec.ts b/src/test/utils/isValidJson.spec.ts index b7af712..1901ba1 100644 --- a/src/test/utils/isValidJson.spec.ts +++ b/src/test/utils/isValidJson.spec.ts @@ -1,4 +1,4 @@ -import { isValidJson } from '../../utils' +import { getValidJson } from '../../utils' describe('jsonValidator', () => { it('should not throw an error with an valid json', () => { @@ -6,7 +6,7 @@ describe('jsonValidator', () => { test: 'test' } - expect(isValidJson(json)).toBe(json) + expect(getValidJson(json)).toBe(json) }) it('should not throw an error with an valid json string', () => { @@ -14,7 +14,7 @@ describe('jsonValidator', () => { test: 'test' } - expect(isValidJson(JSON.stringify(json))).toStrictEqual(json) + expect(getValidJson(JSON.stringify(json))).toStrictEqual(json) }) it('should throw an error with an invalid json', () => { @@ -22,7 +22,7 @@ describe('jsonValidator', () => { expect(() => { try { - isValidJson(json) + getValidJson(json) } catch (err) { throw new Error() } diff --git a/src/utils/isValidJson.ts b/src/utils/getValidJson.ts similarity index 81% rename from src/utils/isValidJson.ts rename to src/utils/getValidJson.ts index 253440f..738f679 100644 --- a/src/utils/isValidJson.ts +++ b/src/utils/getValidJson.ts @@ -2,7 +2,7 @@ * Checks if string is in valid JSON format else throw error. * @param str - string to check. */ -export const isValidJson = (str: string | object) => { +export const getValidJson = (str: string | object) => { try { if (typeof str === 'object') return str diff --git a/src/utils/index.ts b/src/utils/index.ts index 9cc5244..9f70293 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -12,5 +12,5 @@ export * from './serialize' export * from './splitChunks' export * from './parseWeboutResponse' export * from './fetchLogByChunks' -export * from './isValidJson' +export * from './getValidJson' export * from './parseViyaDebugResponse' From 4a61fb8f7fc665e0145ad86a6979d1a316784fc5 Mon Sep 17 00:00:00 2001 From: sabhas Date: Mon, 19 Jul 2021 13:00:06 +0500 Subject: [PATCH 05/23] chore: update variable name from config to ovverrideSasjsConfig --- src/SASjs.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/SASjs.ts b/src/SASjs.ts index 6e7df78..ea54489 100644 --- a/src/SASjs.ts +++ b/src/SASjs.ts @@ -540,17 +540,17 @@ export default class SASjs { * Process). Is prepended at runtime with the value of `appLoc`. * @param files - array of files to be uploaded, including File object and file name. * @param params - request URL parameters. - * @param config - object to override existing config (optional) + * @param overrideSasjsConfig - object to override existing config (optional) */ public uploadFile( sasJob: string, files: UploadFile[], params: any, - config?: any + overrideSasjsConfig?: any ) { - const fileUploader = config + const fileUploader = overrideSasjsConfig ? new FileUploader( - { ...this.sasjsConfig, ...config }, + { ...this.sasjsConfig, ...overrideSasjsConfig }, this.jobsPath, this.requestClient! ) From 85e5ade93aed4ef1d857fd68d472f64a470ec2c5 Mon Sep 17 00:00:00 2001 From: sabhas Date: Mon, 19 Jul 2021 13:01:18 +0500 Subject: [PATCH 06/23] fix: handle the case when array is passed in getValidJson method --- src/test/utils/getValidJson.spec.ts | 41 +++++++++++++++++++++++++++++ src/test/utils/isValidJson.spec.ts | 31 ---------------------- src/utils/getValidJson.ts | 5 +++- 3 files changed, 45 insertions(+), 32 deletions(-) create mode 100644 src/test/utils/getValidJson.spec.ts delete mode 100644 src/test/utils/isValidJson.spec.ts diff --git a/src/test/utils/getValidJson.spec.ts b/src/test/utils/getValidJson.spec.ts new file mode 100644 index 0000000..e7fbf66 --- /dev/null +++ b/src/test/utils/getValidJson.spec.ts @@ -0,0 +1,41 @@ +import { getValidJson } from '../../utils' + +describe('jsonValidator', () => { + it('should not throw an error with a valid json', () => { + const json = { + test: 'test' + } + + expect(getValidJson(json)).toBe(json) + }) + + it('should not throw an error with a valid json string', () => { + const json = { + test: 'test' + } + + expect(getValidJson(JSON.stringify(json))).toStrictEqual(json) + }) + + it('should throw an error with an invalid json', () => { + const json = `{\"test\":\"test\"\"test2\":\"test\"}` + let errorThrown = false + try { + getValidJson(json) + } catch (error) { + errorThrown = true + } + expect(errorThrown).toBe(true) + }) + + it('should throw an error when an array is passed', () => { + const array = ['hello', 'world'] + let errorThrown = false + try { + getValidJson(array) + } catch (error) { + errorThrown = true + } + expect(errorThrown).toBe(true) + }) +}) diff --git a/src/test/utils/isValidJson.spec.ts b/src/test/utils/isValidJson.spec.ts deleted file mode 100644 index 1901ba1..0000000 --- a/src/test/utils/isValidJson.spec.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { getValidJson } from '../../utils' - -describe('jsonValidator', () => { - it('should not throw an error with an valid json', () => { - const json = { - test: 'test' - } - - expect(getValidJson(json)).toBe(json) - }) - - it('should not throw an error with an valid json string', () => { - const json = { - test: 'test' - } - - expect(getValidJson(JSON.stringify(json))).toStrictEqual(json) - }) - - it('should throw an error with an invalid json', () => { - const json = `{\"test\":\"test\"\"test2\":\"test\"}` - - expect(() => { - try { - getValidJson(json) - } catch (err) { - throw new Error() - } - }).toThrowError - }) -}) diff --git a/src/utils/getValidJson.ts b/src/utils/getValidJson.ts index 738f679..0313157 100644 --- a/src/utils/getValidJson.ts +++ b/src/utils/getValidJson.ts @@ -1,9 +1,12 @@ /** - * Checks if string is in valid JSON format else throw error. + * if string passed then parse the string to json else if throw error for all other types unless it is not a valid json object. * @param str - string to check. */ export const getValidJson = (str: string | object) => { try { + if (Array.isArray(str)) { + throw new Error('Can not parse array object to json.') + } if (typeof str === 'object') return str return JSON.parse(str) From e1a76bc45a01f7b1817493d4fe1e7cb3eff93262 Mon Sep 17 00:00:00 2001 From: sabhas Date: Mon, 19 Jul 2021 21:53:58 +0500 Subject: [PATCH 07/23] fix: update error message when folder not found --- src/SASViyaApiClient.ts | 20 +++++++++++++++++--- src/SASjs.ts | 22 +++++++++++++++------- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/SASViyaApiClient.ts b/src/SASViyaApiClient.ts index bfcdb81..e2e03d3 100644 --- a/src/SASViyaApiClient.ts +++ b/src/SASViyaApiClient.ts @@ -36,6 +36,7 @@ import { isAuthorizeFormRequired } from './auth/isAuthorizeFormRequired' import { RequestClient } from './request/RequestClient' import { prefixMessage } from '@sasjs/utils/error' import * as mime from 'mime' +import jwtDecode from 'jwt-decode' /** * A client for interfacing with the SAS Viya REST API. @@ -610,7 +611,8 @@ export class SASViyaApiClient { parentFolderPath?: string, parentFolderUri?: string, accessToken?: string, - isForced?: boolean + isForced?: boolean, + serverUrl?: string ): Promise { const logger = process.logger || console if (!parentFolderPath && !parentFolderUri) { @@ -630,7 +632,17 @@ export class SASViyaApiClient { ) const newFolderName = `${parentFolderPath.split('/').pop()}` if (newParentFolderPath === '') { - throw new Error('Root folder has to be present on the server.') + let error: string = `Root folder ${parentFolderPath} was not found\nPlease check ${serverUrl}/SASDrive\nIf folder DOES exist then it is likely a permission problem\n` + if (accessToken) { + const tokenResponse: any = jwtDecode(accessToken) + const scope = tokenResponse.scope + error = + error + `The following scopes are contained in client/secret:\n` + scope.forEach((element: any) => { + error = error + `* ${element}\n` + }) + } + throw new Error(error) } logger.info( `Creating parent folder:\n'${newFolderName}' in '${newParentFolderPath}'` @@ -639,7 +651,9 @@ export class SASViyaApiClient { newFolderName, newParentFolderPath, undefined, - accessToken + accessToken, + isForced, + serverUrl ) logger.info( `Parent folder '${newFolderName}' has been successfully created.` diff --git a/src/SASjs.ts b/src/SASjs.ts index d60ad27..5cffc70 100644 --- a/src/SASjs.ts +++ b/src/SASjs.ts @@ -282,21 +282,25 @@ export default class SASjs { parentFolderUri?: string, accessToken?: string, sasApiClient?: SASViyaApiClient, - isForced?: boolean + isForced?: boolean, + serverUrl?: string ) { if (sasApiClient) return await sasApiClient.createFolder( folderName, parentFolderPath, parentFolderUri, - accessToken + accessToken, + isForced, + serverUrl ) return await this.sasViyaApiClient!.createFolder( folderName, parentFolderPath, parentFolderUri, accessToken, - isForced + isForced, + serverUrl ) } @@ -761,7 +765,8 @@ export default class SASjs { members, accessToken, sasApiClient, - isForced + isForced, + serverUrl ) } @@ -969,7 +974,8 @@ export default class SASjs { membersJson: any[], accessToken?: string, sasApiClient?: SASViyaApiClient, - isForced?: boolean + isForced?: boolean, + serverUrl?: string ) { await asyncForEach(membersJson, async (member: any) => { switch (member.type) { @@ -980,7 +986,8 @@ export default class SASjs { undefined, accessToken, sasApiClient, - isForced + isForced, + serverUrl ) break case 'file': @@ -1012,7 +1019,8 @@ export default class SASjs { member.members, accessToken, sasApiClient, - isForced + isForced, + serverUrl ) }) } From aeabc29e5594a63f878f0b754a2ab78dade5ccdd Mon Sep 17 00:00:00 2001 From: sabhas Date: Thu, 22 Jul 2021 15:47:37 +0500 Subject: [PATCH 08/23] fix: remove serverurl argument from createFolder method and move decode token to utils project --- src/SASViyaApiClient.ts | 24 ++++++++++++++---------- src/SASjs.ts | 22 +++++++--------------- 2 files changed, 21 insertions(+), 25 deletions(-) diff --git a/src/SASViyaApiClient.ts b/src/SASViyaApiClient.ts index e2e03d3..12066fa 100644 --- a/src/SASViyaApiClient.ts +++ b/src/SASViyaApiClient.ts @@ -28,10 +28,16 @@ import { ContextManager } from './ContextManager' import { timestampToYYYYMMDDHHMMSS } from '@sasjs/utils/time' import { isAccessTokenExpiring, - isRefreshTokenExpiring + isRefreshTokenExpiring, + decodeToken } from '@sasjs/utils/auth' import { Logger, LogLevel } from '@sasjs/utils/logger' -import { SasAuthResponse, MacroVar, AuthConfig } from '@sasjs/utils/types' +import { + SasAuthResponse, + MacroVar, + AuthConfig, + DecodedToken +} from '@sasjs/utils/types' import { isAuthorizeFormRequired } from './auth/isAuthorizeFormRequired' import { RequestClient } from './request/RequestClient' import { prefixMessage } from '@sasjs/utils/error' @@ -611,8 +617,7 @@ export class SASViyaApiClient { parentFolderPath?: string, parentFolderUri?: string, accessToken?: string, - isForced?: boolean, - serverUrl?: string + isForced?: boolean ): Promise { const logger = process.logger || console if (!parentFolderPath && !parentFolderUri) { @@ -632,10 +637,11 @@ export class SASViyaApiClient { ) const newFolderName = `${parentFolderPath.split('/').pop()}` if (newParentFolderPath === '') { - let error: string = `Root folder ${parentFolderPath} was not found\nPlease check ${serverUrl}/SASDrive\nIf folder DOES exist then it is likely a permission problem\n` + let error: string = `Root folder ${parentFolderPath} was not found\nPlease check ${this.serverUrl}/SASDrive\nIf folder DOES exist then it is likely a permission problem\n` if (accessToken) { - const tokenResponse: any = jwtDecode(accessToken) - const scope = tokenResponse.scope + const decodedToken: DecodedToken = decodeToken(accessToken) + console.log(decodedToken) + const scope = decodedToken.scope error = error + `The following scopes are contained in client/secret:\n` scope.forEach((element: any) => { @@ -651,9 +657,7 @@ export class SASViyaApiClient { newFolderName, newParentFolderPath, undefined, - accessToken, - isForced, - serverUrl + accessToken ) logger.info( `Parent folder '${newFolderName}' has been successfully created.` diff --git a/src/SASjs.ts b/src/SASjs.ts index 5cffc70..d60ad27 100644 --- a/src/SASjs.ts +++ b/src/SASjs.ts @@ -282,25 +282,21 @@ export default class SASjs { parentFolderUri?: string, accessToken?: string, sasApiClient?: SASViyaApiClient, - isForced?: boolean, - serverUrl?: string + isForced?: boolean ) { if (sasApiClient) return await sasApiClient.createFolder( folderName, parentFolderPath, parentFolderUri, - accessToken, - isForced, - serverUrl + accessToken ) return await this.sasViyaApiClient!.createFolder( folderName, parentFolderPath, parentFolderUri, accessToken, - isForced, - serverUrl + isForced ) } @@ -765,8 +761,7 @@ export default class SASjs { members, accessToken, sasApiClient, - isForced, - serverUrl + isForced ) } @@ -974,8 +969,7 @@ export default class SASjs { membersJson: any[], accessToken?: string, sasApiClient?: SASViyaApiClient, - isForced?: boolean, - serverUrl?: string + isForced?: boolean ) { await asyncForEach(membersJson, async (member: any) => { switch (member.type) { @@ -986,8 +980,7 @@ export default class SASjs { undefined, accessToken, sasApiClient, - isForced, - serverUrl + isForced ) break case 'file': @@ -1019,8 +1012,7 @@ export default class SASjs { member.members, accessToken, sasApiClient, - isForced, - serverUrl + isForced ) }) } From e70a9645efc28d3578e8525d38e97d62889c8367 Mon Sep 17 00:00:00 2001 From: sabhas Date: Thu, 22 Jul 2021 15:56:22 +0500 Subject: [PATCH 09/23] fix: remove jwtDecode import statement --- src/SASViyaApiClient.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/SASViyaApiClient.ts b/src/SASViyaApiClient.ts index 12066fa..2b96e69 100644 --- a/src/SASViyaApiClient.ts +++ b/src/SASViyaApiClient.ts @@ -42,7 +42,6 @@ import { isAuthorizeFormRequired } from './auth/isAuthorizeFormRequired' import { RequestClient } from './request/RequestClient' import { prefixMessage } from '@sasjs/utils/error' import * as mime from 'mime' -import jwtDecode from 'jwt-decode' /** * A client for interfacing with the SAS Viya REST API. From c2ff28c3234136919e45b3bd0257c8aca7dbc133 Mon Sep 17 00:00:00 2001 From: Allan Bowe <4420615+allanbowe@users.noreply.github.com> Date: Fri, 23 Jul 2021 13:04:38 +0300 Subject: [PATCH 10/23] Update PULL_REQUEST_TEMPLATE.md --- PULL_REQUEST_TEMPLATE.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index 744ed15..03f5c8a 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -12,9 +12,9 @@ What code changes have been made to achieve the intent. ## Checks -No PR (that involves a non-trivial code change) should be merged, unless all four of the 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. + -- [ ] Code is formatted correctly (`npm run lint:fix`). -- [ ] All unit tests are passing (`npm test`). - [ ] All `sasjs-cli` unit tests are passing (`npm test`). - [ ] All `sasjs-tests` are passing (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 From 2a9526d056dc1e3074c0fa05fdc6cde1728854d9 Mon Sep 17 00:00:00 2001 From: Krishna Acondy Date: Fri, 23 Jul 2021 22:23:05 +0100 Subject: [PATCH 11/23] fix(node): add util to check if running in node --- src/utils/isNode.ts | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 src/utils/isNode.ts diff --git a/src/utils/isNode.ts b/src/utils/isNode.ts new file mode 100644 index 0000000..4a47e8f --- /dev/null +++ b/src/utils/isNode.ts @@ -0,0 +1,4 @@ +export const isNode = () => + typeof process !== 'undefined' && + process.versions != null && + process.versions.node != null From 0a6c5a0ec414c4728fe930b85df588e2f2e6de2d Mon Sep 17 00:00:00 2001 From: Krishna Acondy Date: Fri, 23 Jul 2021 22:24:04 +0100 Subject: [PATCH 12/23] fix(fs): replace fs imports with locally defined WriteStream interface --- src/api/viya/saveLog.ts | 2 +- src/api/viya/writeStream.ts | 4 ++-- src/types/WriteStream.ts | 4 ++++ src/types/index.ts | 1 + src/utils/index.ts | 1 + 5 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 src/types/WriteStream.ts diff --git a/src/api/viya/saveLog.ts b/src/api/viya/saveLog.ts index 3bbd498..2b5ec08 100644 --- a/src/api/viya/saveLog.ts +++ b/src/api/viya/saveLog.ts @@ -1,7 +1,7 @@ import { Job } from '../..' import { RequestClient } from '../../request/RequestClient' import { fetchLog } from '../../utils' -import { WriteStream } from 'fs' +import { WriteStream } from '../../types' import { writeStream } from './writeStream' /** diff --git a/src/api/viya/writeStream.ts b/src/api/viya/writeStream.ts index dc09885..0baaaa0 100644 --- a/src/api/viya/writeStream.ts +++ b/src/api/viya/writeStream.ts @@ -1,11 +1,11 @@ -import { WriteStream } from 'fs' +import { WriteStream } from '../../types' export const writeStream = async ( stream: WriteStream, content: string ): Promise => { return new Promise((resolve, reject) => { - stream.write(content + '\n\nnext chunk\n\n', (e) => { + stream.write(content + '\n', (e) => { if (e) { return reject(e) } diff --git a/src/types/WriteStream.ts b/src/types/WriteStream.ts new file mode 100644 index 0000000..83a1d13 --- /dev/null +++ b/src/types/WriteStream.ts @@ -0,0 +1,4 @@ +export interface WriteStream { + write: (content: string, callback: (err?: Error) => any) => void + path: string +} diff --git a/src/types/index.ts b/src/types/index.ts index 313aef2..2303619 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -11,3 +11,4 @@ export * from './SASjsRequest' export * from './Session' export * from './UploadFile' export * from './PollOptions' +export * from './WriteStream' diff --git a/src/utils/index.ts b/src/utils/index.ts index 9f70293..2a05d63 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,6 +1,7 @@ export * from './asyncForEach' export * from './compareTimestamps' export * from './convertToCsv' +export * from './isNode' export * from './isRelativePath' export * from './isUri' export * from './isUrl' From 15d5f9ec915c75ef438876a449b72f0368857574 Mon Sep 17 00:00:00 2001 From: Krishna Acondy Date: Fri, 23 Jul 2021 22:24:21 +0100 Subject: [PATCH 13/23] chore(paths): fix import paths --- src/auth/getTokens.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/auth/getTokens.ts b/src/auth/getTokens.ts index 031c6a3..fe7779d 100644 --- a/src/auth/getTokens.ts +++ b/src/auth/getTokens.ts @@ -1,9 +1,9 @@ import { - AuthConfig, isAccessTokenExpiring, isRefreshTokenExpiring, hasTokenExpired -} from '@sasjs/utils' +} from '@sasjs/utils/auth' +import { AuthConfig } from '@sasjs/utils/types' import { RequestClient } from '../request/RequestClient' import { refreshTokens } from './refreshTokens' From 281a145beffab01ed16f0a679b853aaf04daa2a9 Mon Sep 17 00:00:00 2001 From: Krishna Acondy Date: Fri, 23 Jul 2021 22:24:41 +0100 Subject: [PATCH 14/23] fix(node): only create and write file stream if running in node --- src/api/viya/getFileStream.ts | 16 +++++++++++++ src/api/viya/pollJobState.ts | 44 +++++++++++++---------------------- 2 files changed, 32 insertions(+), 28 deletions(-) create mode 100644 src/api/viya/getFileStream.ts diff --git a/src/api/viya/getFileStream.ts b/src/api/viya/getFileStream.ts new file mode 100644 index 0000000..620ddc0 --- /dev/null +++ b/src/api/viya/getFileStream.ts @@ -0,0 +1,16 @@ +import { isFolder } from '@sasjs/utils/file' +import { generateTimestamp } from '@sasjs/utils/time' +import { Job } from '../../types' + +export const getFileStream = async (job: Job, filePath?: string) => { + const { createWriteStream } = require('@sasjs/utils/file') + const logPath = filePath || process.cwd() + const isFolderPath = await isFolder(logPath) + if (isFolderPath) { + const logFileName = `${job.name || 'job'}-${generateTimestamp()}.log` + const logFilePath = `${filePath || process.cwd()}/${logFileName}` + return await createWriteStream(logFilePath) + } else { + return await createWriteStream(logPath) + } +} diff --git a/src/api/viya/pollJobState.ts b/src/api/viya/pollJobState.ts index c69546e..c4b05d0 100644 --- a/src/api/viya/pollJobState.ts +++ b/src/api/viya/pollJobState.ts @@ -3,11 +3,8 @@ import { Job, PollOptions } from '../..' import { getTokens } from '../../auth/getTokens' import { RequestClient } from '../../request/RequestClient' import { JobStatePollError } from '../../types/errors' -import { generateTimestamp } from '@sasjs/utils/time' -import { saveLog } from './saveLog' -import { createWriteStream, isFolder } from '@sasjs/utils/file' -import { WriteStream } from 'fs' -import { Link } from '../../types' +import { Link, WriteStream } from '../../types' +import { isNode } from '../../utils' export async function pollJobState( requestClient: RequestClient, @@ -55,21 +52,9 @@ export async function pollJobState( } let logFileStream - if (pollOptions.streamLog) { - const logPath = pollOptions?.logFolderPath || process.cwd() - const isFolderPath = await isFolder(logPath) - if (isFolderPath) { - const logFileName = `${ - postedJob.name || 'job' - }-${generateTimestamp()}.log` - const logFilePath = `${ - pollOptions?.logFolderPath || process.cwd() - }/${logFileName}` - - logFileStream = await createWriteStream(logFilePath) - } else { - logFileStream = await createWriteStream(logPath) - } + if (pollOptions.streamLog && isNode()) { + const { getFileStream } = require('./getFileStream') + logFileStream = await getFileStream(postedJob, pollOptions.logFolderPath) } // Poll up to the first 100 times with the specified poll interval @@ -230,14 +215,17 @@ const doPoll = async ( const endLogLine = job.logStatistics?.lineCount ?? 1000000 - await saveLog( - postedJob, - requestClient, - startLogLine, - endLogLine, - logStream, - authConfig?.access_token - ) + const { saveLog } = isNode() ? require('./saveLog') : { saveLog: null } + if (saveLog) { + await saveLog( + postedJob, + requestClient, + startLogLine, + endLogLine, + logStream, + authConfig?.access_token + ) + } startLogLine += endLogLine } From 7cf681bea3836c4b02ef055c037cbf55feac5c51 Mon Sep 17 00:00:00 2001 From: Krishna Acondy Date: Fri, 23 Jul 2021 22:24:48 +0100 Subject: [PATCH 15/23] chore(tests): fix tests --- src/api/viya/spec/getFileStream.spec.ts | 41 +++++++++++++++++++++++++ src/api/viya/spec/pollJobState.spec.ts | 39 ++++++++++++----------- src/api/viya/spec/saveLog.spec.ts | 2 +- src/api/viya/spec/writeStream.spec.ts | 25 +++++++++++++++ 4 files changed, 88 insertions(+), 19 deletions(-) create mode 100644 src/api/viya/spec/getFileStream.spec.ts create mode 100644 src/api/viya/spec/writeStream.spec.ts diff --git a/src/api/viya/spec/getFileStream.spec.ts b/src/api/viya/spec/getFileStream.spec.ts new file mode 100644 index 0000000..0722e37 --- /dev/null +++ b/src/api/viya/spec/getFileStream.spec.ts @@ -0,0 +1,41 @@ +import { Logger, LogLevel } from '@sasjs/utils/logger' +import * as path from 'path' +import * as fileModule from '@sasjs/utils/file' +import { getFileStream } from '../getFileStream' +import { mockJob } from './mockResponses' +import { WriteStream } from '../../../types' + +describe('getFileStream', () => { + beforeEach(() => { + ;(process as any).logger = new Logger(LogLevel.Off) + setupMocks() + }) + it('should use the given log path if it points to a file', async () => { + const { createWriteStream } = require('@sasjs/utils/file') + + await getFileStream(mockJob, path.join(__dirname, 'test.log')) + + expect(createWriteStream).toHaveBeenCalledWith( + path.join(__dirname, 'test.log') + ) + }) + + it('should generate a log file path with a timestamp if it points to a folder', async () => { + const { createWriteStream } = require('@sasjs/utils/file') + + await getFileStream(mockJob, __dirname) + + expect(createWriteStream).not.toHaveBeenCalledWith(__dirname) + expect(createWriteStream).toHaveBeenCalledWith( + expect.stringContaining(__dirname + '/test job-20') + ) + }) +}) + +const setupMocks = () => { + jest.restoreAllMocks() + jest.mock('@sasjs/utils/file/file') + jest + .spyOn(fileModule, 'createWriteStream') + .mockImplementation(() => Promise.resolve({} as unknown as WriteStream)) +} diff --git a/src/api/viya/spec/pollJobState.spec.ts b/src/api/viya/spec/pollJobState.spec.ts index 7855f57..aba468c 100644 --- a/src/api/viya/spec/pollJobState.spec.ts +++ b/src/api/viya/spec/pollJobState.spec.ts @@ -1,11 +1,11 @@ import { Logger, LogLevel } from '@sasjs/utils' -import * as path from 'path' -import * as fileModule from '@sasjs/utils/file' import { RequestClient } from '../../../request/RequestClient' import { mockAuthConfig, mockJob } from './mockResponses' import { pollJobState } from '../pollJobState' import * as getTokensModule from '../../../auth/getTokens' import * as saveLogModule from '../saveLog' +import * as getFileStreamModule from '../getFileStream' +import * as isNodeModule from '../../../utils/isNode' import { PollOptions } from '../../../types' import { WriteStream } from 'fs' @@ -77,42 +77,43 @@ describe('pollJobState', () => { it('should attempt to fetch and save the log after each poll when streamLog is true', async () => { mockSimplePoll() + const { saveLog } = require('../saveLog') await pollJobState(requestClient, mockJob, false, mockAuthConfig, { ...defaultPollOptions, streamLog: true }) - expect(saveLogModule.saveLog).toHaveBeenCalledTimes(2) + expect(saveLog).toHaveBeenCalledTimes(2) }) - it('should use the given log path if it points to a file', async () => { + it('should create a write stream in Node.js environment when streamLog is true', async () => { mockSimplePoll() + const { getFileStream } = require('../getFileStream') + const { saveLog } = require('../saveLog') await pollJobState(requestClient, mockJob, false, mockAuthConfig, { ...defaultPollOptions, - streamLog: true, - logFolderPath: path.join(__dirname, 'test.log') + streamLog: true }) - expect(fileModule.createWriteStream).toHaveBeenCalledWith( - path.join(__dirname, 'test.log') - ) + expect(getFileStream).toHaveBeenCalled() + expect(saveLog).toHaveBeenCalledTimes(2) }) - it('should generate a log file path with a timestamp if it points to a folder', async () => { + it('should not create a write stream in a non-Node.js environment', async () => { mockSimplePoll() + jest.spyOn(isNodeModule, 'isNode').mockImplementation(() => false) + const { saveLog } = require('../saveLog') + const { getFileStream } = require('../getFileStream') await pollJobState(requestClient, mockJob, false, mockAuthConfig, { ...defaultPollOptions, - streamLog: true, - logFolderPath: path.join(__dirname) + streamLog: true }) - expect(fileModule.createWriteStream).not.toHaveBeenCalledWith(__dirname) - expect(fileModule.createWriteStream).toHaveBeenCalledWith( - expect.stringContaining(__dirname + '/test job-20') - ) + expect(getFileStream).not.toHaveBeenCalled() + expect(saveLog).not.toHaveBeenCalled() }) it('should not attempt to fetch and save the log after each poll when streamLog is false', async () => { @@ -247,7 +248,8 @@ const setupMocks = () => { jest.mock('../../../request/RequestClient') jest.mock('../../../auth/getTokens') jest.mock('../saveLog') - jest.mock('@sasjs/utils/file') + jest.mock('../getFileStream') + jest.mock('../../../utils/isNode') jest .spyOn(requestClient, 'get') @@ -261,8 +263,9 @@ const setupMocks = () => { .spyOn(saveLogModule, 'saveLog') .mockImplementation(() => Promise.resolve()) jest - .spyOn(fileModule, 'createWriteStream') + .spyOn(getFileStreamModule, 'getFileStream') .mockImplementation(() => Promise.resolve({} as unknown as WriteStream)) + jest.spyOn(isNodeModule, 'isNode').mockImplementation(() => true) } const mockSimplePoll = (runningCount = 2) => { diff --git a/src/api/viya/spec/saveLog.spec.ts b/src/api/viya/spec/saveLog.spec.ts index a6c662b..261438e 100644 --- a/src/api/viya/spec/saveLog.spec.ts +++ b/src/api/viya/spec/saveLog.spec.ts @@ -4,7 +4,7 @@ import * as fetchLogsModule from '../../../utils/fetchLogByChunks' import * as writeStreamModule from '../writeStream' import { saveLog } from '../saveLog' import { mockJob } from './mockResponses' -import { WriteStream } from 'fs' +import { WriteStream } from '../../../types' const requestClient = new (>RequestClient)() const stream = {} as unknown as WriteStream diff --git a/src/api/viya/spec/writeStream.spec.ts b/src/api/viya/spec/writeStream.spec.ts new file mode 100644 index 0000000..358c82a --- /dev/null +++ b/src/api/viya/spec/writeStream.spec.ts @@ -0,0 +1,25 @@ +import { WriteStream } from '../../../types' +import { writeStream } from '../writeStream' +import 'jest-extended' + +describe('writeStream', () => { + const stream: WriteStream = { + write: jest.fn(), + path: 'test' + } + + it('should resolve when the stream is written successfully', async () => { + expect(writeStream(stream, 'test')).toResolve() + + expect(stream.write).toHaveBeenCalledWith('test\n', expect.anything()) + }) + + it('should reject when the write errors out', async () => { + jest + .spyOn(stream, 'write') + .mockImplementation((_, callback) => callback(new Error('Test Error'))) + const error = await writeStream(stream, 'test').catch((e) => e) + + expect(error.message).toEqual('Test Error') + }) +}) From 87e2edbd6cfa6da61ad10c2ff607e894e4240ea1 Mon Sep 17 00:00:00 2001 From: Krishna Acondy Date: Sat, 24 Jul 2021 00:12:11 +0100 Subject: [PATCH 16/23] chore(test): fix long poll count --- src/api/viya/spec/pollJobState.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/viya/spec/pollJobState.spec.ts b/src/api/viya/spec/pollJobState.spec.ts index aba468c..74f39e1 100644 --- a/src/api/viya/spec/pollJobState.spec.ts +++ b/src/api/viya/spec/pollJobState.spec.ts @@ -311,7 +311,7 @@ const mockLongPoll = () => { return Promise.resolve({ result: mockJob, etag: '', status: 200 }) } return Promise.resolve({ - result: count <= 101 ? 'running' : 'completed', + result: count <= 102 ? 'running' : 'completed', etag: '', status: 200 }) From 626fc2e15f0a51cc7e8bed05e0f0b46683ef52e6 Mon Sep 17 00:00:00 2001 From: Krishna Acondy Date: Sat, 24 Jul 2021 09:53:39 +0100 Subject: [PATCH 17/23] fix(path): make log file path platform-agnostic --- src/api/viya/getFileStream.ts | 3 ++- src/api/viya/spec/getFileStream.spec.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/api/viya/getFileStream.ts b/src/api/viya/getFileStream.ts index 620ddc0..c647f3e 100644 --- a/src/api/viya/getFileStream.ts +++ b/src/api/viya/getFileStream.ts @@ -8,7 +8,8 @@ export const getFileStream = async (job: Job, filePath?: string) => { const isFolderPath = await isFolder(logPath) if (isFolderPath) { const logFileName = `${job.name || 'job'}-${generateTimestamp()}.log` - const logFilePath = `${filePath || process.cwd()}/${logFileName}` + const path = require('path') + const logFilePath = path.join(filePath || process.cwd(), logFileName) return await createWriteStream(logFilePath) } else { return await createWriteStream(logPath) diff --git a/src/api/viya/spec/getFileStream.spec.ts b/src/api/viya/spec/getFileStream.spec.ts index 0722e37..a05b5cb 100644 --- a/src/api/viya/spec/getFileStream.spec.ts +++ b/src/api/viya/spec/getFileStream.spec.ts @@ -27,7 +27,7 @@ describe('getFileStream', () => { expect(createWriteStream).not.toHaveBeenCalledWith(__dirname) expect(createWriteStream).toHaveBeenCalledWith( - expect.stringContaining(__dirname + '/test job-20') + expect.stringContaining(path.join(__dirname, '/test job-20')) ) }) }) From eac9da22bfbdce332d95e6ada0943a01123877cc Mon Sep 17 00:00:00 2001 From: Krishna Acondy Date: Sat, 24 Jul 2021 10:27:31 +0100 Subject: [PATCH 18/23] chore(test): fix assertion --- src/api/viya/spec/getFileStream.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/viya/spec/getFileStream.spec.ts b/src/api/viya/spec/getFileStream.spec.ts index a05b5cb..9ab766b 100644 --- a/src/api/viya/spec/getFileStream.spec.ts +++ b/src/api/viya/spec/getFileStream.spec.ts @@ -27,7 +27,7 @@ describe('getFileStream', () => { expect(createWriteStream).not.toHaveBeenCalledWith(__dirname) expect(createWriteStream).toHaveBeenCalledWith( - expect.stringContaining(path.join(__dirname, '/test job-20')) + expect.stringContaining(path.join(__dirname, 'test job-20')) ) }) }) From dfbe2d8f9449a12f31d0787afae7e33f8726c1c4 Mon Sep 17 00:00:00 2001 From: Allan Bowe Date: Sat, 24 Jul 2021 21:31:51 +0300 Subject: [PATCH 19/23] chore: contributors --- .all-contributorsrc | 102 ++++++++++++++++++++++++++++++++++++++++++++ README.md | 31 +++++++++++++- 2 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 .all-contributorsrc diff --git a/.all-contributorsrc b/.all-contributorsrc new file mode 100644 index 0000000..a0325db --- /dev/null +++ b/.all-contributorsrc @@ -0,0 +1,102 @@ +{ + "projectName": "adapter", + "projectOwner": "sasjs", + "repoType": "github", + "repoHost": "https://github.com", + "files": [ + "README.md" + ], + "imageSize": 100, + "commit": false, + "commitConvention": "angular", + "contributors": [ + { + "login": "krishna-acondy", + "name": "Krishna Acondy", + "avatar_url": "https://avatars.githubusercontent.com/u/2980428?v=4", + "profile": "https://krishna-acondy.io/", + "contributions": [ + "code", + "infra", + "blog", + "content", + "ideas", + "video" + ] + }, + { + "login": "YuryShkoda", + "name": "Yury Shkoda", + "avatar_url": "https://avatars.githubusercontent.com/u/25773492?v=4", + "profile": "https://www.erudicat.com/", + "contributions": [ + "code", + "infra", + "ideas", + "test", + "video" + ] + }, + { + "login": "medjedovicm", + "name": "Mihajlo Medjedovic", + "avatar_url": "https://avatars.githubusercontent.com/u/18329105?v=4", + "profile": "https://github.com/medjedovicm", + "contributions": [ + "code", + "infra", + "test", + "review" + ] + }, + { + "login": "allanbowe", + "name": "Allan Bowe", + "avatar_url": "https://avatars.githubusercontent.com/u/4420615?v=4", + "profile": "https://github.com/allanbowe", + "contributions": [ + "code", + "review", + "test", + "mentoring", + "maintenance" + ] + }, + { + "login": "saadjutt01", + "name": "Muhammad Saad ", + "avatar_url": "https://avatars.githubusercontent.com/u/8914650?v=4", + "profile": "https://github.com/saadjutt01", + "contributions": [ + "code", + "review", + "test", + "mentoring", + "infra" + ] + }, + { + "login": "sabhas", + "name": "Sabir Hassan", + "avatar_url": "https://avatars.githubusercontent.com/u/82647447?v=4", + "profile": "https://github.com/sabhas", + "contributions": [ + "code", + "review", + "test", + "ideas" + ] + }, + { + "login": "VladislavParhomchik", + "name": "VladislavParhomchik", + "avatar_url": "https://avatars.githubusercontent.com/u/83717836?v=4", + "profile": "https://github.com/VladislavParhomchik", + "contributions": [ + "test", + "review" + ] + } + ], + "contributorsPerLine": 7 +} diff --git a/README.md b/README.md index 5665baf..9dc32b4 100644 --- a/README.md +++ b/README.md @@ -172,7 +172,7 @@ Configuration on the client side involves passing an object on startup, which ca * `serverType` - either `SAS9` or `SASVIYA`. * `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. -* `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`. The adapter supports a number of approaches for interfacing with Viya (`serverType` is `SASVIYA`). For maximum performance, be sure to [configure your compute context](https://sasjs.io/guide-viya/#shared-account-and-server-re-use) with `reuseServerProcesses` as `true` and a system account in `runServerAs`. This functionality is available since Viya 3.5. This configuration is supported when [creating contexts using the CLI](https://sasjs.io/sasjs-cli-context/#sasjs-context-create). @@ -230,3 +230,32 @@ If you are a SAS 9 or SAS Viya customer you can also request a copy of [Data Con If you find this library useful, help us grow our star graph! ![](https://starchart.cc/sasjs/adapter.svg) + +## Contributors ✨ + +[![All Contributors](https://img.shields.io/badge/all_contributors-7-orange.svg?style=flat-square)](#contributors-) + + +Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): + + + + + + + + + + + + + + +

Krishna Acondy

💻 🚇 📝 🖋 🤔 📹

Yury Shkoda

💻 🚇 🤔 ⚠️ 📹

Mihajlo Medjedovic

💻 🚇 ⚠️ 👀

Allan Bowe

💻 👀 ⚠️ 🧑‍🏫 🚧

Muhammad Saad

💻 👀 ⚠️ 🧑‍🏫 🚇

Sabir Hassan

💻 👀 ⚠️ 🤔

VladislavParhomchik

⚠️ 👀
+ + + + + + +This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! From 26f008d527e69abb45fa1c0ad7c8724ae221962f Mon Sep 17 00:00:00 2001 From: sabhas Date: Mon, 26 Jul 2021 11:09:31 +0500 Subject: [PATCH 20/23] chore: remove console log statement --- src/SASViyaApiClient.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/SASViyaApiClient.ts b/src/SASViyaApiClient.ts index 2a6b9d1..8657b82 100644 --- a/src/SASViyaApiClient.ts +++ b/src/SASViyaApiClient.ts @@ -390,10 +390,9 @@ export class SASViyaApiClient { let error: string = `Root folder ${parentFolderPath} was not found\nPlease check ${this.serverUrl}/SASDrive\nIf folder DOES exist then it is likely a permission problem\n` if (accessToken) { const decodedToken: DecodedToken = decodeToken(accessToken) - console.log(decodedToken) const scope = decodedToken.scope error = - error + `The following scopes are contained in client/secret:\n` + error + `The following scopes are contained in access token:\n` scope.forEach((element: any) => { error = error + `* ${element}\n` }) From 710056bded3a1367f5c25840f34309d95849494b Mon Sep 17 00:00:00 2001 From: sabhas Date: Mon, 26 Jul 2021 15:30:19 +0500 Subject: [PATCH 21/23] fix: create a utility throwError and add test case for it --- src/SASViyaApiClient.ts | 22 +++------------------- src/test/utils/throwError.spec.ts | 16 ++++++++++++++++ src/utils/index.ts | 1 + src/utils/throwError.ts | 17 +++++++++++++++++ 4 files changed, 37 insertions(+), 19 deletions(-) create mode 100644 src/test/utils/throwError.spec.ts create mode 100644 src/utils/throwError.ts diff --git a/src/SASViyaApiClient.ts b/src/SASViyaApiClient.ts index 8657b82..dcd9d92 100644 --- a/src/SASViyaApiClient.ts +++ b/src/SASViyaApiClient.ts @@ -1,4 +1,4 @@ -import { isRelativePath, isUri, isUrl } from './utils' +import { isRelativePath, isUri, isUrl, rootFolderNotFound } from './utils' import * as NodeFormData from 'form-data' import { Job, @@ -14,13 +14,7 @@ import { import { JobExecutionError } from './types/errors' import { SessionManager } from './SessionManager' import { ContextManager } from './ContextManager' -import { decodeToken } from '@sasjs/utils/auth' -import { - SasAuthResponse, - MacroVar, - AuthConfig, - DecodedToken -} from '@sasjs/utils/types' +import { SasAuthResponse, MacroVar, AuthConfig } from '@sasjs/utils/types' import { isAuthorizeFormRequired } from './auth/isAuthorizeFormRequired' import { RequestClient } from './request/RequestClient' import { prefixMessage } from '@sasjs/utils/error' @@ -387,17 +381,7 @@ export class SASViyaApiClient { ) const newFolderName = `${parentFolderPath.split('/').pop()}` if (newParentFolderPath === '') { - let error: string = `Root folder ${parentFolderPath} was not found\nPlease check ${this.serverUrl}/SASDrive\nIf folder DOES exist then it is likely a permission problem\n` - if (accessToken) { - const decodedToken: DecodedToken = decodeToken(accessToken) - const scope = decodedToken.scope - error = - error + `The following scopes are contained in access token:\n` - scope.forEach((element: any) => { - error = error + `* ${element}\n` - }) - } - throw new Error(error) + rootFolderNotFound(parentFolderPath, this.serverUrl, accessToken) } logger.info( `Creating parent folder:\n'${newFolderName}' in '${newParentFolderPath}'` diff --git a/src/test/utils/throwError.spec.ts b/src/test/utils/throwError.spec.ts new file mode 100644 index 0000000..363ebfb --- /dev/null +++ b/src/test/utils/throwError.spec.ts @@ -0,0 +1,16 @@ +import { rootFolderNotFound } from '../../utils' + +describe('root folder not found', () => { + it('when access token is provided, error message should contain scope of accessToken', () => { + const token = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6WyJzY29wZS0xIiwic2NvcGUtMiJdfQ.ktqPL2ulln-8Asa2jSV9QCfDYmQuNk4tNKopxJR5xZs' + let error + try { + rootFolderNotFound('/myProject', 'https://analytium.co.uk', token) + } catch (err) { + error = err.message + } + expect(error).toContain('scope-1') + expect(error).toContain('scope-2') + }) +}) diff --git a/src/utils/index.ts b/src/utils/index.ts index 2a05d63..6501f54 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -15,3 +15,4 @@ export * from './parseWeboutResponse' export * from './fetchLogByChunks' export * from './getValidJson' export * from './parseViyaDebugResponse' +export * from './throwError' diff --git a/src/utils/throwError.ts b/src/utils/throwError.ts new file mode 100644 index 0000000..c054e10 --- /dev/null +++ b/src/utils/throwError.ts @@ -0,0 +1,17 @@ +import { DecodedToken, decodeToken } from '@sasjs/utils' + +export const rootFolderNotFound = ( + parentFolderPath: string, + serverUrl: string, + accessToken?: string +) => { + let error: string = `Root folder ${parentFolderPath} was not found\nPlease check ${serverUrl}/SASDrive\nIf folder DOES exist then it is likely a permission problem\n` + if (accessToken) { + const decodedToken: DecodedToken = decodeToken(accessToken) + let scope = decodedToken.scope + scope = scope.map((element) => '* ' + element) + error += + `The following scopes are contained in access token:\n` + scope.join('\n') + } + throw new Error(error) +} From 1ace15a3082430c873aed49f08639b353da9d1a5 Mon Sep 17 00:00:00 2001 From: Krishna Acondy Date: Tue, 27 Jul 2021 07:52:19 +0100 Subject: [PATCH 22/23] fix(root-folder-not-found): create RootFolderNotFoundError class --- .git-hooks/commit-msg | 2 +- src/SASViyaApiClient.ts | 10 +++-- src/test/utils/throwError.spec.ts | 16 -------- .../errors/RootFolderNotFoundError.spec.ts | 40 +++++++++++++++++++ src/types/errors/RootFolderNotFoundError.ts | 24 +++++++++++ src/types/errors/index.ts | 1 + src/utils/index.ts | 1 - src/utils/throwError.ts | 17 -------- 8 files changed, 73 insertions(+), 38 deletions(-) delete mode 100644 src/test/utils/throwError.spec.ts create mode 100644 src/types/errors/RootFolderNotFoundError.spec.ts create mode 100644 src/types/errors/RootFolderNotFoundError.ts delete mode 100644 src/utils/throwError.ts diff --git a/.git-hooks/commit-msg b/.git-hooks/commit-msg index 003f76b..cb3bd89 100755 --- a/.git-hooks/commit-msg +++ b/.git-hooks/commit-msg @@ -6,7 +6,7 @@ GREEN="\033[1;32m" # temporary file which holds the message). commit_message=$(cat "$1") -if (echo "$commit_message" | grep -Eq "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z0-9 \-\*]+\))?!?: .+$") then +if (echo "$commit_message" | grep -Eq "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z0-9 -\*]+\))?!?: .+$") then echo "${GREEN} ✔ Commit message meets Conventional Commit standards" exit 0 fi diff --git a/src/SASViyaApiClient.ts b/src/SASViyaApiClient.ts index dcd9d92..a8dbc9f 100644 --- a/src/SASViyaApiClient.ts +++ b/src/SASViyaApiClient.ts @@ -1,4 +1,4 @@ -import { isRelativePath, isUri, isUrl, rootFolderNotFound } from './utils' +import { isRelativePath, isUri, isUrl } from './utils' import * as NodeFormData from 'form-data' import { Job, @@ -11,7 +11,7 @@ import { JobDefinition, PollOptions } from './types' -import { JobExecutionError } from './types/errors' +import { JobExecutionError, RootFolderNotFoundError } from './types/errors' import { SessionManager } from './SessionManager' import { ContextManager } from './ContextManager' import { SasAuthResponse, MacroVar, AuthConfig } from '@sasjs/utils/types' @@ -381,7 +381,11 @@ export class SASViyaApiClient { ) const newFolderName = `${parentFolderPath.split('/').pop()}` if (newParentFolderPath === '') { - rootFolderNotFound(parentFolderPath, this.serverUrl, accessToken) + throw new RootFolderNotFoundError( + parentFolderPath, + this.serverUrl, + accessToken + ) } logger.info( `Creating parent folder:\n'${newFolderName}' in '${newParentFolderPath}'` diff --git a/src/test/utils/throwError.spec.ts b/src/test/utils/throwError.spec.ts deleted file mode 100644 index 363ebfb..0000000 --- a/src/test/utils/throwError.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { rootFolderNotFound } from '../../utils' - -describe('root folder not found', () => { - it('when access token is provided, error message should contain scope of accessToken', () => { - const token = - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6WyJzY29wZS0xIiwic2NvcGUtMiJdfQ.ktqPL2ulln-8Asa2jSV9QCfDYmQuNk4tNKopxJR5xZs' - let error - try { - rootFolderNotFound('/myProject', 'https://analytium.co.uk', token) - } catch (err) { - error = err.message - } - expect(error).toContain('scope-1') - expect(error).toContain('scope-2') - }) -}) diff --git a/src/types/errors/RootFolderNotFoundError.spec.ts b/src/types/errors/RootFolderNotFoundError.spec.ts new file mode 100644 index 0000000..a27e071 --- /dev/null +++ b/src/types/errors/RootFolderNotFoundError.spec.ts @@ -0,0 +1,40 @@ +import { RootFolderNotFoundError } from './RootFolderNotFoundError' + +describe('RootFolderNotFoundError', () => { + it('when access token is provided, error message should contain the scopes in the token', () => { + const token = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6WyJzY29wZS0xIiwic2NvcGUtMiJdfQ.ktqPL2ulln-8Asa2jSV9QCfDYmQuNk4tNKopxJR5xZs' + + const error = new RootFolderNotFoundError( + '/myProject', + 'https://analytium.co.uk', + token + ) + + expect(error).toBeInstanceOf(RootFolderNotFoundError) + expect(error.message).toContain('scope-1') + expect(error.message).toContain('scope-2') + }) + + it('when access token is not provided, error message should not contain scopes', () => { + const error = new RootFolderNotFoundError( + '/myProject', + 'https://analytium.co.uk' + ) + + expect(error).toBeInstanceOf(RootFolderNotFoundError) + expect(error.message).not.toContain( + 'Your access token contains the following scopes' + ) + }) + + it('should include the folder path and SASDrive URL in the message', () => { + const folderPath = '/myProject' + const serverUrl = 'https://analytium.co.uk' + const error = new RootFolderNotFoundError(folderPath, serverUrl) + + expect(error).toBeInstanceOf(RootFolderNotFoundError) + expect(error.message).toContain(folderPath) + expect(error.message).toContain(`${serverUrl}/SASDrive`) + }) +}) diff --git a/src/types/errors/RootFolderNotFoundError.ts b/src/types/errors/RootFolderNotFoundError.ts new file mode 100644 index 0000000..f5f032e --- /dev/null +++ b/src/types/errors/RootFolderNotFoundError.ts @@ -0,0 +1,24 @@ +import { decodeToken } from '@sasjs/utils/auth' + +export class RootFolderNotFoundError extends Error { + constructor( + parentFolderPath: string, + serverUrl: string, + accessToken?: string + ) { + let message: string = + `Root folder ${parentFolderPath} was not found.` + + `\nPlease check ${serverUrl}/SASDrive.` + + `\nIf the folder DOES exist then it is likely a permission problem.\n` + if (accessToken) { + const decodedToken = decodeToken(accessToken) + let scope = decodedToken.scope + scope = scope.map((element) => '* ' + element) + message += + `Your access token contains the following scopes:\n` + scope.join('\n') + } + super(message) + this.name = 'RootFolderNotFoundError' + Object.setPrototypeOf(this, RootFolderNotFoundError.prototype) + } +} diff --git a/src/types/errors/index.ts b/src/types/errors/index.ts index f8595d4..cca1a97 100644 --- a/src/types/errors/index.ts +++ b/src/types/errors/index.ts @@ -7,3 +7,4 @@ export * from './LoginRequiredError' export * from './NotFoundError' export * from './ErrorResponse' export * from './NoSessionStateError' +export * from './RootFolderNotFoundError' diff --git a/src/utils/index.ts b/src/utils/index.ts index 6501f54..2a05d63 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -15,4 +15,3 @@ export * from './parseWeboutResponse' export * from './fetchLogByChunks' export * from './getValidJson' export * from './parseViyaDebugResponse' -export * from './throwError' diff --git a/src/utils/throwError.ts b/src/utils/throwError.ts deleted file mode 100644 index c054e10..0000000 --- a/src/utils/throwError.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { DecodedToken, decodeToken } from '@sasjs/utils' - -export const rootFolderNotFound = ( - parentFolderPath: string, - serverUrl: string, - accessToken?: string -) => { - let error: string = `Root folder ${parentFolderPath} was not found\nPlease check ${serverUrl}/SASDrive\nIf folder DOES exist then it is likely a permission problem\n` - if (accessToken) { - const decodedToken: DecodedToken = decodeToken(accessToken) - let scope = decodedToken.scope - scope = scope.map((element) => '* ' + element) - error += - `The following scopes are contained in access token:\n` + scope.join('\n') - } - throw new Error(error) -} From 7b7a80c502406a7df9c9eff4b0a787180cfe130b Mon Sep 17 00:00:00 2001 From: Krishna Acondy Date: Tue, 27 Jul 2021 08:20:30 +0100 Subject: [PATCH 23/23] chore(root-folder-not-found): add test --- src/SASViyaApiClient.spec.ts | 51 ++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/SASViyaApiClient.spec.ts diff --git a/src/SASViyaApiClient.spec.ts b/src/SASViyaApiClient.spec.ts new file mode 100644 index 0000000..4063817 --- /dev/null +++ b/src/SASViyaApiClient.spec.ts @@ -0,0 +1,51 @@ +import { Logger, LogLevel } from '@sasjs/utils/logger' +import { RequestClient } from './request/RequestClient' +import { SASViyaApiClient } from './SASViyaApiClient' +import { Folder } from './types' +import { RootFolderNotFoundError } from './types/errors' + +const mockFolder: Folder = { + id: '1', + uri: '/folder', + links: [], + memberCount: 1 +} + +const requestClient = new (>RequestClient)() +const sasViyaApiClient = new SASViyaApiClient( + 'https://test.com', + '/test', + 'test context', + requestClient +) + +describe('SASViyaApiClient', () => { + beforeEach(() => { + ;(process as any).logger = new Logger(LogLevel.Off) + setupMocks() + }) + + it('should throw an error when the root folder is not found on the server', async () => { + jest + .spyOn(requestClient, 'get') + .mockImplementation(() => Promise.reject('Not Found')) + const error = await sasViyaApiClient + .createFolder('test', '/foo') + .catch((e) => e) + expect(error).toBeInstanceOf(RootFolderNotFoundError) + }) +}) + +const setupMocks = () => { + jest + .spyOn(requestClient, 'get') + .mockImplementation(() => + Promise.resolve({ result: mockFolder, etag: '', status: 200 }) + ) + + jest + .spyOn(requestClient, 'post') + .mockImplementation(() => + Promise.resolve({ result: mockFolder, etag: '', status: 200 }) + ) +}