1
0
mirror of https://github.com/sasjs/adapter.git synced 2026-04-10 07:53:14 +00:00

Compare commits

..

7 Commits

38 changed files with 42693 additions and 6990 deletions

View File

@@ -1,103 +1,24 @@
{ {
"projectName": "adapter",
"projectOwner": "sasjs",
"repoType": "github",
"repoHost": "https://github.com",
"files": [ "files": [
"README.md" "README.md"
], ],
"imageSize": 100, "imageSize": 100,
"commit": false, "commit": false,
"commitConvention": "angular",
"contributors": [ "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", "login": "medjedovicm",
"name": "Mihajlo Medjedovic", "name": "Mihajlo Medjedovic",
"avatar_url": "https://avatars.githubusercontent.com/u/18329105?v=4", "avatar_url": "https://avatars.githubusercontent.com/u/18329105?v=4",
"profile": "https://github.com/medjedovicm", "profile": "https://github.com/medjedovicm",
"contributions": [ "contributions": [
"code", "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, "contributorsPerLine": 7,
"projectName": "adapter",
"projectOwner": "sasjs",
"repoType": "github",
"repoHost": "https://github.com",
"skipCi": true "skipCi": true
} }

View File

@@ -6,7 +6,7 @@ GREEN="\033[1;32m"
# temporary file which holds the message). # temporary file which holds the message).
commit_message=$(cat "$1") 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" echo "${GREEN} ✔ Commit message meets Conventional Commit standards"
exit 0 exit 0
fi fi

View File

@@ -1,5 +1,4 @@
sasjs-tests/ sasjs-tests/
docs/ docs/
.github/ .github/
*.md CONTRIBUTING.md
*.spec.ts

View File

@@ -172,7 +172,7 @@ Configuration on the client side involves passing an object on startup, which ca
* `serverType` - either `SAS9` or `SASVIYA`. * `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. * `serverUrl` - the location (including http protocol and port) of the SAS Server. Can be omitted, eg if serving directly from the SAS Web Server, or in streaming mode.
* `debug` - if `true` then SAS Logs and extra debug information is returned. * `debug` - if `true` then SAS Logs and extra debug information is returned.
* `useComputeApi` - Only relevant when the serverType is `SASVIYA`. If `true` the [Compute API](#using-the-compute-api) is used. If `false` the [JES API](#using-the-jes-api) is used. If `null` or `undefined` the [Web](#using-jes-web-app) approach is used. * `useComputeApi` - Only relevant when the serverType is `SASVIYA`. If `true` the [Compute API](#using-the-compute-api) is used. If `false` the [JES API](#using-the-jes-api) is used. If `null` or `undefined` the [Web](#using-jes-web-app) approach is used.
* `contextName` - Compute context on which the requests will be called. If missing or not provided, defaults to `Job Execution Compute context`. * `contextName` - Compute context on which the requests will be called. If missing or not provided, defaults to `Job Execution Compute context`.
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). 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).
@@ -236,9 +236,6 @@ If you find this library useful, help us grow our star graph!
![](https://starchart.cc/sasjs/adapter.svg) ![](https://starchart.cc/sasjs/adapter.svg)
## Contributors ✨ ## Contributors ✨
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[![All Contributors](https://img.shields.io/badge/all_contributors-7-orange.svg?style=flat-square)](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
@@ -247,13 +244,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<!-- markdownlint-disable --> <!-- markdownlint-disable -->
<table> <table>
<tr> <tr>
<td align="center"><a href="https://krishna-acondy.io/"><img src="https://avatars.githubusercontent.com/u/2980428?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Krishna Acondy</b></sub></a><br /><a href="https://github.com/sasjs/adapter/commits?author=krishna-acondy" title="Code">💻</a> <a href="#infra-krishna-acondy" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="#blog-krishna-acondy" title="Blogposts">📝</a> <a href="#content-krishna-acondy" title="Content">🖋</a> <a href="#ideas-krishna-acondy" title="Ideas, Planning, & Feedback">🤔</a> <a href="#video-krishna-acondy" title="Videos">📹</a></td> <td align="center"><a href="https://github.com/medjedovicm"><img src="https://avatars.githubusercontent.com/u/18329105?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Mihajlo Medjedovic</b></sub></a><br /><a href="https://github.com/sasjs/adapter/commits?author=medjedovicm" title="Code">💻</a></td>
<td align="center"><a href="https://www.erudicat.com/"><img src="https://avatars.githubusercontent.com/u/25773492?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Yury Shkoda</b></sub></a><br /><a href="https://github.com/sasjs/adapter/commits?author=YuryShkoda" title="Code">💻</a> <a href="#infra-YuryShkoda" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="#ideas-YuryShkoda" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/sasjs/adapter/commits?author=YuryShkoda" title="Tests">⚠️</a> <a href="#video-YuryShkoda" title="Videos">📹</a></td>
<td align="center"><a href="https://github.com/medjedovicm"><img src="https://avatars.githubusercontent.com/u/18329105?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Mihajlo Medjedovic</b></sub></a><br /><a href="https://github.com/sasjs/adapter/commits?author=medjedovicm" title="Code">💻</a> <a href="#infra-medjedovicm" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/sasjs/adapter/commits?author=medjedovicm" title="Tests">⚠️</a> <a href="https://github.com/sasjs/adapter/pulls?q=is%3Apr+reviewed-by%3Amedjedovicm" title="Reviewed Pull Requests">👀</a></td>
<td align="center"><a href="https://github.com/allanbowe"><img src="https://avatars.githubusercontent.com/u/4420615?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Allan Bowe</b></sub></a><br /><a href="https://github.com/sasjs/adapter/commits?author=allanbowe" title="Code">💻</a> <a href="https://github.com/sasjs/adapter/pulls?q=is%3Apr+reviewed-by%3Aallanbowe" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/sasjs/adapter/commits?author=allanbowe" title="Tests">⚠️</a> <a href="#mentoring-allanbowe" title="Mentoring">🧑‍🏫</a> <a href="#maintenance-allanbowe" title="Maintenance">🚧</a></td>
<td align="center"><a href="https://github.com/saadjutt01"><img src="https://avatars.githubusercontent.com/u/8914650?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Muhammad Saad </b></sub></a><br /><a href="https://github.com/sasjs/adapter/commits?author=saadjutt01" title="Code">💻</a> <a href="https://github.com/sasjs/adapter/pulls?q=is%3Apr+reviewed-by%3Asaadjutt01" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/sasjs/adapter/commits?author=saadjutt01" title="Tests">⚠️</a> <a href="#mentoring-saadjutt01" title="Mentoring">🧑‍🏫</a> <a href="#infra-saadjutt01" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center"><a href="https://github.com/sabhas"><img src="https://avatars.githubusercontent.com/u/82647447?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Sabir Hassan</b></sub></a><br /><a href="https://github.com/sasjs/adapter/commits?author=sabhas" title="Code">💻</a> <a href="https://github.com/sasjs/adapter/pulls?q=is%3Apr+reviewed-by%3Asabhas" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/sasjs/adapter/commits?author=sabhas" title="Tests">⚠️</a> <a href="#ideas-sabhas" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/VladislavParhomchik"><img src="https://avatars.githubusercontent.com/u/83717836?v=4?s=100" width="100px;" alt=""/><br /><sub><b>VladislavParhomchik</b></sub></a><br /><a href="https://github.com/sasjs/adapter/commits?author=VladislavParhomchik" title="Tests">⚠️</a> <a href="https://github.com/sasjs/adapter/pulls?q=is%3Apr+reviewed-by%3AVladislavParhomchik" title="Reviewed Pull Requests">👀</a></td>
</tr> </tr>
</table> </table>

View File

@@ -1,16 +0,0 @@
const result = process.versions
if (result && result.node) {
if (parseInt(result.node) < 14) {
console.log(
'\x1b[31m%s\x1b[0m',
`❌ Process failed due to Node Version,\nPlease install and use Node Version >= 14\nYour current Node Version is: ${result.node}`
)
process.exit(1)
}
} else {
console.log(
'\x1b[31m%s\x1b[0m',
'Something went wrong while checking Node version'
)
process.exit(1)
}

18423
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,10 +3,8 @@
"description": "JavaScript adapter for SAS", "description": "JavaScript adapter for SAS",
"homepage": "https://adapter.sasjs.io", "homepage": "https://adapter.sasjs.io",
"scripts": { "scripts": {
"preinstall": "node checkNodeVersion",
"prebuild": "node checkNodeVersion",
"build": "rimraf build && rimraf node && mkdir node && copyfiles -u 1 \"./src/**/*\" ./node && webpack && rimraf build/src && rimraf node", "build": "rimraf build && rimraf node && mkdir node && copyfiles -u 1 \"./src/**/*\" ./node && webpack && rimraf build/src && rimraf node",
"package:lib": "npm run build && copyfiles ./package.json ./checkNodeVersion.js build && cd build && npm version \"5.0.0\" && npm pack", "package:lib": "npm run build && copyfiles ./package.json build && cd build && npm version \"5.0.0\" && npm pack",
"publish:lib": "npm run build && cd build && npm publish", "publish:lib": "npm run build && cd build && npm publish",
"lint:fix": "npx prettier --write \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --write \"sasjs-tests/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\"", "lint:fix": "npx prettier --write \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --write \"sasjs-tests/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\"",
"lint": "npx prettier --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --check \"sasjs-tests/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\"", "lint": "npx prettier --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --check \"sasjs-tests/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\"",
@@ -42,38 +40,41 @@
"devDependencies": { "devDependencies": {
"@types/axios": "^0.14.0", "@types/axios": "^0.14.0",
"@types/form-data": "^2.5.0", "@types/form-data": "^2.5.0",
"@types/jest": "^27.0.1", "@types/jest": "^26.0.24",
"@types/mime": "^2.0.3", "@types/mime": "^2.0.3",
"@types/tough-cookie": "^4.0.1", "@types/tough-cookie": "^4.0.1",
"copyfiles": "^2.4.1", "copyfiles": "^2.4.1",
"cp": "^0.2.0", "cp": "^0.2.0",
"dotenv": "^10.0.0", "dotenv": "^10.0.0",
"jest": "^27.1.0", "jest": "^27.0.6",
"jest-extended": "^0.11.5", "jest-extended": "^0.11.5",
"node-polyfill-webpack-plugin": "^1.1.4", "node-polyfill-webpack-plugin": "^1.1.4",
"path": "^0.12.7", "path": "^0.12.7",
"process": "^0.11.10", "process": "^0.11.10",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"semantic-release": "^17.4.7", "semantic-release": "^17.4.4",
"terser-webpack-plugin": "^5.2.0", "terser-webpack-plugin": "^5.1.4",
"ts-jest": "^27.0.3", "ts-jest": "^27.0.3",
"ts-loader": "^9.2.2", "ts-loader": "^9.2.2",
"tslint": "^6.1.3", "tslint": "^6.1.3",
"tslint-config-prettier": "^1.18.0", "tslint-config-prettier": "^1.18.0",
"typedoc": "^0.21.9", "typedoc": "^0.21.4",
"typedoc-neo-theme": "^1.1.1", "typedoc-neo-theme": "^1.1.1",
"typedoc-plugin-external-module-name": "^4.0.6", "typedoc-plugin-external-module-name": "^4.0.6",
"typescript": "4.3.5", "typescript": "^4.3.5",
"webpack": "^5.44.0", "webpack": "^5.44.0",
"webpack-cli": "^4.7.2" "webpack-cli": "^4.7.2"
}, },
"main": "index.js", "main": "index.js",
"dependencies": { "dependencies": {
"@sasjs/utils": "^2.30.0", "@sasjs/utils": "^2.27.1",
"axios": "^0.21.1", "axios": "^0.21.1",
"axios-cookiejar-support": "^1.0.1", "axios-cookiejar-support": "^1.0.1",
"form-data": "^4.0.0", "form-data": "^4.0.0",
"https": "^1.0.0", "https": "^1.0.0",
"tough-cookie": "^4.0.0" "tough-cookie": "^4.0.0"
},
"engines": {
"node": ">=15"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -23,7 +23,7 @@
"test": "react-scripts test", "test": "react-scripts test",
"eject": "react-scripts eject", "eject": "react-scripts eject",
"update:adapter": "cd .. && npm run package:lib && cd sasjs-tests && npm i ../build/sasjs-adapter-5.0.0.tgz", "update:adapter": "cd .. && npm run package:lib && cd sasjs-tests && npm i ../build/sasjs-adapter-5.0.0.tgz",
"deploy:tests": "rsync -avhe ssh ./build/* --delete sabhas@sas.analytium.co.uk:/var/www/html/sabhas/sasjs-test || npm run deploy:tests-win", "deploy:tests": "rsync -avhe ssh ./build/* --delete $SSH_ACCOUNT:$DEPLOY_PATH || npm run deploy:tests-win",
"deploy:tests-win": "scp %DEPLOY_PATH% ./build/*", "deploy:tests-win": "scp %DEPLOY_PATH% ./build/*",
"deploy": "npm run update:adapter && npm run build && npm run deploy:tests" "deploy": "npm run update:adapter && npm run build && npm run deploy:tests"
}, },

View File

@@ -2,7 +2,7 @@
"userName": "", "userName": "",
"password": "", "password": "",
"sasJsConfig": { "sasJsConfig": {
"serverUrl": "https://sas.analytium.co.uk/", "serverUrl": "",
"appLoc": "/Public/app", "appLoc": "/Public/app",
"serverType": "SASVIYA", "serverType": "SASVIYA",
"debug": false, "debug": false,

View File

@@ -14,16 +14,16 @@ const App = (): ReactElement<{}> => {
useEffect(() => { useEffect(() => {
if (adapter) { if (adapter) {
const testSuites = [ const testSuites = [
// basicTests(adapter, config.userName, config.password), basicTests(adapter, config.userName, config.password),
// sendArrTests(adapter), sendArrTests(adapter),
// sendObjTests(adapter), sendObjTests(adapter),
// specialCaseTests(adapter), specialCaseTests(adapter),
sasjsRequestTests(adapter) sasjsRequestTests(adapter)
] ]
// if (adapter.getSasjsConfig().serverType === 'SASVIYA') { if (adapter.getSasjsConfig().serverType === 'SASVIYA') {
// testSuites.push(computeTests(adapter)) testSuites.push(computeTests(adapter))
// } }
setTestSuites(testSuites) setTestSuites(testSuites)
} }

View File

@@ -47,7 +47,9 @@ export const basicTests = (
'Should fail on first attempt and should log the user in on second attempt', 'Should fail on first attempt and should log the user in on second attempt',
test: async () => { test: async () => {
await adapter.logOut() await adapter.logOut()
await sleep(1000)
await adapter.logIn('invalid', 'invalid') await adapter.logIn('invalid', 'invalid')
await sleep(1000)
return adapter.logIn(userName, password) return adapter.logIn(userName, password)
}, },
assertion: (response: any) => assertion: (response: any) =>
@@ -151,6 +153,9 @@ export const basicTests = (
description: description:
'Should complete successful request with extra attributes present in response', 'Should complete successful request with extra attributes present in response',
test: async () => { test: async () => {
if (adapter.getSasjsConfig().serverType !== 'SASVIYA')
return Promise.resolve('skip')
const config = { const config = {
useComputeApi: false useComputeApi: false
} }
@@ -165,9 +170,15 @@ export const basicTests = (
) )
}, },
assertion: (response: any) => { assertion: (response: any) => {
if (response === 'skip') return true
const responseKeys: any = Object.keys(response) const responseKeys: any = Object.keys(response)
return responseKeys.includes('file') && responseKeys.includes('data') return responseKeys.includes('file') && responseKeys.includes('data')
} }
} }
] ]
}) })
const sleep = (ms: number) => {
return new Promise((resolve) => setTimeout(resolve, ms))
}

View File

@@ -61,30 +61,24 @@ export class FileUploader {
'Content-Type': 'text/plain' 'Content-Type': 'text/plain'
} }
// currently only web approach is supported for file upload
// therefore log is part of response with debug enabled and must be parsed
return this.requestClient return this.requestClient
.post( .post(uploadUrl, formData, undefined, 'application/json', headers)
uploadUrl,
formData,
undefined,
'application/json',
headers,
this.sasjsConfig.debug,
true,
sasJob
)
.then(async (res) => { .then(async (res) => {
// for web approach on Viya
if ( if (
this.sasjsConfig.serverType === ServerType.SasViya && this.sasjsConfig.debug &&
this.sasjsConfig.debug (this.sasjsConfig.useComputeApi === null ||
this.sasjsConfig.useComputeApi === undefined) &&
this.sasjsConfig.serverType === ServerType.SasViya
) { ) {
const jsonResponse = await parseSasViyaDebugResponse( const jsonResponse = await parseSasViyaDebugResponse(
res.result as string, res.result as string,
this.requestClient, this.requestClient,
this.sasjsConfig.serverUrl this.sasjsConfig.serverUrl
) )
return jsonResponse return typeof jsonResponse === 'string'
? getValidJson(jsonResponse)
: jsonResponse
} }
return typeof res.result === 'string' return typeof res.result === 'string'

View File

@@ -10,13 +10,9 @@ import { isUrl } from './utils'
export class SAS9ApiClient { export class SAS9ApiClient {
private requestClient: Sas9RequestClient private requestClient: Sas9RequestClient
constructor( constructor(private serverUrl: string, private jobsPath: string) {
private serverUrl: string,
private jobsPath: string,
allowInsecureRequests: boolean
) {
if (serverUrl) isUrl(serverUrl) if (serverUrl) isUrl(serverUrl)
this.requestClient = new Sas9RequestClient(serverUrl, allowInsecureRequests) this.requestClient = new Sas9RequestClient(serverUrl, false)
} }
/** /**

View File

@@ -1,51 +0,0 @@
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 (<jest.Mock<RequestClient>>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 })
)
}

View File

@@ -11,7 +11,7 @@ import {
JobDefinition, JobDefinition,
PollOptions PollOptions
} from './types' } from './types'
import { JobExecutionError, RootFolderNotFoundError } from './types/errors' import { JobExecutionError } from './types/errors'
import { SessionManager } from './SessionManager' import { SessionManager } from './SessionManager'
import { ContextManager } from './ContextManager' import { ContextManager } from './ContextManager'
import { SasAuthResponse, MacroVar, AuthConfig } from '@sasjs/utils/types' import { SasAuthResponse, MacroVar, AuthConfig } from '@sasjs/utils/types'
@@ -381,11 +381,7 @@ export class SASViyaApiClient {
) )
const newFolderName = `${parentFolderPath.split('/').pop()}` const newFolderName = `${parentFolderPath.split('/').pop()}`
if (newParentFolderPath === '') { if (newParentFolderPath === '') {
throw new RootFolderNotFoundError( throw new Error('Root folder has to be present on the server.')
parentFolderPath,
this.serverUrl,
accessToken
)
} }
logger.info( logger.info(
`Creating parent folder:\n'${newFolderName}' in '${newParentFolderPath}'` `Creating parent folder:\n'${newFolderName}' in '${newParentFolderPath}'`
@@ -782,24 +778,12 @@ export class SASViyaApiClient {
jobResult = await this.requestClient.get<any>( jobResult = await this.requestClient.get<any>(
`${this.serverUrl}${resultLink}/content`, `${this.serverUrl}${resultLink}/content`,
access_token, access_token,
'text/plain', 'text/plain'
{},
debug,
true,
sasJob
) )
} }
if (debug && logLink) { if (debug && logLink) {
log = await this.requestClient log = await this.requestClient
.get<any>( .get<any>(`${this.serverUrl}${logLink.href}/content`, access_token)
`${this.serverUrl}${logLink.href}/content`,
access_token,
'application/json',
{},
debug,
true,
sasJob
)
.then((res: any) => res.result.items.map((i: any) => i.line).join('\n')) .then((res: any) => res.result.items.map((i: any) => i.line).join('\n'))
} }
if (jobStatus === 'failed') { if (jobStatus === 'failed') {

View File

@@ -50,7 +50,6 @@ export default class SASjs {
private sas9JobExecutor: JobExecutor | null = null private sas9JobExecutor: JobExecutor | null = null
constructor(config?: any) { constructor(config?: any) {
console.log('from SASjs constructor')
this.sasjsConfig = { this.sasjsConfig = {
...defaultConfig, ...defaultConfig,
...config ...config
@@ -612,7 +611,6 @@ export default class SASjs {
config.useComputeApi !== null config.useComputeApi !== null
) { ) {
if (config.useComputeApi) { if (config.useComputeApi) {
console.log(615)
return await this.computeJobExecutor!.execute( return await this.computeJobExecutor!.execute(
sasJob, sasJob,
data, data,
@@ -621,11 +619,6 @@ export default class SASjs {
authConfig authConfig
) )
} else { } else {
if (!config.contextName)
config = {
...config,
contextName: 'SAS Job Execution compute context'
}
return await this.jesJobExecutor!.execute( return await this.jesJobExecutor!.execute(
sasJob, sasJob,
data, data,
@@ -756,11 +749,7 @@ export default class SASjs {
) )
sasApiClient.debug = this.sasjsConfig.debug sasApiClient.debug = this.sasjsConfig.debug
} else if (this.sasjsConfig.serverType === ServerType.Sas9) { } else if (this.sasjsConfig.serverType === ServerType.Sas9) {
sasApiClient = new SAS9ApiClient( sasApiClient = new SAS9ApiClient(serverUrl, this.jobsPath)
serverUrl,
this.jobsPath,
this.sasjsConfig.allowInsecureRequests
)
} }
} else { } else {
let sasClientConfig: any = null let sasClientConfig: any = null
@@ -880,20 +869,20 @@ export default class SASjs {
}) })
} }
/**
* this method returns an array of SASjsRequest
* @returns SASjsRequest[]
*/
public getSasRequests() { public getSasRequests() {
console.log('from getSASRequests') const requests = [
const requests = this.requestClient!.getRequests() ...this.webJobExecutor!.getRequests(),
...this.computeJobExecutor!.getRequests(),
...this.jesJobExecutor!.getRequests()
]
const sortedRequests = requests.sort(compareTimestamps) const sortedRequests = requests.sort(compareTimestamps)
console.log('sortedRequests', sortedRequests)
return sortedRequests return sortedRequests
} }
public clearSasRequests() { public clearSasRequests() {
this.requestClient!.clearRequests() this.webJobExecutor!.clearRequests()
this.computeJobExecutor!.clearRequests()
this.jesJobExecutor!.clearRequests()
} }
private setupConfiguration() { private setupConfiguration() {
@@ -955,8 +944,7 @@ export default class SASjs {
else else
this.sas9ApiClient = new SAS9ApiClient( this.sas9ApiClient = new SAS9ApiClient(
this.sasjsConfig.serverUrl, this.sasjsConfig.serverUrl,
this.jobsPath, this.jobsPath
this.sasjsConfig.allowInsecureRequests
) )
} }
@@ -977,8 +965,7 @@ export default class SASjs {
this.sas9JobExecutor = new Sas9JobExecutor( this.sas9JobExecutor = new Sas9JobExecutor(
this.sasjsConfig.serverUrl, this.sasjsConfig.serverUrl,
this.sasjsConfig.serverType!, this.sasjsConfig.serverType!,
this.jobsPath, this.jobsPath
this.sasjsConfig.allowInsecureRequests
) )
this.computeJobExecutor = new ComputeJobExecutor( this.computeJobExecutor = new ComputeJobExecutor(

View File

@@ -5,10 +5,10 @@ import { prefixMessage } from '@sasjs/utils/error'
import { RequestClient } from './request/RequestClient' import { RequestClient } from './request/RequestClient'
const MAX_SESSION_COUNT = 1 const MAX_SESSION_COUNT = 1
const RETRY_LIMIT: number = 3
let RETRY_COUNT: number = 0
export class SessionManager { export class SessionManager {
private loggedErrors: NoSessionStateError[] = []
constructor( constructor(
private serverUrl: string, private serverUrl: string,
private contextName: string, private contextName: string,
@@ -154,75 +154,69 @@ export class SessionManager {
session: Session, session: Session,
etag: string | null, etag: string | null,
accessToken?: string accessToken?: string
): Promise<string> { ) {
const logger = process.logger || console const logger = process.logger || console
let sessionState = session.state let sessionState = session.state
const stateLink = session.links.find((l: any) => l.rel === 'state') const stateLink = session.links.find((l: any) => l.rel === 'state')
if ( return new Promise(async (resolve, reject) => {
sessionState === 'pending' || if (
sessionState === 'running' || sessionState === 'pending' ||
sessionState === '' sessionState === 'running' ||
) { sessionState === ''
if (stateLink) { ) {
if (this.debug && !this.printedSessionState.printed) { if (stateLink) {
logger.info('Polling session status...') if (this.debug && !this.printedSessionState.printed) {
logger.info('Polling session status...')
this.printedSessionState.printed = true this.printedSessionState.printed = true
}
const { result: state, responseStatus: responseStatus } =
await this.getSessionState(
`${this.serverUrl}${stateLink.href}?wait=30`,
etag!,
accessToken
).catch((err) => {
throw prefixMessage(err, 'Error while getting session state.')
})
sessionState = state.trim()
if (this.debug && this.printedSessionState.state !== sessionState) {
logger.info(`Current session state is '${sessionState}'`)
this.printedSessionState.state = sessionState
this.printedSessionState.printed = false
}
if (!sessionState) {
const stateError = new NoSessionStateError(
responseStatus,
this.serverUrl + stateLink.href,
session.links.find((l: any) => l.rel === 'log')?.href as string
)
if (
!this.loggedErrors.find(
(err: NoSessionStateError) =>
err.serverResponseStatus === stateError.serverResponseStatus
)
) {
this.loggedErrors.push(stateError)
logger.info(stateError.message)
} }
return await this.waitForSession(session, etag, accessToken) const { result: state, responseStatus: responseStatus } =
await this.getSessionState(
`${this.serverUrl}${stateLink.href}?wait=30`,
etag!,
accessToken
).catch((err) => {
throw prefixMessage(err, 'Error while getting session state.')
})
sessionState = state.trim()
if (this.debug && this.printedSessionState.state !== sessionState) {
logger.info(`Current session state is '${sessionState}'`)
this.printedSessionState.state = sessionState
this.printedSessionState.printed = false
}
// There is an internal error present in SAS Viya 3.5
// Retry to wait for a session status in such case of SAS internal error
if (!sessionState) {
if (RETRY_COUNT < RETRY_LIMIT) {
RETRY_COUNT++
resolve(this.waitForSession(session, etag, accessToken))
} else {
reject(
new NoSessionStateError(
responseStatus,
this.serverUrl + stateLink.href,
session.links.find((l: any) => l.rel === 'log')
?.href as string
)
)
}
}
resolve(sessionState)
} }
this.loggedErrors = []
return sessionState
} else { } else {
throw 'Error while getting session state link.' resolve(sessionState)
} }
} else { })
this.loggedErrors = []
return sessionState
}
} }
private async getSessionState( private async getSessionState(

View File

@@ -239,15 +239,7 @@ export async function executeScript(
const resultLink = `/compute/sessions/${executionSessionId}/filerefs/_webout/content` const resultLink = `/compute/sessions/${executionSessionId}/filerefs/_webout/content`
jobResult = await requestClient jobResult = await requestClient
.get<any>( .get<any>(resultLink, access_token, 'text/plain')
resultLink,
access_token,
'text/plain',
{},
debug,
true,
jobPath
)
.catch(async (e) => { .catch(async (e) => {
if (e instanceof NotFoundError) { if (e instanceof NotFoundError) {
if (logLink) { if (logLink) {

View File

@@ -124,8 +124,7 @@ export class AuthManager {
if (!isLoggedIn) { if (!isLoggedIn) {
//We will logout to make sure cookies are removed and login form is presented //We will logout to make sure cookies are removed and login form is presented
//Residue can happen in case of session expiration this.logOut()
await this.logOut()
const { result: formResponse } = await this.requestClient.get<string>( const { result: formResponse } = await this.requestClient.get<string>(
this.loginUrl.replace('.do', ''), this.loginUrl.replace('.do', ''),

View File

@@ -35,11 +35,14 @@ export class ComputeJobExecutor extends BaseJobExecutor {
expectWebout expectWebout
) )
.then((response) => { .then((response) => {
console.log('then block of compute job executor') this.appendRequest(response, sasJob, config.debug)
resolve(response.result) resolve(response.result)
}) })
.catch(async (e: Error) => { .catch(async (e: Error) => {
if (e instanceof ComputeJobExecutionError) { if (e instanceof ComputeJobExecutionError) {
this.appendRequest(e, sasJob, config.debug)
reject(new ErrorResponse(e?.message, e)) reject(new ErrorResponse(e?.message, e))
} }

View File

@@ -27,6 +27,8 @@ export class JesJobExecutor extends BaseJobExecutor {
this.sasViyaApiClient this.sasViyaApiClient
?.executeJob(sasJob, config.contextName, config.debug, data, authConfig) ?.executeJob(sasJob, config.contextName, config.debug, data, authConfig)
.then((response: any) => { .then((response: any) => {
this.appendRequest(response, sasJob, config.debug)
let responseObject = {} let responseObject = {}
if (extraResponseAttributes && extraResponseAttributes.length > 0) { if (extraResponseAttributes && extraResponseAttributes.length > 0) {
@@ -47,6 +49,8 @@ export class JesJobExecutor extends BaseJobExecutor {
}) })
.catch(async (e: Error) => { .catch(async (e: Error) => {
if (e instanceof JobExecutionError) { if (e instanceof JobExecutionError) {
this.appendRequest(e, sasJob, config.debug)
reject(new ErrorResponse(e?.message, e)) reject(new ErrorResponse(e?.message, e))
} }

View File

@@ -15,6 +15,8 @@ export interface JobExecutor {
extraResponseAttributes?: ExtraResponseAttributes[] extraResponseAttributes?: ExtraResponseAttributes[]
) => Promise<any> ) => Promise<any>
resendWaitingRequests: () => Promise<void> resendWaitingRequests: () => Promise<void>
getRequests: () => SASjsRequest[]
clearRequests: () => void
} }
export abstract class BaseJobExecutor implements JobExecutor { export abstract class BaseJobExecutor implements JobExecutor {
@@ -44,7 +46,54 @@ export abstract class BaseJobExecutor implements JobExecutor {
return return
} }
getRequests = () => this.requests
clearRequests = () => {
this.requests = []
}
protected appendWaitingRequest(request: ExecuteFunction) { protected appendWaitingRequest(request: ExecuteFunction) {
this.waitingRequests.push(request) this.waitingRequests.push(request)
} }
protected appendRequest(response: any, program: string, debug: boolean) {
let sourceCode = ''
let generatedCode = ''
let sasWork = null
if (debug) {
if (response?.log) {
sourceCode = parseSourceCode(response.log)
generatedCode = parseGeneratedCode(response.log)
if (response?.result) {
sasWork = response.result.WORK
} else {
sasWork = response.log
}
} else if (response?.result) {
sourceCode = parseSourceCode(response.result)
generatedCode = parseGeneratedCode(response.result)
sasWork = response.result.WORK
}
}
const stringifiedResult =
typeof response?.result === 'string'
? response?.result
: JSON.stringify(response?.result, null, 2)
this.requests.push({
logFile: response?.log || stringifiedResult || response,
serviceLink: program,
timestamp: new Date(),
sourceCode,
generatedCode,
SASWORK: sasWork
})
if (this.requests.length > 20) {
this.requests.splice(0, 1)
}
}
} }

View File

@@ -16,11 +16,10 @@ export class Sas9JobExecutor extends BaseJobExecutor {
constructor( constructor(
serverUrl: string, serverUrl: string,
serverType: ServerType, serverType: ServerType,
private jobsPath: string, private jobsPath: string
allowInsecureRequests: boolean
) { ) {
super(serverUrl, serverType) super(serverUrl, serverType)
this.requestClient = new Sas9RequestClient(serverUrl, allowInsecureRequests) this.requestClient = new Sas9RequestClient(serverUrl, false)
} }
async execute(sasJob: string, data: any, config: any) { async execute(sasJob: string, data: any, config: any) {

View File

@@ -2,8 +2,7 @@ import { ServerType } from '@sasjs/utils/types'
import { import {
ErrorResponse, ErrorResponse,
JobExecutionError, JobExecutionError,
LoginRequiredError, LoginRequiredError
WeboutResponseError
} from '../types/errors' } from '../types/errors'
import { generateFileUploadForm } from '../file/generateFileUploadForm' import { generateFileUploadForm } from '../file/generateFileUploadForm'
import { generateTableUploadForm } from '../file/generateTableUploadForm' import { generateTableUploadForm } from '../file/generateTableUploadForm'
@@ -55,21 +54,7 @@ export class WebJobExecutor extends BaseJobExecutor {
apiUrl += jobUri.length > 0 ? '&_job=' + jobUri : '' apiUrl += jobUri.length > 0 ? '&_job=' + jobUri : ''
if (jobUri.length > 0) { apiUrl += config.contextName ? `&_contextname=${config.contextName}` : ''
apiUrl += '&_job=' + jobUri
/**
* Using both _job and _program parameters will cause a conflict in the JES web app, as its not clear whether or not the server should make the extra fetch for the job uri.
* To handle this, we add the extra underscore and recreate the _program variable in the SAS side of the SASjs adapter so it remains available for backend developers.
*/
apiUrl = apiUrl.replace('_program=', '__program=')
}
// if context name exists and is not blank string
// then add _contextname variable in apiUrl
apiUrl +=
config.contextName && !/\s/.test(config.contextName)
? `&_contextname=${config.contextName}`
: ''
} }
let requestParams = { let requestParams = {
@@ -111,38 +96,26 @@ export class WebJobExecutor extends BaseJobExecutor {
} }
const requestPromise = new Promise((resolve, reject) => { const requestPromise = new Promise((resolve, reject) => {
this.requestClient!.post( this.requestClient!.post(apiUrl, formData, undefined)
apiUrl, .then(async (res) => {
formData,
undefined,
'application/json',
{},
config.debug,
true,
sasJob
)
.then(async (res: any) => {
if (this.serverType === ServerType.SasViya && config.debug) { if (this.serverType === ServerType.SasViya && config.debug) {
const jsonResponse = await parseSasViyaDebugResponse( const jsonResponse = await parseSasViyaDebugResponse(
res.result, res.result as string,
this.requestClient, this.requestClient,
this.serverUrl this.serverUrl
) )
this.appendRequest(res, sasJob, config.debug)
resolve(jsonResponse) resolve(jsonResponse)
} }
if (this.serverType === ServerType.Sas9 && config.debug) {
let jsonResponse = res.result this.appendRequest(res, sasJob, config.debug)
if (typeof res.result === 'string')
jsonResponse = parseWeboutResponse(res.result, apiUrl)
getValidJson(jsonResponse)
resolve(res.result)
}
getValidJson(res.result as string) getValidJson(res.result as string)
resolve(res.result) resolve(res.result)
}) })
.catch(async (e: Error) => { .catch(async (e: Error) => {
if (e instanceof JobExecutionError) { if (e instanceof JobExecutionError) {
this.appendRequest(e, sasJob, config.debug)
reject(new ErrorResponse(e?.message, e)) reject(new ErrorResponse(e?.message, e))
} }

View File

@@ -8,11 +8,10 @@ import {
InternalServerError, InternalServerError,
JobExecutionError JobExecutionError
} from '../types/errors' } from '../types/errors'
import { SASjsRequest } from '../types'
import { parseWeboutResponse } from '../utils/parseWeboutResponse' import { parseWeboutResponse } from '../utils/parseWeboutResponse'
import { prefixMessage } from '@sasjs/utils/error' import { prefixMessage } from '@sasjs/utils/error'
import { SAS9AuthError } from '../types/errors/SAS9AuthError' import { SAS9AuthError } from '../types/errors/SAS9AuthError'
import { parseGeneratedCode, parseSourceCode } from '../utils' import { getValidJson } from '../utils'
export interface HttpClient { export interface HttpClient {
get<T>( get<T>(
@@ -48,8 +47,6 @@ export interface HttpClient {
} }
export class RequestClient implements HttpClient { export class RequestClient implements HttpClient {
private requests: SASjsRequest[] = []
protected csrfToken: CsrfToken = { headerName: '', value: '' } protected csrfToken: CsrfToken = { headerName: '', value: '' }
protected fileUploadCsrfToken: CsrfToken | undefined protected fileUploadCsrfToken: CsrfToken | undefined
protected httpClient: AxiosInstance protected httpClient: AxiosInstance
@@ -86,75 +83,12 @@ export class RequestClient implements HttpClient {
return this.httpClient.defaults.baseURL || '' return this.httpClient.defaults.baseURL || ''
} }
/**
* this method returns all requests, an array of SASjsRequest type
* @returns SASjsRequest[]
*/
public getRequests = () => this.requests
/**
* this method clears the requests array, i.e set to empty
*/
public clearRequests = () => {
this.requests = []
}
/**
* this method appends the response from sasjs request to requests array
* @param response - response from sasjs request
* @param program - name of program
* @param debug - a boolean that indicates whether debug was enabled or not
*/
public appendRequest = (response: any, program: string, debug: boolean) => {
console.log('from appendRequest')
let sourceCode = ''
let generatedCode = ''
let sasWork = null
if (debug) {
if (response?.log) {
sourceCode = parseSourceCode(response.log)
generatedCode = parseGeneratedCode(response.log)
if (response?.result) {
sasWork = response.result.WORK
} else {
sasWork = response.log
}
} else if (response?.result) {
sourceCode = parseSourceCode(response.result)
generatedCode = parseGeneratedCode(response.result)
sasWork = response.result.WORK
}
}
const stringifiedResult =
typeof response?.result === 'string'
? response?.result
: JSON.stringify(response?.result, null, 2)
this.requests.push({
logFile: response?.log || stringifiedResult || response,
serviceLink: program,
timestamp: new Date(),
sourceCode,
generatedCode,
SASWORK: sasWork
})
if (this.requests.length > 20) {
this.requests.splice(0, 1)
}
}
public async get<T>( public async get<T>(
url: string, url: string,
accessToken: string | undefined, accessToken: string | undefined,
contentType: string = 'application/json', contentType: string = 'application/json',
overrideHeaders: { [key: string]: string | number } = {}, overrideHeaders: { [key: string]: string | number } = {},
debug: boolean = false, debug: boolean = false
captureRequest: boolean = false,
sasJob: string = ''
): Promise<{ result: T; etag: string; status: number }> { ): Promise<{ result: T; etag: string; status: number }> {
const headers = { const headers = {
...this.getHeaders(accessToken, contentType), ...this.getHeaders(accessToken, contentType),
@@ -174,11 +108,8 @@ export class RequestClient implements HttpClient {
.get<T>(url, requestConfig) .get<T>(url, requestConfig)
.then((response) => { .then((response) => {
throwIfError(response) throwIfError(response)
const responseToReturn = this.parseResponse<T>(response)
if (captureRequest) { return this.parseResponse<T>(response)
this.appendRequest(responseToReturn, sasJob, debug)
}
return responseToReturn
}) })
.catch(async (e) => { .catch(async (e) => {
return await this.handleError( return await this.handleError(
@@ -204,10 +135,7 @@ export class RequestClient implements HttpClient {
data: any, data: any,
accessToken: string | undefined, accessToken: string | undefined,
contentType = 'application/json', contentType = 'application/json',
overrideHeaders: { [key: string]: string | number } = {}, overrideHeaders: { [key: string]: string | number } = {}
debug: boolean = false,
captureRequest: boolean = false,
sasJob: string = ''
): Promise<{ result: T; etag: string }> { ): Promise<{ result: T; etag: string }> {
const headers = { const headers = {
...this.getHeaders(accessToken, contentType), ...this.getHeaders(accessToken, contentType),
@@ -218,11 +146,7 @@ export class RequestClient implements HttpClient {
.post<T>(url, data, { headers, withCredentials: true }) .post<T>(url, data, { headers, withCredentials: true })
.then((response) => { .then((response) => {
throwIfError(response) throwIfError(response)
const responseToReturn = this.parseResponse<T>(response) return this.parseResponse<T>(response)
if (captureRequest) {
this.appendRequest(responseToReturn, sasJob, debug)
}
return responseToReturn
}) })
.catch(async (e) => { .catch(async (e) => {
return await this.handleError(e, () => return await this.handleError(e, () =>
@@ -505,7 +429,13 @@ export class RequestClient implements HttpClient {
} }
} catch { } catch {
try { try {
parsedResponse = JSON.parse(parseWeboutResponse(response.data)) const weboutResponse = parseWeboutResponse(response.data)
if (weboutResponse === '') {
throw new Error('Valid JSON could not be extracted from response.')
}
const jsonResponse = getValidJson(weboutResponse)
parsedResponse = jsonResponse
} catch { } catch {
parsedResponse = response.data parsedResponse = response.data
} }

View File

@@ -34,8 +34,7 @@ const prepareFilesAndParams = () => {
describe('FileUploader', () => { describe('FileUploader', () => {
const config: SASjsConfig = { const config: SASjsConfig = {
...new SASjsConfig(), ...new SASjsConfig(),
appLoc: '/sample/apploc', appLoc: '/sample/apploc'
debug: false
} }
const fileUploader = new FileUploader( const fileUploader = new FileUploader(

View File

@@ -3,8 +3,6 @@ import { RequestClient } from '../request/RequestClient'
import { NoSessionStateError } from '../types/errors' import { NoSessionStateError } from '../types/errors'
import * as dotenv from 'dotenv' import * as dotenv from 'dotenv'
import axios from 'axios' import axios from 'axios'
import { Logger, LogLevel } from '@sasjs/utils'
import { Session } from '../types'
jest.mock('axios') jest.mock('axios')
const mockedAxios = axios as jest.Mocked<typeof axios> const mockedAxios = axios as jest.Mocked<typeof axios>
@@ -49,91 +47,36 @@ describe('SessionManager', () => {
}) })
describe('waitForSession', () => { describe('waitForSession', () => {
const session: Session = {
id: 'id',
state: '',
links: [{ rel: 'state', href: '', uri: '', type: '', method: 'GET' }],
attributes: {
sessionInactiveTimeout: 0
},
creationTimeStamp: ''
}
beforeEach(() => {
;(process as any).logger = new Logger(LogLevel.Off)
})
it('should reject with NoSessionStateError if SAS server did not provide session state', async () => { it('should reject with NoSessionStateError if SAS server did not provide session state', async () => {
let requestAttempt = 0 const responseStatus = 304
const requestAttemptLimit = 10
const sessionState = 'idle'
mockedAxios.get.mockImplementation(() => {
requestAttempt += 1
if (requestAttempt >= requestAttemptLimit) {
return Promise.resolve({ data: sessionState, status: 200 })
}
return Promise.resolve({ data: '', status: 304 })
})
jest.spyOn((process as any).logger, 'info')
sessionManager.debug = true
await expect(
sessionManager['waitForSession'](session, null, 'access_token')
).resolves.toEqual(sessionState)
expect(mockedAxios.get).toHaveBeenCalledTimes(requestAttemptLimit)
expect((process as any).logger.info).toHaveBeenCalledTimes(3)
expect((process as any).logger.info).toHaveBeenNthCalledWith(
1,
'Polling session status...'
)
expect((process as any).logger.info).toHaveBeenNthCalledWith(
2,
`Could not get session state. Server responded with 304 whilst checking state: ${process.env.SERVER_URL}`
)
expect((process as any).logger.info).toHaveBeenNthCalledWith(
3,
`Current session state is '${sessionState}'`
)
})
it('should throw an error if there is no session link', async () => {
const customSession = JSON.parse(JSON.stringify(session))
customSession.links = []
mockedAxios.get.mockImplementation(() => mockedAxios.get.mockImplementation(() =>
Promise.resolve({ data: customSession.state, status: 200 }) Promise.resolve({ data: '', status: responseStatus })
) )
await expect( await expect(
sessionManager['waitForSession'](customSession, null, 'access_token') sessionManager['waitForSession'](
).rejects.toContain('Error while getting session state link.') {
}) id: 'id',
state: '',
it('should throw an error if could not get session state', async () => { links: [
mockedAxios.get.mockImplementation(() => Promise.reject('Mocked error')) { rel: 'state', href: '', uri: '', type: '', method: 'GET' }
],
await expect( attributes: {
sessionManager['waitForSession'](session, null, 'access_token') sessionInactiveTimeout: 0
).rejects.toContain('Error while getting session state.') },
}) creationTimeStamp: ''
},
it('should return session state', async () => { null,
const customSession = JSON.parse(JSON.stringify(session)) 'access_token'
customSession.state = 'completed' )
).rejects.toEqual(
mockedAxios.get.mockImplementation(() => new NoSessionStateError(
Promise.resolve({ data: customSession.state, status: 200 }) responseStatus,
process.env.SERVER_URL as string,
'logUrl'
)
) )
await expect(
sessionManager['waitForSession'](customSession, null, 'access_token')
).resolves.toEqual(customSession.state)
}) })
}) })
}) })

View File

@@ -1,5 +1,4 @@
import { getValidJson } from '../../utils' import { getValidJson } from '../../utils'
import { JsonParseArrayError, InvalidJsonError } from '../../types/errors'
describe('jsonValidator', () => { describe('jsonValidator', () => {
it('should not throw an error with a valid json', () => { it('should not throw an error with a valid json', () => {
@@ -20,31 +19,23 @@ describe('jsonValidator', () => {
it('should throw an error with an invalid json', () => { it('should throw an error with an invalid json', () => {
const json = `{\"test\":\"test\"\"test2\":\"test\"}` const json = `{\"test\":\"test\"\"test2\":\"test\"}`
const test = () => { let errorThrown = false
try {
getValidJson(json) getValidJson(json)
} catch (error) {
errorThrown = true
} }
expect(test).toThrowError(InvalidJsonError) expect(errorThrown).toBe(true)
}) })
it('should throw an error when an array is passed', () => { it('should throw an error when an array is passed', () => {
const array = ['hello', 'world'] const array = ['hello', 'world']
const test = () => { let errorThrown = false
try {
getValidJson(array) getValidJson(array)
} catch (error) {
errorThrown = true
} }
expect(test).toThrow(JsonParseArrayError) expect(errorThrown).toBe(true)
})
it('should throw an error when null is passed', () => {
const test = () => {
getValidJson(null as any)
}
expect(test).toThrow(InvalidJsonError)
})
it('should throw an error when undefined is passed', () => {
const test = () => {
getValidJson(undefined as any)
}
expect(test).toThrow(InvalidJsonError)
}) })
}) })

View File

@@ -1,7 +0,0 @@
export class InvalidJsonError extends Error {
constructor() {
super('Error: invalid Json string')
this.name = 'InvalidJsonError'
Object.setPrototypeOf(this, InvalidJsonError.prototype)
}
}

View File

@@ -1,7 +0,0 @@
export class JsonParseArrayError extends Error {
constructor() {
super('Can not parse array object to json.')
this.name = 'JsonParseArrayError'
Object.setPrototypeOf(this, JsonParseArrayError.prototype)
}
}

View File

@@ -1,40 +0,0 @@
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`)
})
})

View File

@@ -1,24 +0,0 @@
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)
}
}

View File

@@ -1,7 +0,0 @@
export class WeboutResponseError extends Error {
constructor(public url: string) {
super(`Error: error while parsing response from ${url}`)
this.name = 'WeboutResponseError'
Object.setPrototypeOf(this, WeboutResponseError.prototype)
}
}

View File

@@ -7,7 +7,3 @@ export * from './LoginRequiredError'
export * from './NotFoundError' export * from './NotFoundError'
export * from './ErrorResponse' export * from './ErrorResponse'
export * from './NoSessionStateError' export * from './NoSessionStateError'
export * from './RootFolderNotFoundError'
export * from './JsonParseArrayError'
export * from './WeboutResponseError'
export * from './InvalidJsonError'

View File

@@ -1,20 +1,16 @@
import { JsonParseArrayError, InvalidJsonError } from '../types/errors'
/** /**
* 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. * 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. * @param str - string to check.
*/ */
export const getValidJson = (str: string | object) => { export const getValidJson = (str: string | object) => {
try { try {
if (str === null || str === undefined) throw new InvalidJsonError() if (Array.isArray(str)) {
throw new Error('Can not parse array object to json.')
if (Array.isArray(str)) throw new JsonParseArrayError() }
if (typeof str === 'object') return str if (typeof str === 'object') return str
return JSON.parse(str) return JSON.parse(str)
} catch (e) { } catch (e) {
if (e instanceof JsonParseArrayError) throw e throw new Error('Invalid JSON response.')
throw new InvalidJsonError()
} }
} }

View File

@@ -1,5 +1,4 @@
import { RequestClient } from '../request/RequestClient' import { RequestClient } from '../request/RequestClient'
import { getValidJson } from '../utils'
/** /**
* When querying a Viya job using the Web approach (as opposed to using the APIs) with _DEBUG enabled, * When querying a Viya job using the Web approach (as opposed to using the APIs) with _DEBUG enabled,
@@ -26,5 +25,5 @@ export const parseSasViyaDebugResponse = async (
return requestClient return requestClient
.get(serverUrl + jsonUrl, undefined) .get(serverUrl + jsonUrl, undefined)
.then((res: any) => getValidJson(res.result)) .then((res) => res.result)
} }

View File

@@ -1,6 +1,4 @@
import { WeboutResponseError } from '../types/errors' export const parseWeboutResponse = (response: string) => {
export const parseWeboutResponse = (response: string, url?: string) => {
let sasResponse = '' let sasResponse = ''
if (response.includes('>>weboutBEGIN<<')) { if (response.includes('>>weboutBEGIN<<')) {
@@ -9,7 +7,6 @@ export const parseWeboutResponse = (response: string, url?: string) => {
.split('>>weboutBEGIN<<')[1] .split('>>weboutBEGIN<<')[1]
.split('>>weboutEND<<')[0] .split('>>weboutEND<<')[0]
} catch (e) { } catch (e) {
if (url) throw new WeboutResponseError(url)
sasResponse = '' sasResponse = ''
console.error(e) console.error(e)
} }