mirror of
https://github.com/sasjs/adapter.git
synced 2026-01-10 13:50:05 +00:00
Compare commits
30 Commits
fixing-sas
...
v2.10.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b2b8e6429 | ||
|
|
5375d0a208 | ||
|
|
f2da84829e | ||
|
|
f172ad66bc | ||
|
|
046c58bb80 | ||
|
|
bf825a4f65 | ||
|
|
d58cff9081 | ||
|
|
7ab1964746 | ||
|
|
b118280a77 | ||
|
|
5317c14d54 | ||
|
|
85fed5cd76 | ||
|
|
6f9196c690 | ||
|
|
2d0a73e74d | ||
|
|
ac8821baec | ||
|
|
0b9284e481 | ||
|
|
7b7a80c502 | ||
|
|
1ace15a308 | ||
|
|
e1b3ef7c8c | ||
| 710056bded | |||
|
|
fb7a0f43e1 | ||
|
|
6c901f1c21 | ||
| 26f008d527 | |||
| 56ebc7be3b | |||
|
|
cb30ed2b98 | ||
|
|
dfbe2d8f94 | ||
|
|
fbaa2327c6 | ||
| 6dd1d47bb2 | |||
| e70a9645ef | |||
| aeabc29e55 | |||
| e1a76bc45a |
@@ -1,24 +1,103 @@
|
|||||||
{
|
{
|
||||||
|
"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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
11
README.md
11
README.md
@@ -236,6 +236,9 @@ If you find this library useful, help us grow our star graph!
|
|||||||

|

|
||||||
|
|
||||||
## Contributors ✨
|
## Contributors ✨
|
||||||
|
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||||
|
[](#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)):
|
||||||
|
|
||||||
@@ -244,7 +247,13 @@ 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://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://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://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>
|
||||||
|
|
||||||
|
|||||||
@@ -47,9 +47,7 @@ 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) =>
|
||||||
@@ -153,9 +151,6 @@ 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
|
||||||
}
|
}
|
||||||
@@ -170,15 +165,9 @@ 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))
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -61,15 +61,14 @@ 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(uploadUrl, formData, undefined, 'application/json', headers)
|
.post(uploadUrl, formData, undefined, 'application/json', headers)
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
// for web approach on Viya
|
|
||||||
if (
|
if (
|
||||||
this.sasjsConfig.debug &&
|
this.sasjsConfig.serverType === ServerType.SasViya &&
|
||||||
(this.sasjsConfig.useComputeApi === null ||
|
this.sasjsConfig.debug
|
||||||
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,
|
||||||
|
|||||||
@@ -10,9 +10,13 @@ import { isUrl } from './utils'
|
|||||||
export class SAS9ApiClient {
|
export class SAS9ApiClient {
|
||||||
private requestClient: Sas9RequestClient
|
private requestClient: Sas9RequestClient
|
||||||
|
|
||||||
constructor(private serverUrl: string, private jobsPath: string) {
|
constructor(
|
||||||
|
private serverUrl: string,
|
||||||
|
private jobsPath: string,
|
||||||
|
allowInsecureRequests: boolean
|
||||||
|
) {
|
||||||
if (serverUrl) isUrl(serverUrl)
|
if (serverUrl) isUrl(serverUrl)
|
||||||
this.requestClient = new Sas9RequestClient(serverUrl, false)
|
this.requestClient = new Sas9RequestClient(serverUrl, allowInsecureRequests)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
51
src/SASViyaApiClient.spec.ts
Normal file
51
src/SASViyaApiClient.spec.ts
Normal file
@@ -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 (<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 })
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
JobDefinition,
|
JobDefinition,
|
||||||
PollOptions
|
PollOptions
|
||||||
} from './types'
|
} from './types'
|
||||||
import { JobExecutionError } from './types/errors'
|
import { JobExecutionError, RootFolderNotFoundError } 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,7 +381,11 @@ export class SASViyaApiClient {
|
|||||||
)
|
)
|
||||||
const newFolderName = `${parentFolderPath.split('/').pop()}`
|
const newFolderName = `${parentFolderPath.split('/').pop()}`
|
||||||
if (newParentFolderPath === '') {
|
if (newParentFolderPath === '') {
|
||||||
throw new Error('Root folder has to be present on the server.')
|
throw new RootFolderNotFoundError(
|
||||||
|
parentFolderPath,
|
||||||
|
this.serverUrl,
|
||||||
|
accessToken
|
||||||
|
)
|
||||||
}
|
}
|
||||||
logger.info(
|
logger.info(
|
||||||
`Creating parent folder:\n'${newFolderName}' in '${newParentFolderPath}'`
|
`Creating parent folder:\n'${newFolderName}' in '${newParentFolderPath}'`
|
||||||
|
|||||||
12
src/SASjs.ts
12
src/SASjs.ts
@@ -749,7 +749,11 @@ 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(serverUrl, this.jobsPath)
|
sasApiClient = new SAS9ApiClient(
|
||||||
|
serverUrl,
|
||||||
|
this.jobsPath,
|
||||||
|
this.sasjsConfig.allowInsecureRequests
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let sasClientConfig: any = null
|
let sasClientConfig: any = null
|
||||||
@@ -944,7 +948,8 @@ 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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -965,7 +970,8 @@ 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(
|
||||||
|
|||||||
@@ -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,14 +154,13 @@ 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')
|
||||||
|
|
||||||
return new Promise(async (resolve, reject) => {
|
|
||||||
if (
|
if (
|
||||||
sessionState === 'pending' ||
|
sessionState === 'pending' ||
|
||||||
sessionState === 'running' ||
|
sessionState === 'running' ||
|
||||||
@@ -192,31 +191,38 @@ export class SessionManager {
|
|||||||
this.printedSessionState.printed = false
|
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 (!sessionState) {
|
||||||
if (RETRY_COUNT < RETRY_LIMIT) {
|
const stateError = new NoSessionStateError(
|
||||||
RETRY_COUNT++
|
|
||||||
|
|
||||||
resolve(this.waitForSession(session, etag, accessToken))
|
|
||||||
} else {
|
|
||||||
reject(
|
|
||||||
new NoSessionStateError(
|
|
||||||
responseStatus,
|
responseStatus,
|
||||||
this.serverUrl + stateLink.href,
|
this.serverUrl + stateLink.href,
|
||||||
session.links.find((l: any) => l.rel === 'log')
|
session.links.find((l: any) => l.rel === 'log')?.href as string
|
||||||
?.href as string
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
!this.loggedErrors.find(
|
||||||
|
(err: NoSessionStateError) =>
|
||||||
|
err.serverResponseStatus === stateError.serverResponseStatus
|
||||||
)
|
)
|
||||||
}
|
) {
|
||||||
|
this.loggedErrors.push(stateError)
|
||||||
|
|
||||||
|
logger.info(stateError.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(sessionState)
|
return await this.waitForSession(session, etag, accessToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loggedErrors = []
|
||||||
|
|
||||||
|
return sessionState
|
||||||
|
} else {
|
||||||
|
throw 'Error while getting session state link.'
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
resolve(sessionState)
|
this.loggedErrors = []
|
||||||
|
|
||||||
|
return sessionState
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getSessionState(
|
private async getSessionState(
|
||||||
|
|||||||
@@ -16,10 +16,11 @@ 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, false)
|
this.requestClient = new Sas9RequestClient(serverUrl, allowInsecureRequests)
|
||||||
}
|
}
|
||||||
|
|
||||||
async execute(sasJob: string, data: any, config: any) {
|
async execute(sasJob: string, data: any, config: any) {
|
||||||
|
|||||||
@@ -107,9 +107,20 @@ export class WebJobExecutor extends BaseJobExecutor {
|
|||||||
this.appendRequest(res, sasJob, config.debug)
|
this.appendRequest(res, sasJob, config.debug)
|
||||||
resolve(jsonResponse)
|
resolve(jsonResponse)
|
||||||
}
|
}
|
||||||
|
if (this.serverType === ServerType.Sas9 && config.debug) {
|
||||||
|
const jsonResponse = parseWeboutResponse(res.result as string)
|
||||||
|
if (jsonResponse === '') {
|
||||||
|
throw new Error(
|
||||||
|
'Valid JSON could not be extracted from response.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
getValidJson(jsonResponse)
|
||||||
this.appendRequest(res, sasJob, config.debug)
|
this.appendRequest(res, sasJob, config.debug)
|
||||||
|
resolve(res.result)
|
||||||
|
}
|
||||||
getValidJson(res.result as string)
|
getValidJson(res.result as string)
|
||||||
|
this.appendRequest(res, sasJob, config.debug)
|
||||||
resolve(res.result)
|
resolve(res.result)
|
||||||
})
|
})
|
||||||
.catch(async (e: Error) => {
|
.catch(async (e: Error) => {
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ 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>
|
||||||
@@ -47,36 +49,91 @@ describe('SessionManager', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('waitForSession', () => {
|
describe('waitForSession', () => {
|
||||||
it('should reject with NoSessionStateError if SAS server did not provide session state', async () => {
|
const session: Session = {
|
||||||
const responseStatus = 304
|
|
||||||
|
|
||||||
mockedAxios.get.mockImplementation(() =>
|
|
||||||
Promise.resolve({ data: '', status: responseStatus })
|
|
||||||
)
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
sessionManager['waitForSession'](
|
|
||||||
{
|
|
||||||
id: 'id',
|
id: 'id',
|
||||||
state: '',
|
state: '',
|
||||||
links: [
|
links: [{ rel: 'state', href: '', uri: '', type: '', method: 'GET' }],
|
||||||
{ rel: 'state', href: '', uri: '', type: '', method: 'GET' }
|
|
||||||
],
|
|
||||||
attributes: {
|
attributes: {
|
||||||
sessionInactiveTimeout: 0
|
sessionInactiveTimeout: 0
|
||||||
},
|
},
|
||||||
creationTimeStamp: ''
|
creationTimeStamp: ''
|
||||||
},
|
}
|
||||||
null,
|
|
||||||
'access_token'
|
beforeEach(() => {
|
||||||
|
;(process as any).logger = new Logger(LogLevel.Off)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should reject with NoSessionStateError if SAS server did not provide session state', async () => {
|
||||||
|
let requestAttempt = 0
|
||||||
|
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...'
|
||||||
)
|
)
|
||||||
).rejects.toEqual(
|
expect((process as any).logger.info).toHaveBeenNthCalledWith(
|
||||||
new NoSessionStateError(
|
2,
|
||||||
responseStatus,
|
`Could not get session state. Server responded with 304 whilst checking state: ${process.env.SERVER_URL}`
|
||||||
process.env.SERVER_URL as string,
|
|
||||||
'logUrl'
|
|
||||||
)
|
)
|
||||||
|
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(() =>
|
||||||
|
Promise.resolve({ data: customSession.state, status: 200 })
|
||||||
|
)
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sessionManager['waitForSession'](customSession, null, 'access_token')
|
||||||
|
).rejects.toContain('Error while getting session state link.')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw an error if could not get session state', async () => {
|
||||||
|
mockedAxios.get.mockImplementation(() => Promise.reject('Mocked error'))
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sessionManager['waitForSession'](session, null, 'access_token')
|
||||||
|
).rejects.toContain('Error while getting session state.')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return session state', async () => {
|
||||||
|
const customSession = JSON.parse(JSON.stringify(session))
|
||||||
|
customSession.state = 'completed'
|
||||||
|
|
||||||
|
mockedAxios.get.mockImplementation(() =>
|
||||||
|
Promise.resolve({ data: customSession.state, status: 200 })
|
||||||
|
)
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sessionManager['waitForSession'](customSession, null, 'access_token')
|
||||||
|
).resolves.toEqual(customSession.state)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
40
src/types/errors/RootFolderNotFoundError.spec.ts
Normal file
40
src/types/errors/RootFolderNotFoundError.spec.ts
Normal file
@@ -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`)
|
||||||
|
})
|
||||||
|
})
|
||||||
24
src/types/errors/RootFolderNotFoundError.ts
Normal file
24
src/types/errors/RootFolderNotFoundError.ts
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,3 +7,4 @@ export * from './LoginRequiredError'
|
|||||||
export * from './NotFoundError'
|
export * from './NotFoundError'
|
||||||
export * from './ErrorResponse'
|
export * from './ErrorResponse'
|
||||||
export * from './NoSessionStateError'
|
export * from './NoSessionStateError'
|
||||||
|
export * from './RootFolderNotFoundError'
|
||||||
|
|||||||
Reference in New Issue
Block a user