mirror of
https://github.com/sasjs/adapter.git
synced 2025-12-12 09:44:36 +00:00
Compare commits
89 Commits
v2.9.0
...
debug-sasj
| Author | SHA1 | Date | |
|---|---|---|---|
| 2c6198ae25 | |||
| 73f50c0435 | |||
| 5ee57f3d07 | |||
|
|
ed72c5c48c | ||
| 146b0715bf | |||
| dfc1d567a5 | |||
| 779200f5fc | |||
| cf4c4cfca9 | |||
|
|
ad4eead4ca | ||
|
|
aa9383a483 | ||
|
|
ba105f609c | ||
|
|
b831b93133 | ||
|
|
0c3aab673a | ||
| 33e7564e8f | |||
| 77c4c473c1 | |||
| 47ff1a2293 | |||
|
|
ffae344476 | ||
|
|
4f62cd0148 | ||
| bd92c1925e | |||
|
|
6c29d7823b | ||
| 3c9f133374 | |||
| e72195ca5d | |||
| 3e7ddf59b4 | |||
| cd67fb38dc | |||
| 78149e6c54 | |||
| 63e220c5be | |||
| 8464e506e0 | |||
| 0bc69401e5 | |||
| 47fe7686cb | |||
|
|
dd2b3671fd | ||
| bd03b2b06d | |||
|
|
2b2b8e6429 | ||
|
|
5375d0a208 | ||
|
|
f2da84829e | ||
|
|
f172ad66bc | ||
|
|
046c58bb80 | ||
|
|
bf825a4f65 | ||
|
|
d58cff9081 | ||
|
|
7ab1964746 | ||
|
|
b118280a77 | ||
|
|
5317c14d54 | ||
|
|
85fed5cd76 | ||
|
|
6f9196c690 | ||
|
|
2d0a73e74d | ||
|
|
ac8821baec | ||
|
|
0b9284e481 | ||
|
|
7b7a80c502 | ||
|
|
1ace15a308 | ||
|
|
e1b3ef7c8c | ||
| 710056bded | |||
|
|
fb7a0f43e1 | ||
|
|
6c901f1c21 | ||
| 26f008d527 | |||
| 56ebc7be3b | |||
|
|
0ea66f6d37 | ||
|
|
cb30ed2b98 | ||
|
|
dfbe2d8f94 | ||
|
|
eac9da22bf | ||
|
|
626fc2e15f | ||
|
|
87e2edbd6c | ||
|
|
7cf681bea3 | ||
|
|
281a145bef | ||
|
|
15d5f9ec91 | ||
|
|
0a6c5a0ec4 | ||
|
|
2a9526d056 | ||
|
|
c2ff28c323 | ||
|
|
fbaa2327c6 | ||
|
|
50710ee1df | ||
|
|
062ba91c17 | ||
| 6dd1d47bb2 | |||
| e70a9645ef | |||
| aeabc29e55 | |||
|
|
9600fa2512 | ||
|
|
7951817480 | ||
|
|
405eea1d6c | ||
|
|
e3f189eed4 | ||
|
|
0bb42c5e3c | ||
|
|
c02eac196e | ||
|
|
3fb0d863e9 | ||
|
|
6d573d3897 | ||
|
|
33280d7a5b | ||
| e1a76bc45a | |||
| 85e5ade93a | |||
| 4a61fb8f7f | |||
| 5347aeba09 | |||
|
|
7ac7c5e52b | ||
| 5098342dfe | |||
| c69be8ffc3 | |||
| 69999d8e8b |
103
.all-contributorsrc
Normal file
103
.all-contributorsrc
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
{
|
||||||
|
"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,
|
||||||
|
"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
|
||||||
|
|||||||
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [12.x]
|
node-version: [15.x]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
sasjs-tests/
|
sasjs-tests/
|
||||||
docs/
|
docs/
|
||||||
.github/
|
.github/
|
||||||
CONTRIBUTING.md
|
*.md
|
||||||
|
*.spec.ts
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ What code changes have been made to achieve the intent.
|
|||||||
|
|
||||||
## Checks
|
## 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-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)).
|
- [ ] 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
|
||||||
|
|||||||
31
README.md
31
README.md
@@ -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).
|
||||||
@@ -234,3 +234,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!
|
If you find this library useful, help us grow our star graph!
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
## 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)):
|
||||||
|
|
||||||
|
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||||
|
<!-- prettier-ignore-start -->
|
||||||
|
<!-- markdownlint-disable -->
|
||||||
|
<table>
|
||||||
|
<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://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>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- markdownlint-restore -->
|
||||||
|
<!-- prettier-ignore-end -->
|
||||||
|
|
||||||
|
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||||
|
|
||||||
|
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|
||||||
|
|||||||
16
checkNodeVersion.js
Normal file
16
checkNodeVersion.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
18435
package-lock.json
generated
18435
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
18
package.json
18
package.json
@@ -3,8 +3,10 @@
|
|||||||
"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 build && cd build && npm version \"5.0.0\" && npm pack",
|
"package:lib": "npm run build && copyfiles ./package.json ./checkNodeVersion.js 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}\"",
|
||||||
@@ -40,34 +42,34 @@
|
|||||||
"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": "^26.0.24",
|
"@types/jest": "^27.0.1",
|
||||||
"@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.0.6",
|
"jest": "^27.1.0",
|
||||||
"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.4",
|
"semantic-release": "^17.4.7",
|
||||||
"terser-webpack-plugin": "^5.1.4",
|
"terser-webpack-plugin": "^5.2.0",
|
||||||
"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.4",
|
"typedoc": "^0.21.9",
|
||||||
"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.25.4",
|
"@sasjs/utils": "^2.30.0",
|
||||||
"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",
|
||||||
|
|||||||
30314
sasjs-tests/package-lock.json
generated
30314
sasjs-tests/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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 $SSH_ACCOUNT:$DEPLOY_PATH || npm run deploy:tests-win",
|
"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-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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"userName": "",
|
"userName": "",
|
||||||
"password": "",
|
"password": "",
|
||||||
"sasJsConfig": {
|
"sasJsConfig": {
|
||||||
"serverUrl": "",
|
"serverUrl": "https://sas.analytium.co.uk/",
|
||||||
"appLoc": "/Public/app",
|
"appLoc": "/Public/app",
|
||||||
"serverType": "SASVIYA",
|
"serverType": "SASVIYA",
|
||||||
"debug": false,
|
"debug": false,
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { isUrl } from './utils'
|
import { isUrl, getValidJson, parseSasViyaDebugResponse } from './utils'
|
||||||
import { UploadFile } from './types/UploadFile'
|
import { UploadFile } from './types/UploadFile'
|
||||||
import { ErrorResponse, LoginRequiredError } from './types/errors'
|
import { ErrorResponse, LoginRequiredError } from './types/errors'
|
||||||
import { RequestClient } from './request/RequestClient'
|
import { RequestClient } from './request/RequestClient'
|
||||||
@@ -61,15 +61,36 @@ 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(
|
||||||
.then((res) => {
|
uploadUrl,
|
||||||
let result
|
formData,
|
||||||
|
undefined,
|
||||||
|
'application/json',
|
||||||
|
headers,
|
||||||
|
this.sasjsConfig.debug,
|
||||||
|
true,
|
||||||
|
sasJob
|
||||||
|
)
|
||||||
|
.then(async (res) => {
|
||||||
|
if (
|
||||||
|
this.sasjsConfig.serverType === ServerType.SasViya &&
|
||||||
|
this.sasjsConfig.debug
|
||||||
|
) {
|
||||||
|
const jsonResponse = await parseSasViyaDebugResponse(
|
||||||
|
res.result as string,
|
||||||
|
this.requestClient,
|
||||||
|
this.sasjsConfig.serverUrl
|
||||||
|
)
|
||||||
|
return jsonResponse
|
||||||
|
}
|
||||||
|
|
||||||
result =
|
return typeof res.result === 'string'
|
||||||
typeof res.result === 'string' ? JSON.parse(res.result) : res.result
|
? getValidJson(res.result)
|
||||||
|
: res.result
|
||||||
|
|
||||||
return result
|
|
||||||
//TODO: append to SASjs requests
|
//TODO: append to SASjs requests
|
||||||
})
|
})
|
||||||
.catch((err: Error) => {
|
.catch((err: Error) => {
|
||||||
|
|||||||
@@ -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}'`
|
||||||
@@ -778,12 +782,24 @@ 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>(`${this.serverUrl}${logLink.href}/content`, access_token)
|
.get<any>(
|
||||||
|
`${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') {
|
||||||
|
|||||||
54
src/SASjs.ts
54
src/SASjs.ts
@@ -50,6 +50,7 @@ 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
|
||||||
@@ -544,11 +545,22 @@ export default class SASjs {
|
|||||||
* Process). Is prepended at runtime with the value of `appLoc`.
|
* 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 files - array of files to be uploaded, including File object and file name.
|
||||||
* @param params - request URL parameters.
|
* @param params - request URL parameters.
|
||||||
|
* @param overrideSasjsConfig - object to override existing config (optional)
|
||||||
*/
|
*/
|
||||||
public uploadFile(sasJob: string, files: UploadFile[], params: any) {
|
public uploadFile(
|
||||||
const fileUploader =
|
sasJob: string,
|
||||||
this.fileUploader ||
|
files: UploadFile[],
|
||||||
new FileUploader(this.sasjsConfig, this.jobsPath, this.requestClient!)
|
params: any,
|
||||||
|
overrideSasjsConfig?: any
|
||||||
|
) {
|
||||||
|
const fileUploader = overrideSasjsConfig
|
||||||
|
? new FileUploader(
|
||||||
|
{ ...this.sasjsConfig, ...overrideSasjsConfig },
|
||||||
|
this.jobsPath,
|
||||||
|
this.requestClient!
|
||||||
|
)
|
||||||
|
: this.fileUploader ||
|
||||||
|
new FileUploader(this.sasjsConfig, this.jobsPath, this.requestClient!)
|
||||||
|
|
||||||
return fileUploader.uploadFile(sasJob, files, params)
|
return fileUploader.uploadFile(sasJob, files, params)
|
||||||
}
|
}
|
||||||
@@ -600,6 +612,7 @@ 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,
|
||||||
@@ -608,6 +621,11 @@ 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,
|
||||||
@@ -738,7 +756,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
|
||||||
@@ -858,20 +880,20 @@ export default class SASjs {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* this method returns an array of SASjsRequest
|
||||||
|
* @returns SASjsRequest[]
|
||||||
|
*/
|
||||||
public getSasRequests() {
|
public getSasRequests() {
|
||||||
const requests = [
|
console.log('from getSASRequests')
|
||||||
...this.webJobExecutor!.getRequests(),
|
const requests = this.requestClient!.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.webJobExecutor!.clearRequests()
|
this.requestClient!.clearRequests()
|
||||||
this.computeJobExecutor!.clearRequests()
|
|
||||||
this.jesJobExecutor!.clearRequests()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupConfiguration() {
|
private setupConfiguration() {
|
||||||
@@ -933,7 +955,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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -954,7 +977,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,69 +154,75 @@ 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' ||
|
sessionState === ''
|
||||||
sessionState === ''
|
) {
|
||||||
) {
|
if (stateLink) {
|
||||||
if (stateLink) {
|
if (this.debug && !this.printedSessionState.printed) {
|
||||||
if (this.debug && !this.printedSessionState.printed) {
|
logger.info('Polling session status...')
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loggedErrors = []
|
||||||
|
|
||||||
|
return sessionState
|
||||||
} else {
|
} else {
|
||||||
resolve(sessionState)
|
throw 'Error while getting session state link.'
|
||||||
}
|
}
|
||||||
})
|
} else {
|
||||||
|
this.loggedErrors = []
|
||||||
|
|
||||||
|
return sessionState
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getSessionState(
|
private async getSessionState(
|
||||||
|
|||||||
@@ -239,7 +239,15 @@ 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>(resultLink, access_token, 'text/plain')
|
.get<any>(
|
||||||
|
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) {
|
||||||
|
|||||||
17
src/api/viya/getFileStream.ts
Normal file
17
src/api/viya/getFileStream.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
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 path = require('path')
|
||||||
|
const logFilePath = path.join(filePath || process.cwd(), logFileName)
|
||||||
|
return await createWriteStream(logFilePath)
|
||||||
|
} else {
|
||||||
|
return await createWriteStream(logPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,11 +3,8 @@ import { Job, PollOptions } from '../..'
|
|||||||
import { getTokens } from '../../auth/getTokens'
|
import { getTokens } from '../../auth/getTokens'
|
||||||
import { RequestClient } from '../../request/RequestClient'
|
import { RequestClient } from '../../request/RequestClient'
|
||||||
import { JobStatePollError } from '../../types/errors'
|
import { JobStatePollError } from '../../types/errors'
|
||||||
import { generateTimestamp } from '@sasjs/utils/time'
|
import { Link, WriteStream } from '../../types'
|
||||||
import { saveLog } from './saveLog'
|
import { isNode } from '../../utils'
|
||||||
import { createWriteStream } from '@sasjs/utils/file'
|
|
||||||
import { WriteStream } from 'fs'
|
|
||||||
import { Link } from '../../types'
|
|
||||||
|
|
||||||
export async function pollJobState(
|
export async function pollJobState(
|
||||||
requestClient: RequestClient,
|
requestClient: RequestClient,
|
||||||
@@ -21,11 +18,14 @@ export async function pollJobState(
|
|||||||
let pollInterval = 300
|
let pollInterval = 300
|
||||||
let maxPollCount = 1000
|
let maxPollCount = 1000
|
||||||
|
|
||||||
if (pollOptions) {
|
const defaultPollOptions: PollOptions = {
|
||||||
pollInterval = pollOptions.pollInterval || pollInterval
|
maxPollCount,
|
||||||
maxPollCount = pollOptions.maxPollCount || maxPollCount
|
pollInterval,
|
||||||
|
streamLog: false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pollOptions = { ...defaultPollOptions, ...(pollOptions || {}) }
|
||||||
|
|
||||||
const stateLink = postedJob.links.find((l: any) => l.rel === 'state')
|
const stateLink = postedJob.links.find((l: any) => l.rel === 'state')
|
||||||
if (!stateLink) {
|
if (!stateLink) {
|
||||||
throw new Error(`Job state link was not found.`)
|
throw new Error(`Job state link was not found.`)
|
||||||
@@ -52,15 +52,12 @@ export async function pollJobState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let logFileStream
|
let logFileStream
|
||||||
if (pollOptions?.streamLog) {
|
if (pollOptions.streamLog && isNode()) {
|
||||||
const logFileName = `${postedJob.name || 'job'}-${generateTimestamp()}.log`
|
const { getFileStream } = require('./getFileStream')
|
||||||
const logFilePath = `${
|
logFileStream = await getFileStream(postedJob, pollOptions.logFolderPath)
|
||||||
pollOptions?.logFolderPath || process.cwd()
|
|
||||||
}/${logFileName}`
|
|
||||||
|
|
||||||
logFileStream = await createWriteStream(logFilePath)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Poll up to the first 100 times with the specified poll interval
|
||||||
let result = await doPoll(
|
let result = await doPoll(
|
||||||
requestClient,
|
requestClient,
|
||||||
postedJob,
|
postedJob,
|
||||||
@@ -68,14 +65,18 @@ export async function pollJobState(
|
|||||||
debug,
|
debug,
|
||||||
pollCount,
|
pollCount,
|
||||||
authConfig,
|
authConfig,
|
||||||
pollOptions,
|
{
|
||||||
|
...pollOptions,
|
||||||
|
maxPollCount:
|
||||||
|
pollOptions.maxPollCount <= 100 ? pollOptions.maxPollCount : 100
|
||||||
|
},
|
||||||
logFileStream
|
logFileStream
|
||||||
)
|
)
|
||||||
|
|
||||||
currentState = result.state
|
currentState = result.state
|
||||||
pollCount = result.pollCount
|
pollCount = result.pollCount
|
||||||
|
|
||||||
if (!needsRetry(currentState) || pollCount >= maxPollCount) {
|
if (!needsRetry(currentState) || pollCount >= pollOptions.maxPollCount) {
|
||||||
return currentState
|
return currentState
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,7 +185,7 @@ const doPoll = async (
|
|||||||
throw new Error(`Job state link was not found.`)
|
throw new Error(`Job state link was not found.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
while (needsRetry(state) && pollCount <= 100 && pollCount <= maxPollCount) {
|
while (needsRetry(state) && pollCount <= maxPollCount) {
|
||||||
state = await getJobState(
|
state = await getJobState(
|
||||||
requestClient,
|
requestClient,
|
||||||
postedJob,
|
postedJob,
|
||||||
@@ -214,14 +215,17 @@ const doPoll = async (
|
|||||||
|
|
||||||
const endLogLine = job.logStatistics?.lineCount ?? 1000000
|
const endLogLine = job.logStatistics?.lineCount ?? 1000000
|
||||||
|
|
||||||
await saveLog(
|
const { saveLog } = isNode() ? require('./saveLog') : { saveLog: null }
|
||||||
postedJob,
|
if (saveLog) {
|
||||||
requestClient,
|
await saveLog(
|
||||||
startLogLine,
|
postedJob,
|
||||||
endLogLine,
|
requestClient,
|
||||||
logStream,
|
startLogLine,
|
||||||
authConfig?.access_token
|
endLogLine,
|
||||||
)
|
logStream,
|
||||||
|
authConfig?.access_token
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
startLogLine += endLogLine
|
startLogLine += endLogLine
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Job } from '../..'
|
import { Job } from '../..'
|
||||||
import { RequestClient } from '../../request/RequestClient'
|
import { RequestClient } from '../../request/RequestClient'
|
||||||
import { fetchLog } from '../../utils'
|
import { fetchLog } from '../../utils'
|
||||||
import { WriteStream } from 'fs'
|
import { WriteStream } from '../../types'
|
||||||
import { writeStream } from './writeStream'
|
import { writeStream } from './writeStream'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
41
src/api/viya/spec/getFileStream.spec.ts
Normal file
41
src/api/viya/spec/getFileStream.spec.ts
Normal file
@@ -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(path.join(__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))
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Logger, LogLevel } from '@sasjs/utils'
|
import { Logger, LogLevel } from '@sasjs/utils'
|
||||||
import * as fileModule from '@sasjs/utils/file'
|
|
||||||
import { RequestClient } from '../../../request/RequestClient'
|
import { RequestClient } from '../../../request/RequestClient'
|
||||||
import { mockAuthConfig, mockJob } from './mockResponses'
|
import { mockAuthConfig, mockJob } from './mockResponses'
|
||||||
import { pollJobState } from '../pollJobState'
|
import { pollJobState } from '../pollJobState'
|
||||||
import * as getTokensModule from '../../../auth/getTokens'
|
import * as getTokensModule from '../../../auth/getTokens'
|
||||||
import * as saveLogModule from '../saveLog'
|
import * as saveLogModule from '../saveLog'
|
||||||
|
import * as getFileStreamModule from '../getFileStream'
|
||||||
|
import * as isNodeModule from '../../../utils/isNode'
|
||||||
import { PollOptions } from '../../../types'
|
import { PollOptions } from '../../../types'
|
||||||
import { WriteStream } from 'fs'
|
import { WriteStream } from 'fs'
|
||||||
|
|
||||||
@@ -76,13 +77,43 @@ describe('pollJobState', () => {
|
|||||||
|
|
||||||
it('should attempt to fetch and save the log after each poll when streamLog is true', async () => {
|
it('should attempt to fetch and save the log after each poll when streamLog is true', async () => {
|
||||||
mockSimplePoll()
|
mockSimplePoll()
|
||||||
|
const { saveLog } = require('../saveLog')
|
||||||
|
|
||||||
await pollJobState(requestClient, mockJob, false, mockAuthConfig, {
|
await pollJobState(requestClient, mockJob, false, mockAuthConfig, {
|
||||||
...defaultPollOptions,
|
...defaultPollOptions,
|
||||||
streamLog: true
|
streamLog: true
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(saveLogModule.saveLog).toHaveBeenCalledTimes(2)
|
expect(saveLog).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
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
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(getFileStream).toHaveBeenCalled()
|
||||||
|
expect(saveLog).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
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
|
||||||
|
})
|
||||||
|
|
||||||
|
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 () => {
|
it('should not attempt to fetch and save the log after each poll when streamLog is false', async () => {
|
||||||
@@ -217,7 +248,8 @@ const setupMocks = () => {
|
|||||||
jest.mock('../../../request/RequestClient')
|
jest.mock('../../../request/RequestClient')
|
||||||
jest.mock('../../../auth/getTokens')
|
jest.mock('../../../auth/getTokens')
|
||||||
jest.mock('../saveLog')
|
jest.mock('../saveLog')
|
||||||
jest.mock('@sasjs/utils/file')
|
jest.mock('../getFileStream')
|
||||||
|
jest.mock('../../../utils/isNode')
|
||||||
|
|
||||||
jest
|
jest
|
||||||
.spyOn(requestClient, 'get')
|
.spyOn(requestClient, 'get')
|
||||||
@@ -231,8 +263,9 @@ const setupMocks = () => {
|
|||||||
.spyOn(saveLogModule, 'saveLog')
|
.spyOn(saveLogModule, 'saveLog')
|
||||||
.mockImplementation(() => Promise.resolve())
|
.mockImplementation(() => Promise.resolve())
|
||||||
jest
|
jest
|
||||||
.spyOn(fileModule, 'createWriteStream')
|
.spyOn(getFileStreamModule, 'getFileStream')
|
||||||
.mockImplementation(() => Promise.resolve({} as unknown as WriteStream))
|
.mockImplementation(() => Promise.resolve({} as unknown as WriteStream))
|
||||||
|
jest.spyOn(isNodeModule, 'isNode').mockImplementation(() => true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const mockSimplePoll = (runningCount = 2) => {
|
const mockSimplePoll = (runningCount = 2) => {
|
||||||
@@ -278,7 +311,7 @@ const mockLongPoll = () => {
|
|||||||
return Promise.resolve({ result: mockJob, etag: '', status: 200 })
|
return Promise.resolve({ result: mockJob, etag: '', status: 200 })
|
||||||
}
|
}
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
result: count <= 101 ? 'running' : 'completed',
|
result: count <= 102 ? 'running' : 'completed',
|
||||||
etag: '',
|
etag: '',
|
||||||
status: 200
|
status: 200
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import * as fetchLogsModule from '../../../utils/fetchLogByChunks'
|
|||||||
import * as writeStreamModule from '../writeStream'
|
import * as writeStreamModule from '../writeStream'
|
||||||
import { saveLog } from '../saveLog'
|
import { saveLog } from '../saveLog'
|
||||||
import { mockJob } from './mockResponses'
|
import { mockJob } from './mockResponses'
|
||||||
import { WriteStream } from 'fs'
|
import { WriteStream } from '../../../types'
|
||||||
|
|
||||||
const requestClient = new (<jest.Mock<RequestClient>>RequestClient)()
|
const requestClient = new (<jest.Mock<RequestClient>>RequestClient)()
|
||||||
const stream = {} as unknown as WriteStream
|
const stream = {} as unknown as WriteStream
|
||||||
|
|||||||
25
src/api/viya/spec/writeStream.spec.ts
Normal file
25
src/api/viya/spec/writeStream.spec.ts
Normal file
@@ -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')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import { WriteStream } from 'fs'
|
import { WriteStream } from '../../types'
|
||||||
|
|
||||||
export const writeStream = async (
|
export const writeStream = async (
|
||||||
stream: WriteStream,
|
stream: WriteStream,
|
||||||
content: string
|
content: string
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
stream.write(content + '\n\nnext chunk\n\n', (e) => {
|
stream.write(content + '\n', (e) => {
|
||||||
if (e) {
|
if (e) {
|
||||||
return reject(e)
|
return reject(e)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,7 +124,8 @@ 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
|
||||||
this.logOut()
|
//Residue can happen in case of session expiration
|
||||||
|
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', ''),
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
AuthConfig,
|
|
||||||
isAccessTokenExpiring,
|
isAccessTokenExpiring,
|
||||||
isRefreshTokenExpiring,
|
isRefreshTokenExpiring,
|
||||||
hasTokenExpired
|
hasTokenExpired
|
||||||
} from '@sasjs/utils'
|
} from '@sasjs/utils/auth'
|
||||||
|
import { AuthConfig } from '@sasjs/utils/types'
|
||||||
import { RequestClient } from '../request/RequestClient'
|
import { RequestClient } from '../request/RequestClient'
|
||||||
import { refreshTokens } from './refreshTokens'
|
import { refreshTokens } from './refreshTokens'
|
||||||
|
|
||||||
|
|||||||
@@ -35,14 +35,11 @@ export class ComputeJobExecutor extends BaseJobExecutor {
|
|||||||
expectWebout
|
expectWebout
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
this.appendRequest(response, sasJob, config.debug)
|
console.log('then block of compute job executor')
|
||||||
|
|
||||||
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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,8 +27,6 @@ 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) {
|
||||||
@@ -49,8 +47,6 @@ 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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ 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 {
|
||||||
@@ -46,54 +44,7 @@ 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -2,13 +2,18 @@ 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'
|
||||||
import { RequestClient } from '../request/RequestClient'
|
import { RequestClient } from '../request/RequestClient'
|
||||||
import { SASViyaApiClient } from '../SASViyaApiClient'
|
import { SASViyaApiClient } from '../SASViyaApiClient'
|
||||||
import { isRelativePath, isValidJson } from '../utils'
|
import {
|
||||||
|
isRelativePath,
|
||||||
|
getValidJson,
|
||||||
|
parseSasViyaDebugResponse
|
||||||
|
} from '../utils'
|
||||||
import { BaseJobExecutor } from './JobExecutor'
|
import { BaseJobExecutor } from './JobExecutor'
|
||||||
import { parseWeboutResponse } from '../utils/parseWeboutResponse'
|
import { parseWeboutResponse } from '../utils/parseWeboutResponse'
|
||||||
|
|
||||||
@@ -50,7 +55,21 @@ export class WebJobExecutor extends BaseJobExecutor {
|
|||||||
|
|
||||||
apiUrl += jobUri.length > 0 ? '&_job=' + jobUri : ''
|
apiUrl += jobUri.length > 0 ? '&_job=' + jobUri : ''
|
||||||
|
|
||||||
apiUrl += config.contextName ? `&_contextname=${config.contextName}` : ''
|
if (jobUri.length > 0) {
|
||||||
|
apiUrl += '&_job=' + jobUri
|
||||||
|
/**
|
||||||
|
* Using both _job and _program parameters will cause a conflict in the JES web app, as it’s 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 = {
|
||||||
@@ -92,35 +111,38 @@ export class WebJobExecutor extends BaseJobExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const requestPromise = new Promise((resolve, reject) => {
|
const requestPromise = new Promise((resolve, reject) => {
|
||||||
this.requestClient!.post(apiUrl, formData, undefined)
|
this.requestClient!.post(
|
||||||
.then(async (res) => {
|
apiUrl,
|
||||||
|
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 this.parseSasViyaDebugResponse(
|
const jsonResponse = await parseSasViyaDebugResponse(
|
||||||
res.result as string
|
res.result,
|
||||||
|
this.requestClient,
|
||||||
|
this.serverUrl
|
||||||
)
|
)
|
||||||
this.appendRequest(res, sasJob, config.debug)
|
|
||||||
resolve(jsonResponse)
|
resolve(jsonResponse)
|
||||||
}
|
}
|
||||||
if (this.serverType === ServerType.Sas9 && config.debug) {
|
if (this.serverType === ServerType.Sas9 && config.debug) {
|
||||||
const jsonResponse = parseWeboutResponse(res.result as string)
|
let jsonResponse = res.result
|
||||||
if (jsonResponse === '') {
|
if (typeof res.result === 'string')
|
||||||
throw new Error(
|
jsonResponse = parseWeboutResponse(res.result, apiUrl)
|
||||||
'Valid JSON could not be extracted from response.'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
isValidJson(jsonResponse)
|
getValidJson(jsonResponse)
|
||||||
this.appendRequest(res, sasJob, config.debug)
|
|
||||||
resolve(res.result)
|
resolve(res.result)
|
||||||
}
|
}
|
||||||
isValidJson(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) => {
|
||||||
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))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,20 +173,6 @@ export class WebJobExecutor extends BaseJobExecutor {
|
|||||||
return requestPromise
|
return requestPromise
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseSasViyaDebugResponse = async (response: string) => {
|
|
||||||
const iframeStart = response.split(
|
|
||||||
'<iframe style="width: 99%; height: 500px" src="'
|
|
||||||
)[1]
|
|
||||||
const jsonUrl = iframeStart ? iframeStart.split('"></iframe>')[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) {
|
private async getJobUri(sasJob: string) {
|
||||||
if (!this.sasViyaApiClient) return ''
|
if (!this.sasViyaApiClient) return ''
|
||||||
let uri = ''
|
let uri = ''
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ 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 { isValidJson } from '../utils'
|
import { parseGeneratedCode, parseSourceCode } from '../utils'
|
||||||
|
|
||||||
export interface HttpClient {
|
export interface HttpClient {
|
||||||
get<T>(
|
get<T>(
|
||||||
@@ -47,6 +48,8 @@ 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
|
||||||
@@ -83,12 +86,75 @@ 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),
|
||||||
@@ -108,8 +174,11 @@ 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)
|
||||||
return this.parseResponse<T>(response)
|
if (captureRequest) {
|
||||||
|
this.appendRequest(responseToReturn, sasJob, debug)
|
||||||
|
}
|
||||||
|
return responseToReturn
|
||||||
})
|
})
|
||||||
.catch(async (e) => {
|
.catch(async (e) => {
|
||||||
return await this.handleError(
|
return await this.handleError(
|
||||||
@@ -135,7 +204,10 @@ 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),
|
||||||
@@ -146,7 +218,11 @@ 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)
|
||||||
return this.parseResponse<T>(response)
|
const responseToReturn = 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, () =>
|
||||||
@@ -429,13 +505,7 @@ export class RequestClient implements HttpClient {
|
|||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
try {
|
try {
|
||||||
const weboutResponse = parseWeboutResponse(response.data)
|
parsedResponse = JSON.parse(parseWeboutResponse(response.data))
|
||||||
if (weboutResponse === '') {
|
|
||||||
throw new Error('Valid JSON could not be extracted from response.')
|
|
||||||
}
|
|
||||||
|
|
||||||
const jsonResponse = isValidJson(weboutResponse)
|
|
||||||
parsedResponse = jsonResponse
|
|
||||||
} catch {
|
} catch {
|
||||||
parsedResponse = response.data
|
parsedResponse = response.data
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ 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(
|
||||||
|
|||||||
@@ -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', () => {
|
||||||
|
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 () => {
|
||||||
const responseStatus = 304
|
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...'
|
||||||
|
)
|
||||||
|
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: '', status: responseStatus })
|
Promise.resolve({ data: customSession.state, status: 200 })
|
||||||
)
|
)
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
sessionManager['waitForSession'](
|
sessionManager['waitForSession'](customSession, null, 'access_token')
|
||||||
{
|
).rejects.toContain('Error while getting session state link.')
|
||||||
id: 'id',
|
})
|
||||||
state: '',
|
|
||||||
links: [
|
it('should throw an error if could not get session state', async () => {
|
||||||
{ rel: 'state', href: '', uri: '', type: '', method: 'GET' }
|
mockedAxios.get.mockImplementation(() => Promise.reject('Mocked error'))
|
||||||
],
|
|
||||||
attributes: {
|
await expect(
|
||||||
sessionInactiveTimeout: 0
|
sessionManager['waitForSession'](session, null, 'access_token')
|
||||||
},
|
).rejects.toContain('Error while getting session state.')
|
||||||
creationTimeStamp: ''
|
})
|
||||||
},
|
|
||||||
null,
|
it('should return session state', async () => {
|
||||||
'access_token'
|
const customSession = JSON.parse(JSON.stringify(session))
|
||||||
)
|
customSession.state = 'completed'
|
||||||
).rejects.toEqual(
|
|
||||||
new NoSessionStateError(
|
mockedAxios.get.mockImplementation(() =>
|
||||||
responseStatus,
|
Promise.resolve({ data: customSession.state, status: 200 })
|
||||||
process.env.SERVER_URL as string,
|
|
||||||
'logUrl'
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sessionManager['waitForSession'](customSession, null, 'access_token')
|
||||||
|
).resolves.toEqual(customSession.state)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
50
src/test/utils/getValidJson.spec.ts
Normal file
50
src/test/utils/getValidJson.spec.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { getValidJson } from '../../utils'
|
||||||
|
import { JsonParseArrayError, InvalidJsonError } from '../../types/errors'
|
||||||
|
|
||||||
|
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\"}`
|
||||||
|
const test = () => {
|
||||||
|
getValidJson(json)
|
||||||
|
}
|
||||||
|
expect(test).toThrowError(InvalidJsonError)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw an error when an array is passed', () => {
|
||||||
|
const array = ['hello', 'world']
|
||||||
|
const test = () => {
|
||||||
|
getValidJson(array)
|
||||||
|
}
|
||||||
|
expect(test).toThrow(JsonParseArrayError)
|
||||||
|
})
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import { isValidJson } from '../../utils'
|
|
||||||
|
|
||||||
describe('jsonValidator', () => {
|
|
||||||
it('should not throw an error with an valid json', () => {
|
|
||||||
const json = {
|
|
||||||
test: 'test'
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(isValidJson(json)).toBe(json)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should not throw an error with an valid json string', () => {
|
|
||||||
const json = {
|
|
||||||
test: 'test'
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(isValidJson(JSON.stringify(json))).toStrictEqual(json)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should throw an error with an invalid json', () => {
|
|
||||||
const json = `{\"test\":\"test\"\"test2\":\"test\"}`
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
try {
|
|
||||||
isValidJson(json)
|
|
||||||
} catch (err) {
|
|
||||||
throw new Error()
|
|
||||||
}
|
|
||||||
}).toThrowError
|
|
||||||
})
|
|
||||||
})
|
|
||||||
4
src/types/WriteStream.ts
Normal file
4
src/types/WriteStream.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export interface WriteStream {
|
||||||
|
write: (content: string, callback: (err?: Error) => any) => void
|
||||||
|
path: string
|
||||||
|
}
|
||||||
7
src/types/errors/InvalidJsonError.ts
Normal file
7
src/types/errors/InvalidJsonError.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export class InvalidJsonError extends Error {
|
||||||
|
constructor() {
|
||||||
|
super('Error: invalid Json string')
|
||||||
|
this.name = 'InvalidJsonError'
|
||||||
|
Object.setPrototypeOf(this, InvalidJsonError.prototype)
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/types/errors/JsonParseArrayError.ts
Normal file
7
src/types/errors/JsonParseArrayError.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export class JsonParseArrayError extends Error {
|
||||||
|
constructor() {
|
||||||
|
super('Can not parse array object to json.')
|
||||||
|
this.name = 'JsonParseArrayError'
|
||||||
|
Object.setPrototypeOf(this, JsonParseArrayError.prototype)
|
||||||
|
}
|
||||||
|
}
|
||||||
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
src/types/errors/WeboutResponseError.ts
Normal file
7
src/types/errors/WeboutResponseError.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,3 +7,7 @@ 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'
|
||||||
|
|||||||
@@ -11,3 +11,4 @@ export * from './SASjsRequest'
|
|||||||
export * from './Session'
|
export * from './Session'
|
||||||
export * from './UploadFile'
|
export * from './UploadFile'
|
||||||
export * from './PollOptions'
|
export * from './PollOptions'
|
||||||
|
export * from './WriteStream'
|
||||||
|
|||||||
20
src/utils/getValidJson.ts
Normal file
20
src/utils/getValidJson.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
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.
|
||||||
|
* @param str - string to check.
|
||||||
|
*/
|
||||||
|
export const getValidJson = (str: string | object) => {
|
||||||
|
try {
|
||||||
|
if (str === null || str === undefined) throw new InvalidJsonError()
|
||||||
|
|
||||||
|
if (Array.isArray(str)) throw new JsonParseArrayError()
|
||||||
|
|
||||||
|
if (typeof str === 'object') return str
|
||||||
|
|
||||||
|
return JSON.parse(str)
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof JsonParseArrayError) throw e
|
||||||
|
throw new InvalidJsonError()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
export * from './asyncForEach'
|
export * from './asyncForEach'
|
||||||
export * from './compareTimestamps'
|
export * from './compareTimestamps'
|
||||||
export * from './convertToCsv'
|
export * from './convertToCsv'
|
||||||
|
export * from './isNode'
|
||||||
export * from './isRelativePath'
|
export * from './isRelativePath'
|
||||||
export * from './isUri'
|
export * from './isUri'
|
||||||
export * from './isUrl'
|
export * from './isUrl'
|
||||||
@@ -12,4 +13,5 @@ export * from './serialize'
|
|||||||
export * from './splitChunks'
|
export * from './splitChunks'
|
||||||
export * from './parseWeboutResponse'
|
export * from './parseWeboutResponse'
|
||||||
export * from './fetchLogByChunks'
|
export * from './fetchLogByChunks'
|
||||||
export * from './isValidJson'
|
export * from './getValidJson'
|
||||||
|
export * from './parseViyaDebugResponse'
|
||||||
|
|||||||
4
src/utils/isNode.ts
Normal file
4
src/utils/isNode.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export const isNode = () =>
|
||||||
|
typeof process !== 'undefined' &&
|
||||||
|
process.versions != null &&
|
||||||
|
process.versions.node != null
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
/**
|
|
||||||
* Checks if string is in valid JSON format else throw error.
|
|
||||||
* @param str - string to check.
|
|
||||||
*/
|
|
||||||
export const isValidJson = (str: string | object) => {
|
|
||||||
try {
|
|
||||||
if (typeof str === 'object') return str
|
|
||||||
|
|
||||||
return JSON.parse(str)
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error('Invalid JSON response.')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
30
src/utils/parseViyaDebugResponse.ts
Normal file
30
src/utils/parseViyaDebugResponse.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
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,
|
||||||
|
* 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(
|
||||||
|
'<iframe style="width: 99%; height: 500px" src="'
|
||||||
|
)[1]
|
||||||
|
const jsonUrl = iframeStart ? iframeStart.split('"></iframe>')[0] : null
|
||||||
|
if (!jsonUrl) {
|
||||||
|
throw new Error('Unable to find webout file URL.')
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestClient
|
||||||
|
.get(serverUrl + jsonUrl, undefined)
|
||||||
|
.then((res: any) => getValidJson(res.result))
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
export const parseWeboutResponse = (response: string) => {
|
import { WeboutResponseError } from '../types/errors'
|
||||||
|
|
||||||
|
export const parseWeboutResponse = (response: string, url?: string) => {
|
||||||
let sasResponse = ''
|
let sasResponse = ''
|
||||||
|
|
||||||
if (response.includes('>>weboutBEGIN<<')) {
|
if (response.includes('>>weboutBEGIN<<')) {
|
||||||
@@ -7,6 +9,7 @@ export const parseWeboutResponse = (response: 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)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user