mirror of
https://github.com/sasjs/adapter.git
synced 2026-01-07 04:20:05 +00:00
Compare commits
122 Commits
v1.11.0
...
session-ma
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
232f4ec3fb | ||
|
|
e1f17ef47d | ||
|
|
8a40071c35 | ||
|
|
430957eb3d | ||
|
|
25874be679 | ||
|
|
ed8440434f | ||
|
|
0f9884c1b6 | ||
|
|
d126a05347 | ||
|
|
3e26bbbbba | ||
|
|
982cc8f7a0 | ||
|
|
d1770698e0 | ||
|
|
b78e8617c4 | ||
|
|
3ce9ca0986 | ||
|
|
04d17c3680 | ||
|
|
d26e15f91c | ||
|
|
83c46091b3 | ||
|
|
d640d7c040 | ||
|
|
c934eb2b08 | ||
|
|
24dd5e32ad | ||
|
|
a23103b2c3 | ||
|
|
35aa4235e4 | ||
|
|
e9be1cf99a | ||
|
|
c7b0821081 | ||
|
|
4a4618dd32 | ||
|
|
d223e83c60 | ||
|
|
d1f1a20126 | ||
|
|
4b89e3762f | ||
|
|
bc110288de | ||
|
|
e94e16b52c | ||
|
|
76aacee016 | ||
|
|
1a3bd5d1f5 | ||
|
|
3f6e89d716 | ||
|
|
361ec84638 | ||
|
|
35cc1e4f62 | ||
|
|
64a976e888 | ||
|
|
7e2cb8491f | ||
|
|
2cdab7522d | ||
|
|
a07eabc408 | ||
|
|
7279c23fe2 | ||
|
|
80707d77d9 | ||
|
|
d5920c5885 | ||
|
|
6a3a6b4485 | ||
|
|
2b1df0c61a | ||
|
|
216725f306 | ||
|
|
3183f89a62 | ||
|
|
f5cc16c3bd | ||
|
|
e78dc76e56 | ||
|
|
bfdb5ef0a6 | ||
|
|
35353d3fce | ||
|
|
7a02c8ad34 | ||
|
|
331d9b0010 | ||
|
|
ef5686cce7 | ||
|
|
fa87111f4a | ||
|
|
94967b0f6c | ||
|
|
3f796b300d | ||
|
|
a07c16fb52 | ||
|
|
fd6905ea9f | ||
|
|
08f58b5f4f | ||
|
|
bd8012fe3e | ||
|
|
fa531b34fd | ||
|
|
354443c98b | ||
|
|
ee30ab195f | ||
|
|
02c1712d22 | ||
|
|
37def7a956 | ||
|
|
653e3d05e0 | ||
|
|
d8467c24b1 | ||
|
|
fc9056c1ac | ||
|
|
9b1d295b82 | ||
|
|
e2ea3f4ddc | ||
|
|
99d0b01a24 | ||
|
|
131c672020 | ||
|
|
338f2fb2dd | ||
|
|
4552a9a856 | ||
|
|
daeb753f9e | ||
|
|
f50a99d0b8 | ||
|
|
e6d0d3efd5 | ||
|
|
057460467c | ||
|
|
5aee9d955e | ||
|
|
7fb1da31e4 | ||
|
|
1aa92c0a69 | ||
|
|
4c097a69fd | ||
|
|
2634933e84 | ||
|
|
d60c0850c2 | ||
|
|
491bc3371c | ||
|
|
c1bab07b08 | ||
|
|
95f3ebd51d | ||
|
|
0e5b72b54f | ||
|
|
33ce592379 | ||
|
|
9f6591d7e3 | ||
|
|
5343ca00d8 | ||
|
|
f764f1f22f | ||
|
|
978af5037e | ||
|
|
39e88052c7 | ||
|
|
889351caf1 | ||
|
|
e6476dc230 | ||
|
|
e7de45c94f | ||
|
|
2f822aba71 | ||
|
|
ba687bf8e2 | ||
|
|
618cbe5a21 | ||
|
|
d723150b6d | ||
|
|
b1ad983ca5 | ||
|
|
4711d0510e | ||
|
|
93854c287f | ||
|
|
687a3047fd | ||
|
|
c067c6e74d | ||
|
|
04b44f40ba | ||
|
|
ce2126bd34 | ||
|
|
638efe8899 | ||
|
|
23353355e4 | ||
|
|
1be64798c5 | ||
|
|
a92a458cf1 | ||
|
|
703fdf9c02 | ||
|
|
bc239cf5d6 | ||
|
|
86780db478 | ||
|
|
5d5afa20c7 | ||
|
|
d662c1a981 | ||
|
|
f3abafd5ed | ||
|
|
5076ea696c | ||
|
|
3a60e6422c | ||
|
|
b90b5d5c03 | ||
|
|
d5a8140d4f | ||
|
|
5f5d84da87 |
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
@@ -27,6 +27,16 @@ jobs:
|
|||||||
run: npm run lint
|
run: npm run lint
|
||||||
- name: Run unit tests
|
- name: Run unit tests
|
||||||
run: npm test
|
run: npm test
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
CLIENT: ${{secrets.CLIENT}}
|
||||||
|
SECRET: ${{secrets.SECRET}}
|
||||||
|
SAS_USERNAME: ${{secrets.SAS_USERNAME}}
|
||||||
|
SAS_PASSWORD: ${{secrets.SAS_PASSWORD}}
|
||||||
|
SERVER_URL: ${{secrets.SERVER_URL}}
|
||||||
|
SERVER_TYPE: ${{secrets.SERVER_TYPE}}
|
||||||
|
ACCESS_TOKEN: ${{secrets.ACCESS_TOKEN}}
|
||||||
|
REFRESH_TOKEN: ${{secrets.REFRESH_TOKEN}}
|
||||||
- name: Build Package
|
- name: Build Package
|
||||||
run: npm run package:lib
|
run: npm run package:lib
|
||||||
env:
|
env:
|
||||||
|
|||||||
18
PULL_REQUEST_TEMPLATE.md
Normal file
18
PULL_REQUEST_TEMPLATE.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
## Issue
|
||||||
|
|
||||||
|
Link any related issue(s) in this section.
|
||||||
|
|
||||||
|
## Intent
|
||||||
|
|
||||||
|
What this PR intends to achieve.
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
What code changes have been made to achieve the intent.
|
||||||
|
|
||||||
|
## Checks
|
||||||
|
|
||||||
|
- [ ] Code is formatted correctly (`npm run lint:fix`).
|
||||||
|
- [ ] All unit tests are passing (`npm test`).
|
||||||
|
- [ ] All `sasjs-tests` 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)).
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
231
docs/classes/reflection-725.reflection-191.fileuploader.html
Normal file
231
docs/classes/reflection-725.reflection-191.fileuploader.html
Normal file
File diff suppressed because one or more lines are too long
312
docs/classes/reflection-725.reflection-191.sas9apiclient.html
Normal file
312
docs/classes/reflection-725.reflection-191.sas9apiclient.html
Normal file
File diff suppressed because one or more lines are too long
1336
docs/classes/reflection-725.reflection-191.sasjs.html
Normal file
1336
docs/classes/reflection-725.reflection-191.sasjs.html
Normal file
File diff suppressed because one or more lines are too long
1267
docs/classes/reflection-725.reflection-191.sasviyaapiclient.html
Normal file
1267
docs/classes/reflection-725.reflection-191.sasviyaapiclient.html
Normal file
File diff suppressed because one or more lines are too long
271
docs/classes/reflection-725.reflection-191.sessionmanager.html
Normal file
271
docs/classes/reflection-725.reflection-191.sessionmanager.html
Normal file
File diff suppressed because one or more lines are too long
231
docs/classes/reflection-725.reflection-194.fileuploader.html
Normal file
231
docs/classes/reflection-725.reflection-194.fileuploader.html
Normal file
File diff suppressed because one or more lines are too long
312
docs/classes/reflection-725.reflection-194.sas9apiclient.html
Normal file
312
docs/classes/reflection-725.reflection-194.sas9apiclient.html
Normal file
File diff suppressed because one or more lines are too long
1336
docs/classes/reflection-725.reflection-194.sasjs.html
Normal file
1336
docs/classes/reflection-725.reflection-194.sasjs.html
Normal file
File diff suppressed because one or more lines are too long
1270
docs/classes/reflection-725.reflection-194.sasviyaapiclient.html
Normal file
1270
docs/classes/reflection-725.reflection-194.sasviyaapiclient.html
Normal file
File diff suppressed because one or more lines are too long
271
docs/classes/reflection-725.reflection-194.sessionmanager.html
Normal file
271
docs/classes/reflection-725.reflection-194.sessionmanager.html
Normal file
File diff suppressed because one or more lines are too long
231
docs/classes/reflection-734.reflection-194.fileuploader.html
Normal file
231
docs/classes/reflection-734.reflection-194.fileuploader.html
Normal file
File diff suppressed because one or more lines are too long
312
docs/classes/reflection-734.reflection-194.sas9apiclient.html
Normal file
312
docs/classes/reflection-734.reflection-194.sas9apiclient.html
Normal file
File diff suppressed because one or more lines are too long
1385
docs/classes/reflection-734.reflection-194.sasjs.html
Normal file
1385
docs/classes/reflection-734.reflection-194.sasjs.html
Normal file
File diff suppressed because one or more lines are too long
1350
docs/classes/reflection-734.reflection-194.sasviyaapiclient.html
Normal file
1350
docs/classes/reflection-734.reflection-194.sasviyaapiclient.html
Normal file
File diff suppressed because one or more lines are too long
271
docs/classes/reflection-734.reflection-194.sessionmanager.html
Normal file
271
docs/classes/reflection-734.reflection-194.sessionmanager.html
Normal file
File diff suppressed because one or more lines are too long
231
docs/classes/reflection-743.reflection-195.fileuploader.html
Normal file
231
docs/classes/reflection-743.reflection-195.fileuploader.html
Normal file
File diff suppressed because one or more lines are too long
312
docs/classes/reflection-743.reflection-195.sas9apiclient.html
Normal file
312
docs/classes/reflection-743.reflection-195.sas9apiclient.html
Normal file
File diff suppressed because one or more lines are too long
1419
docs/classes/reflection-743.reflection-195.sasjs.html
Normal file
1419
docs/classes/reflection-743.reflection-195.sasjs.html
Normal file
File diff suppressed because one or more lines are too long
1415
docs/classes/reflection-743.reflection-195.sasviyaapiclient.html
Normal file
1415
docs/classes/reflection-743.reflection-195.sasviyaapiclient.html
Normal file
File diff suppressed because one or more lines are too long
271
docs/classes/reflection-743.reflection-195.sessionmanager.html
Normal file
271
docs/classes/reflection-743.reflection-195.sessionmanager.html
Normal file
File diff suppressed because one or more lines are too long
231
docs/classes/reflection-759.reflection-211.fileuploader.html
Normal file
231
docs/classes/reflection-759.reflection-211.fileuploader.html
Normal file
File diff suppressed because one or more lines are too long
312
docs/classes/reflection-759.reflection-211.sas9apiclient.html
Normal file
312
docs/classes/reflection-759.reflection-211.sas9apiclient.html
Normal file
File diff suppressed because one or more lines are too long
1430
docs/classes/reflection-759.reflection-211.sasjs.html
Normal file
1430
docs/classes/reflection-759.reflection-211.sasjs.html
Normal file
File diff suppressed because one or more lines are too long
1395
docs/classes/reflection-759.reflection-211.sasviyaapiclient.html
Normal file
1395
docs/classes/reflection-759.reflection-211.sasviyaapiclient.html
Normal file
File diff suppressed because one or more lines are too long
271
docs/classes/reflection-759.reflection-211.sessionmanager.html
Normal file
271
docs/classes/reflection-759.reflection-211.sessionmanager.html
Normal file
File diff suppressed because one or more lines are too long
231
docs/classes/reflection-762.reflection-214.fileuploader.html
Normal file
231
docs/classes/reflection-762.reflection-214.fileuploader.html
Normal file
File diff suppressed because one or more lines are too long
312
docs/classes/reflection-762.reflection-214.sas9apiclient.html
Normal file
312
docs/classes/reflection-762.reflection-214.sas9apiclient.html
Normal file
File diff suppressed because one or more lines are too long
1430
docs/classes/reflection-762.reflection-214.sasjs.html
Normal file
1430
docs/classes/reflection-762.reflection-214.sasjs.html
Normal file
File diff suppressed because one or more lines are too long
1395
docs/classes/reflection-762.reflection-214.sasviyaapiclient.html
Normal file
1395
docs/classes/reflection-762.reflection-214.sasviyaapiclient.html
Normal file
File diff suppressed because one or more lines are too long
271
docs/classes/reflection-762.reflection-214.sessionmanager.html
Normal file
271
docs/classes/reflection-762.reflection-214.sessionmanager.html
Normal file
File diff suppressed because one or more lines are too long
231
docs/classes/reflection-787.reflection-214.fileuploader.html
Normal file
231
docs/classes/reflection-787.reflection-214.fileuploader.html
Normal file
File diff suppressed because one or more lines are too long
312
docs/classes/reflection-787.reflection-214.sas9apiclient.html
Normal file
312
docs/classes/reflection-787.reflection-214.sas9apiclient.html
Normal file
File diff suppressed because one or more lines are too long
1590
docs/classes/reflection-787.reflection-214.sasjs.html
Normal file
1590
docs/classes/reflection-787.reflection-214.sasjs.html
Normal file
File diff suppressed because one or more lines are too long
1435
docs/classes/reflection-787.reflection-214.sasviyaapiclient.html
Normal file
1435
docs/classes/reflection-787.reflection-214.sasviyaapiclient.html
Normal file
File diff suppressed because one or more lines are too long
323
docs/classes/reflection-787.reflection-214.sessionmanager.html
Normal file
323
docs/classes/reflection-787.reflection-214.sessionmanager.html
Normal file
File diff suppressed because one or more lines are too long
231
docs/classes/reflection-790.reflection-214.fileuploader.html
Normal file
231
docs/classes/reflection-790.reflection-214.fileuploader.html
Normal file
File diff suppressed because one or more lines are too long
312
docs/classes/reflection-790.reflection-214.sas9apiclient.html
Normal file
312
docs/classes/reflection-790.reflection-214.sas9apiclient.html
Normal file
File diff suppressed because one or more lines are too long
1641
docs/classes/reflection-790.reflection-214.sasjs.html
Normal file
1641
docs/classes/reflection-790.reflection-214.sasjs.html
Normal file
File diff suppressed because one or more lines are too long
1444
docs/classes/reflection-790.reflection-214.sasviyaapiclient.html
Normal file
1444
docs/classes/reflection-790.reflection-214.sasviyaapiclient.html
Normal file
File diff suppressed because one or more lines are too long
323
docs/classes/reflection-790.reflection-214.sessionmanager.html
Normal file
323
docs/classes/reflection-790.reflection-214.sessionmanager.html
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
398
docs/interfaces/types.contextallattributes.html
Normal file
398
docs/interfaces/types.contextallattributes.html
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
106
docs/modules/reflection-725.html
Normal file
106
docs/modules/reflection-725.html
Normal file
File diff suppressed because one or more lines are too long
128
docs/modules/reflection-725.reflection-191.html
Normal file
128
docs/modules/reflection-725.reflection-191.html
Normal file
File diff suppressed because one or more lines are too long
128
docs/modules/reflection-725.reflection-194.html
Normal file
128
docs/modules/reflection-725.reflection-194.html
Normal file
File diff suppressed because one or more lines are too long
106
docs/modules/reflection-734.html
Normal file
106
docs/modules/reflection-734.html
Normal file
File diff suppressed because one or more lines are too long
128
docs/modules/reflection-734.reflection-194.html
Normal file
128
docs/modules/reflection-734.reflection-194.html
Normal file
File diff suppressed because one or more lines are too long
106
docs/modules/reflection-743.html
Normal file
106
docs/modules/reflection-743.html
Normal file
File diff suppressed because one or more lines are too long
128
docs/modules/reflection-743.reflection-195.html
Normal file
128
docs/modules/reflection-743.reflection-195.html
Normal file
File diff suppressed because one or more lines are too long
106
docs/modules/reflection-759.html
Normal file
106
docs/modules/reflection-759.html
Normal file
File diff suppressed because one or more lines are too long
128
docs/modules/reflection-759.reflection-211.html
Normal file
128
docs/modules/reflection-759.reflection-211.html
Normal file
File diff suppressed because one or more lines are too long
106
docs/modules/reflection-762.html
Normal file
106
docs/modules/reflection-762.html
Normal file
File diff suppressed because one or more lines are too long
128
docs/modules/reflection-762.reflection-214.html
Normal file
128
docs/modules/reflection-762.reflection-214.html
Normal file
File diff suppressed because one or more lines are too long
106
docs/modules/reflection-787.html
Normal file
106
docs/modules/reflection-787.html
Normal file
File diff suppressed because one or more lines are too long
128
docs/modules/reflection-787.reflection-214.html
Normal file
128
docs/modules/reflection-787.reflection-214.html
Normal file
File diff suppressed because one or more lines are too long
106
docs/modules/reflection-790.html
Normal file
106
docs/modules/reflection-790.html
Normal file
File diff suppressed because one or more lines are too long
128
docs/modules/reflection-790.reflection-214.html
Normal file
128
docs/modules/reflection-790.reflection-214.html
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -30,8 +30,8 @@
|
|||||||
$('#chart-container').append('<canvas id="myChart" style="display: none;"></canvas>')
|
$('#chart-container').append('<canvas id="myChart" style="display: none;"></canvas>')
|
||||||
// make a request to a SAS service
|
// make a request to a SAS service
|
||||||
var type = $("#cars")[0].options[$("#cars")[0].selectedIndex].value;
|
var type = $("#cars")[0].options[$("#cars")[0].selectedIndex].value;
|
||||||
// request data from an endpoint under your appLoc
|
// request data from an endpoint under your appLoc (missing opening slash implies relative path)
|
||||||
sasJs.request("/common/getdata", {
|
sasJs.request("common/getdata", {
|
||||||
// send data as an array of objects - each object is one row
|
// send data as an array of objects - each object is one row
|
||||||
fromjs: [{ type: type }]
|
fromjs: [{ type: type }]
|
||||||
}).then((response) => {
|
}).then((response) => {
|
||||||
|
|||||||
1951
package-lock.json
generated
1951
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
@@ -37,23 +37,24 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/isomorphic-fetch": "0.0.35",
|
"@types/isomorphic-fetch": "0.0.35",
|
||||||
"@types/jest": "^26.0.13",
|
"@types/jest": "^26.0.15",
|
||||||
"cp": "^0.2.0",
|
"cp": "^0.2.0",
|
||||||
|
"dotenv": "^8.2.0",
|
||||||
"jest": "^25.5.4",
|
"jest": "^25.5.4",
|
||||||
"path": "^0.12.7",
|
"path": "^0.12.7",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"semantic-release": "^17.1.1",
|
"semantic-release": "^17.2.3",
|
||||||
|
"terser-webpack-plugin": "^4.2.3",
|
||||||
"ts-jest": "^25.5.1",
|
"ts-jest": "^25.5.1",
|
||||||
"ts-loader": "^8.0.3",
|
"ts-loader": "^8.0.11",
|
||||||
"tslint": "^6.1.3",
|
"tslint": "^6.1.3",
|
||||||
"tslint-config-prettier": "^1.18.0",
|
"tslint-config-prettier": "^1.18.0",
|
||||||
"typedoc": "^0.17.8",
|
"typedoc": "^0.17.8",
|
||||||
"typedoc-neo-theme": "^1.0.10",
|
"typedoc-neo-theme": "^1.0.10",
|
||||||
"typedoc-plugin-external-module-name": "^4.0.3",
|
"typedoc-plugin-external-module-name": "^4.0.3",
|
||||||
"typescript": "^3.9.7",
|
"typescript": "^3.9.7",
|
||||||
"uglifyjs-webpack-plugin": "^2.2.0",
|
"webpack": "^4.44.2",
|
||||||
"webpack": "^4.44.1",
|
"webpack-cli": "^4.2.0"
|
||||||
"webpack-cli": "^3.3.12"
|
|
||||||
},
|
},
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
6
sasjs-tests/package-lock.json
generated
6
sasjs-tests/package-lock.json
generated
@@ -1357,9 +1357,9 @@
|
|||||||
"integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw=="
|
"integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw=="
|
||||||
},
|
},
|
||||||
"@sasjs/adapter": {
|
"@sasjs/adapter": {
|
||||||
"version": "1.3.13",
|
"version": "1.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sasjs/adapter/-/adapter-1.3.13.tgz",
|
"resolved": "https://registry.npmjs.org/@sasjs/adapter/-/adapter-1.12.0.tgz",
|
||||||
"integrity": "sha512-dWcDxgY3FB7Yx1I5dPpeQeyJDu4lezhIFrjn6lbdwRhV15aqOt4l9o9qZP+VbgOXqyi9gN0Y+p+vs2chBDFQqg==",
|
"integrity": "sha512-0uGQH9ynomWzdBaEujEtcR38q6V7LCgG0mrb1Wellv6cC/IHD3j6WfeZZAgtiMPeOSJjbCDBOlVnzC2TlBqJFw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"es6-promise": "^4.2.8",
|
"es6-promise": "^4.2.8",
|
||||||
"form-data": "^3.0.0",
|
"form-data": "^3.0.0",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"homepage": ".",
|
"homepage": ".",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sasjs/adapter": "^1.3.13",
|
"@sasjs/adapter": "^1.12.0",
|
||||||
"@sasjs/test-framework": "^1.4.0",
|
"@sasjs/test-framework": "^1.4.0",
|
||||||
"@testing-library/jest-dom": "^4.2.4",
|
"@testing-library/jest-dom": "^4.2.4",
|
||||||
"@testing-library/react": "^9.5.0",
|
"@testing-library/react": "^9.5.0",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { sendArrTests, sendObjTests } from "./testSuites/RequestData";
|
|||||||
import { specialCaseTests } from "./testSuites/SpecialCases";
|
import { specialCaseTests } from "./testSuites/SpecialCases";
|
||||||
import { sasjsRequestTests } from "./testSuites/SasjsRequests";
|
import { sasjsRequestTests } from "./testSuites/SasjsRequests";
|
||||||
import "@sasjs/test-framework/dist/index.css";
|
import "@sasjs/test-framework/dist/index.css";
|
||||||
|
import { computeTests } from "./testSuites/Compute";
|
||||||
|
|
||||||
const App = (): ReactElement<{}> => {
|
const App = (): ReactElement<{}> => {
|
||||||
const { adapter, config } = useContext(AppContext);
|
const { adapter, config } = useContext(AppContext);
|
||||||
@@ -17,7 +18,8 @@ const App = (): ReactElement<{}> => {
|
|||||||
sendArrTests(adapter),
|
sendArrTests(adapter),
|
||||||
sendObjTests(adapter),
|
sendObjTests(adapter),
|
||||||
specialCaseTests(adapter),
|
specialCaseTests(adapter),
|
||||||
sasjsRequestTests(adapter)
|
sasjsRequestTests(adapter),
|
||||||
|
computeTests(adapter)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}, [adapter, config]);
|
}, [adapter, config]);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const defaultConfig: SASjsConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const customConfig = {
|
const customConfig = {
|
||||||
serverUrl: "url",
|
serverUrl: "http://url.com",
|
||||||
pathSAS9: "sas9",
|
pathSAS9: "sas9",
|
||||||
pathSASViya: "viya",
|
pathSASViya: "viya",
|
||||||
appLoc: "/Public/seedapp",
|
appLoc: "/Public/seedapp",
|
||||||
@@ -37,6 +37,17 @@ export const basicTests = (
|
|||||||
assertion: (response: any) =>
|
assertion: (response: any) =>
|
||||||
response && response.isLoggedIn && response.userName === userName
|
response && response.isLoggedIn && response.userName === userName
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Multiple Log in attempts",
|
||||||
|
description: "Should fail on first attempt and should log the user in on second attempt",
|
||||||
|
test: async () => {
|
||||||
|
await adapter.logOut()
|
||||||
|
await adapter.logIn('invalid', 'invalid')
|
||||||
|
return adapter.logIn(userName, password)
|
||||||
|
},
|
||||||
|
assertion: (response: any) =>
|
||||||
|
response && response.isLoggedIn && response.userName === userName
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "Default config",
|
title: "Default config",
|
||||||
description:
|
description:
|
||||||
|
|||||||
41
sasjs-tests/src/testSuites/Compute.ts
Normal file
41
sasjs-tests/src/testSuites/Compute.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import SASjs from "@sasjs/adapter";
|
||||||
|
import { TestSuite } from "@sasjs/test-framework";
|
||||||
|
|
||||||
|
export const computeTests = (adapter: SASjs): TestSuite => ({
|
||||||
|
name: "Compute",
|
||||||
|
tests: [
|
||||||
|
{
|
||||||
|
title: "Start Compute Job - not waiting for result",
|
||||||
|
description: "Should start a compute job and return the session",
|
||||||
|
test: () => {
|
||||||
|
const data: any = { table1: [{ col1: "first col value" }] };
|
||||||
|
return adapter.startComputeJob("/Public/app/common/sendArr", data);
|
||||||
|
},
|
||||||
|
assertion: (res: any) => {
|
||||||
|
const expectedProperties = ["id", "applicationName", "attributes"]
|
||||||
|
return validate(expectedProperties, res);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Start Compute Job - waiting for result",
|
||||||
|
description: "Should start a compute job and return the job",
|
||||||
|
test: () => {
|
||||||
|
const data: any = { table1: [{ col1: "first col value" }] };
|
||||||
|
return adapter.startComputeJob("/Public/app/common/sendArr", data, {}, "", true);
|
||||||
|
},
|
||||||
|
assertion: (res: any) => {
|
||||||
|
const expectedProperties = ["id", "state", "creationTimeStamp", "jobConditionCode"]
|
||||||
|
return validate(expectedProperties, res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const validate = (expectedProperties: string[], data: any): boolean => {
|
||||||
|
const actualProperties = Object.keys(data);
|
||||||
|
|
||||||
|
const isValid = expectedProperties.every(
|
||||||
|
(property) => actualProperties.includes(property)
|
||||||
|
);
|
||||||
|
return isValid
|
||||||
|
}
|
||||||
@@ -47,6 +47,16 @@ const getLargeObjectData = () => {
|
|||||||
export const sendArrTests = (adapter: SASjs): TestSuite => ({
|
export const sendArrTests = (adapter: SASjs): TestSuite => ({
|
||||||
name: "sendArr",
|
name: "sendArr",
|
||||||
tests: [
|
tests: [
|
||||||
|
{
|
||||||
|
title: "Absolute paths",
|
||||||
|
description: "Should work with absolute paths to SAS jobs",
|
||||||
|
test: () => {
|
||||||
|
return adapter.request("/Public/app/common/sendArr", stringData);
|
||||||
|
},
|
||||||
|
assertion: (res: any) => {
|
||||||
|
return res.table1[0][0] === stringData.table1[0].col1;
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "Single string value",
|
title: "Single string value",
|
||||||
description: "Should send an array with a single string value",
|
description: "Should send an array with a single string value",
|
||||||
@@ -78,7 +88,7 @@ export const sendArrTests = (adapter: SASjs): TestSuite => ({
|
|||||||
return adapter.request("common/sendArr", data).catch((e) => e);
|
return adapter.request("common/sendArr", data).catch((e) => e);
|
||||||
},
|
},
|
||||||
assertion: (error: any) => {
|
assertion: (error: any) => {
|
||||||
return !!error && !!error.MESSAGE;
|
return !!error && !!error.body && !!error.body.message;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -175,7 +185,7 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
|||||||
};
|
};
|
||||||
return adapter.request("common/sendObj", invalidData).catch((e) => e);
|
return adapter.request("common/sendObj", invalidData).catch((e) => e);
|
||||||
},
|
},
|
||||||
assertion: (error: any) => !!error && !!error.MESSAGE
|
assertion: (error: any) => !!error && !!error.body && !!error.body.message
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Single string value",
|
title: "Single string value",
|
||||||
@@ -209,7 +219,7 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
|||||||
.catch((e) => e);
|
.catch((e) => e);
|
||||||
},
|
},
|
||||||
assertion: (error: any) => {
|
assertion: (error: any) => {
|
||||||
return !!error && !!error.MESSAGE;
|
return !!error && !!error.body && !!error.body.message;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { isLogInRequired, needsRetry, isUrl } from './utils'
|
import { isLogInRequired, needsRetry, isUrl } from './utils'
|
||||||
import { CsrfToken } from './types/CsrfToken'
|
import { CsrfToken } from './types/CsrfToken'
|
||||||
import { UploadFile } from './types/UploadFile'
|
import { UploadFile } from './types/UploadFile'
|
||||||
|
import { ErrorResponse } from './types'
|
||||||
|
|
||||||
const requestRetryLimit = 5
|
const requestRetryLimit = 5
|
||||||
|
|
||||||
@@ -18,8 +19,11 @@ export class FileUploader {
|
|||||||
private retryCount = 0
|
private retryCount = 0
|
||||||
|
|
||||||
public uploadFile(sasJob: string, files: UploadFile[], params: any) {
|
public uploadFile(sasJob: string, files: UploadFile[], params: any) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
if (files?.length < 1)
|
if (files?.length < 1)
|
||||||
throw new Error('At least one file must be provided.')
|
reject(new ErrorResponse('At least one file must be provided.'))
|
||||||
|
if (!sasJob || sasJob === '')
|
||||||
|
reject(new ErrorResponse('sasJob must be provided.'))
|
||||||
|
|
||||||
let paramsString = ''
|
let paramsString = ''
|
||||||
|
|
||||||
@@ -40,7 +44,6 @@ export class FileUploader {
|
|||||||
'cache-control': 'no-cache'
|
'cache-control': 'no-cache'
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
|
|
||||||
for (let file of files) {
|
for (let file of files) {
|
||||||
@@ -76,7 +79,7 @@ export class FileUploader {
|
|||||||
})
|
})
|
||||||
.then((responseText) => {
|
.then((responseText) => {
|
||||||
if (isLogInRequired(responseText))
|
if (isLogInRequired(responseText))
|
||||||
reject('You must be logged in to upload a file')
|
reject(new ErrorResponse('You must be logged in to upload a file.'))
|
||||||
|
|
||||||
if (needsRetry(responseText)) {
|
if (needsRetry(responseText)) {
|
||||||
if (this.retryCount < requestRetryLimit) {
|
if (this.retryCount < requestRetryLimit) {
|
||||||
@@ -95,10 +98,18 @@ export class FileUploader {
|
|||||||
try {
|
try {
|
||||||
resolve(JSON.parse(responseText))
|
resolve(JSON.parse(responseText))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
reject(e)
|
reject(
|
||||||
|
new ErrorResponse(
|
||||||
|
'Error while parsing json from upload response.',
|
||||||
|
e
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.catch((err: any) => {
|
||||||
|
reject(new ErrorResponse('Upload request failed.', err))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,20 +3,22 @@ import {
|
|||||||
parseAndSubmitAuthorizeForm,
|
parseAndSubmitAuthorizeForm,
|
||||||
convertToCSV,
|
convertToCSV,
|
||||||
makeRequest,
|
makeRequest,
|
||||||
|
isRelativePath,
|
||||||
isUri,
|
isUri,
|
||||||
isUrl
|
isUrl
|
||||||
} from './utils'
|
} from './utils'
|
||||||
import * as NodeFormData from 'form-data'
|
import * as NodeFormData from 'form-data'
|
||||||
import * as path from 'path'
|
|
||||||
import {
|
import {
|
||||||
Job,
|
Job,
|
||||||
Session,
|
Session,
|
||||||
Context,
|
Context,
|
||||||
|
ContextAllAttributes,
|
||||||
Folder,
|
Folder,
|
||||||
CsrfToken,
|
CsrfToken,
|
||||||
EditContextInput
|
EditContextInput,
|
||||||
|
ErrorResponse,
|
||||||
|
JobDefinition
|
||||||
} from './types'
|
} from './types'
|
||||||
import { JobDefinition } from './types/JobDefinition'
|
|
||||||
import { formatDataForRequest } from './utils/formatDataForRequest'
|
import { formatDataForRequest } from './utils/formatDataForRequest'
|
||||||
import { SessionManager } from './SessionManager'
|
import { SessionManager } from './SessionManager'
|
||||||
|
|
||||||
@@ -29,35 +31,45 @@ export class SASViyaApiClient {
|
|||||||
private serverUrl: string,
|
private serverUrl: string,
|
||||||
private rootFolderName: string,
|
private rootFolderName: string,
|
||||||
private contextName: string,
|
private contextName: string,
|
||||||
private setCsrfToken: (csrfToken: CsrfToken) => void,
|
private setCsrfToken: (csrfToken: CsrfToken) => void
|
||||||
private rootFolderMap = new Map<string, Job[]>()
|
|
||||||
) {
|
) {
|
||||||
if (!rootFolderName) {
|
|
||||||
throw new Error('Root folder must be provided.')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (serverUrl) isUrl(serverUrl)
|
if (serverUrl) isUrl(serverUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
private csrfToken: CsrfToken | null = null
|
private csrfToken: CsrfToken | null = null
|
||||||
private rootFolder: Folder | null = null
|
private fileUploadCsrfToken: CsrfToken | null = null
|
||||||
|
private _debug = false
|
||||||
private sessionManager = new SessionManager(
|
private sessionManager = new SessionManager(
|
||||||
this.serverUrl,
|
this.serverUrl,
|
||||||
this.contextName,
|
this.contextName,
|
||||||
this.setCsrfToken
|
this.setCsrfToken
|
||||||
)
|
)
|
||||||
private isForceDeploy: boolean = false
|
private folderMap = new Map<string, Job[]>()
|
||||||
|
|
||||||
/**
|
public get debug() {
|
||||||
* Returns a map containing the directory structure in the currently set root folder.
|
return this._debug
|
||||||
*/
|
|
||||||
public async getAppLocMap() {
|
|
||||||
if (this.rootFolderMap.size) {
|
|
||||||
return this.rootFolderMap
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.populateRootFolderMap()
|
public set debug(value: boolean) {
|
||||||
return this.rootFolderMap
|
this._debug = value
|
||||||
|
if (this.sessionManager) {
|
||||||
|
this.sessionManager.debug = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of jobs in the currently set root folder.
|
||||||
|
*/
|
||||||
|
public async getJobsInFolder(folderPath: string) {
|
||||||
|
const path = isRelativePath(folderPath)
|
||||||
|
? `${this.rootFolderName}/${folderPath}`
|
||||||
|
: folderPath
|
||||||
|
if (this.folderMap.get(path)) {
|
||||||
|
return this.folderMap.get(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.populateFolderMap(path)
|
||||||
|
return this.folderMap.get(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -135,32 +147,36 @@ export class SASViyaApiClient {
|
|||||||
const promises = contextsList.map((context: any) => {
|
const promises = contextsList.map((context: any) => {
|
||||||
const linesOfCode = ['%put &=sysuserid;']
|
const linesOfCode = ['%put &=sysuserid;']
|
||||||
|
|
||||||
return this.executeScript(
|
return () =>
|
||||||
|
this.executeScript(
|
||||||
`test-${context.name}`,
|
`test-${context.name}`,
|
||||||
linesOfCode,
|
linesOfCode,
|
||||||
context.name,
|
context.name,
|
||||||
accessToken,
|
accessToken,
|
||||||
false,
|
|
||||||
null,
|
null,
|
||||||
|
true,
|
||||||
true
|
true
|
||||||
).catch(() => null)
|
).catch((err) => err)
|
||||||
})
|
})
|
||||||
|
|
||||||
const results = await Promise.all(promises)
|
let results: any[] = []
|
||||||
|
|
||||||
|
for (const promise of promises) results.push(await promise())
|
||||||
|
|
||||||
results.forEach((result: any, index: number) => {
|
results.forEach((result: any, index: number) => {
|
||||||
if (result) {
|
if (result && result.error && result.error.details) {
|
||||||
|
try {
|
||||||
|
const resultParsed = result.error.details
|
||||||
|
|
||||||
|
if (resultParsed && resultParsed.body) {
|
||||||
let sysUserId = ''
|
let sysUserId = ''
|
||||||
|
|
||||||
if (result.log) {
|
const sysUserIdLog = resultParsed.body
|
||||||
const sysUserIdLog = result.log
|
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.find((line: string) => line.startsWith('SYSUSERID='))
|
.find((line: string) => line.startsWith('SYSUSERID='))
|
||||||
|
|
||||||
if (sysUserIdLog) {
|
if (sysUserIdLog) {
|
||||||
sysUserId = sysUserIdLog.replace('SYSUSERID=', '')
|
sysUserId = sysUserIdLog.replace('SYSUSERID=', '')
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
executableContexts.push({
|
executableContexts.push({
|
||||||
createdBy: contextsList[index].createdBy,
|
createdBy: contextsList[index].createdBy,
|
||||||
@@ -172,6 +188,11 @@ export class SASViyaApiClient {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return executableContexts
|
return executableContexts
|
||||||
@@ -192,7 +213,7 @@ export class SASViyaApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { result: contexts } = await this.request<{ items: Context[] }>(
|
const { result: contexts } = await this.request<{ items: Context[] }>(
|
||||||
`${this.serverUrl}/compute/contexts`,
|
`${this.serverUrl}/compute/contexts?limit=10000`,
|
||||||
{ headers }
|
{ headers }
|
||||||
)
|
)
|
||||||
const executionContext =
|
const executionContext =
|
||||||
@@ -224,16 +245,16 @@ export class SASViyaApiClient {
|
|||||||
* @param launchContextName - the name of the launcher context used by the compute service.
|
* @param launchContextName - the name of the launcher context used by the compute service.
|
||||||
* @param sharedAccountId - the ID of the account to run the servers for this context.
|
* @param sharedAccountId - the ID of the account to run the servers for this context.
|
||||||
* @param autoExecLines - the lines of code to execute during session initialization.
|
* @param autoExecLines - the lines of code to execute during session initialization.
|
||||||
* @param authorizedUsers - an optional list of authorized user IDs.
|
|
||||||
* @param accessToken - an access token for an authorized user.
|
* @param accessToken - an access token for an authorized user.
|
||||||
|
* @param authorizedUsers - an optional list of authorized user IDs.
|
||||||
*/
|
*/
|
||||||
public async createContext(
|
public async createContext(
|
||||||
contextName: string,
|
contextName: string,
|
||||||
launchContextName: string,
|
launchContextName: string,
|
||||||
sharedAccountId: string,
|
sharedAccountId: string,
|
||||||
autoExecLines: string[],
|
autoExecLines: string[],
|
||||||
authorizedUsers: string[],
|
accessToken?: string,
|
||||||
accessToken?: string
|
authorizedUsers?: string[]
|
||||||
) {
|
) {
|
||||||
if (!contextName) {
|
if (!contextName) {
|
||||||
throw new Error('Context name is required.')
|
throw new Error('Context name is required.')
|
||||||
@@ -313,10 +334,24 @@ export class SASViyaApiClient {
|
|||||||
headers.Authorization = `Bearer ${accessToken}`
|
headers.Authorization = `Bearer ${accessToken}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const originalContext = await this.getContextByName(
|
let originalContext
|
||||||
|
|
||||||
|
originalContext = await this.getComputeContextByName(
|
||||||
contextName,
|
contextName,
|
||||||
accessToken
|
accessToken
|
||||||
)
|
).catch((err) => {
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
|
||||||
|
// Try to find context by id, when context name has been changed.
|
||||||
|
if (!originalContext) {
|
||||||
|
originalContext = await this.getComputeContextById(
|
||||||
|
editedContext.id!,
|
||||||
|
accessToken
|
||||||
|
).catch((err) => {
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const { result: context, etag } = await this.request<Context>(
|
const { result: context, etag } = await this.request<Context>(
|
||||||
`${this.serverUrl}/compute/contexts/${originalContext.id}`,
|
`${this.serverUrl}/compute/contexts/${originalContext.id}`,
|
||||||
@@ -372,7 +407,7 @@ export class SASViyaApiClient {
|
|||||||
headers.Authorization = `Bearer ${accessToken}`
|
headers.Authorization = `Bearer ${accessToken}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const context = await this.getContextByName(contextName, accessToken)
|
const context = await this.getComputeContextByName(contextName, accessToken)
|
||||||
|
|
||||||
const deleteContextRequest: RequestInit = {
|
const deleteContextRequest: RequestInit = {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
@@ -387,27 +422,24 @@ export class SASViyaApiClient {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes code on the current SAS Viya server.
|
* Executes code on the current SAS Viya server.
|
||||||
* @param fileName - a name for the file being submitted for execution.
|
* @param jobPath - the path to the file being submitted for execution.
|
||||||
* @param linesOfCode - an array of code lines to execute.
|
* @param linesOfCode - an array of code lines to execute.
|
||||||
* @param contextName - the context to execute the code in.
|
* @param contextName - the context to execute the code in.
|
||||||
* @param accessToken - an access token for an authorized user.
|
* @param accessToken - an access token for an authorized user.
|
||||||
* @param sessionId - optional session ID to reuse.
|
* @param sessionId - optional session ID to reuse.
|
||||||
* @param silent - optional flag to disable logging.
|
|
||||||
* @param data - execution data.
|
* @param data - execution data.
|
||||||
* @param debug - when set to true, the log will be returned.
|
* @param debug - when set to true, the log will be returned.
|
||||||
* @param expectWebout - when set to true, the automatic _webout fileref will be checked for content, and that content returned. This fileref is used when the Job contains a SASjs web request (as opposed to executing arbitrary SAS code).
|
* @param expectWebout - when set to true, the automatic _webout fileref will be checked for content, and that content returned. This fileref is used when the Job contains a SASjs web request (as opposed to executing arbitrary SAS code).
|
||||||
*/
|
*/
|
||||||
public async executeScript(
|
public async executeScript(
|
||||||
jobName: string,
|
jobPath: string,
|
||||||
linesOfCode: string[],
|
linesOfCode: string[],
|
||||||
contextName: string,
|
contextName: string,
|
||||||
accessToken?: string,
|
accessToken?: string,
|
||||||
silent = false,
|
|
||||||
data = null,
|
data = null,
|
||||||
debug = false,
|
expectWebout = false,
|
||||||
expectWebout = false
|
waitForResult = true
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
silent = !debug
|
|
||||||
try {
|
try {
|
||||||
const headers: any = {
|
const headers: any = {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
@@ -418,7 +450,12 @@ export class SASViyaApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let executionSessionId: string
|
let executionSessionId: string
|
||||||
const session = await this.sessionManager.getSession(accessToken)
|
const session = await this.sessionManager
|
||||||
|
.getSession(accessToken)
|
||||||
|
.catch((err) => {
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
|
||||||
executionSessionId = session!.id
|
executionSessionId = session!.id
|
||||||
|
|
||||||
const jobArguments: { [key: string]: any } = {
|
const jobArguments: { [key: string]: any } = {
|
||||||
@@ -430,26 +467,36 @@ export class SASViyaApiClient {
|
|||||||
_OMITTEXTLOG: true
|
_OMITTEXTLOG: true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (debug) {
|
if (this.debug) {
|
||||||
jobArguments['_OMITTEXTLOG'] = false
|
jobArguments['_OMITTEXTLOG'] = false
|
||||||
jobArguments['_OMITSESSIONRESULTS'] = false
|
jobArguments['_OMITSESSIONRESULTS'] = false
|
||||||
jobArguments['_DEBUG'] = 131
|
jobArguments['_DEBUG'] = 131
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileName = `exec-${
|
let fileName
|
||||||
jobName.includes('/') ? jobName.split('/')[1] : jobName
|
if (isRelativePath(jobPath)) {
|
||||||
|
fileName = `exec-${
|
||||||
|
jobPath.includes('/') ? jobPath.split('/')[1] : jobPath
|
||||||
}`
|
}`
|
||||||
|
} else {
|
||||||
|
const jobPathParts = jobPath.split('/')
|
||||||
|
fileName = jobPathParts.pop()
|
||||||
|
}
|
||||||
|
|
||||||
let jobVariables: any = {
|
let jobVariables: any = {
|
||||||
SYS_JES_JOB_URI: '',
|
SYS_JES_JOB_URI: '',
|
||||||
_program: this.rootFolderName + '/' + jobName
|
_program: isRelativePath(jobPath)
|
||||||
|
? this.rootFolderName + '/' + jobPath
|
||||||
|
: jobPath
|
||||||
}
|
}
|
||||||
|
|
||||||
let files: any[] = []
|
let files: any[] = []
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
if (JSON.stringify(data).includes(';')) {
|
if (JSON.stringify(data).includes(';')) {
|
||||||
files = await this.uploadTables(data, accessToken)
|
files = await this.uploadTables(data, accessToken).catch((err) => {
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
|
||||||
jobVariables['_webin_file_count'] = files.length
|
jobVariables['_webin_file_count'] = files.length
|
||||||
|
|
||||||
@@ -480,9 +527,15 @@ export class SASViyaApiClient {
|
|||||||
const { result: postedJob, etag } = await this.request<Job>(
|
const { result: postedJob, etag } = await this.request<Job>(
|
||||||
`${this.serverUrl}/compute/sessions/${executionSessionId}/jobs`,
|
`${this.serverUrl}/compute/sessions/${executionSessionId}/jobs`,
|
||||||
postJobRequest
|
postJobRequest
|
||||||
)
|
).catch((err) => {
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
|
||||||
if (!silent) {
|
if (!waitForResult) {
|
||||||
|
return session
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.debug) {
|
||||||
console.log(`Job has been submitted for '${fileName}'.`)
|
console.log(`Job has been submitted for '${fileName}'.`)
|
||||||
console.log(
|
console.log(
|
||||||
`You can monitor the job progress at '${this.serverUrl}${
|
`You can monitor the job progress at '${this.serverUrl}${
|
||||||
@@ -491,32 +544,33 @@ export class SASViyaApiClient {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const jobStatus = await this.pollJobState(
|
const jobStatus = await this.pollJobState(postedJob, etag, accessToken)
|
||||||
postedJob,
|
|
||||||
etag,
|
|
||||||
accessToken,
|
|
||||||
silent
|
|
||||||
)
|
|
||||||
|
|
||||||
const { result: currentJob } = await this.request<Job>(
|
const { result: currentJob } = await this.request<Job>(
|
||||||
`${this.serverUrl}/compute/sessions/${executionSessionId}/jobs/${postedJob.id}`,
|
`${this.serverUrl}/compute/sessions/${executionSessionId}/jobs/${postedJob.id}`,
|
||||||
{ headers }
|
{ headers }
|
||||||
)
|
).catch((err) => {
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
|
||||||
let jobResult
|
let jobResult
|
||||||
let log
|
let log
|
||||||
|
|
||||||
const logLink = currentJob.links.find((l) => l.rel === 'log')
|
const logLink = currentJob.links.find((l) => l.rel === 'log')
|
||||||
|
|
||||||
if (debug && logLink) {
|
if (this.debug && logLink) {
|
||||||
log = await this.request<any>(
|
log = await this.request<any>(
|
||||||
`${this.serverUrl}${logLink.href}/content?limit=10000`,
|
`${this.serverUrl}${logLink.href}/content?limit=10000`,
|
||||||
{
|
{
|
||||||
headers
|
headers
|
||||||
}
|
}
|
||||||
).then((res: any) =>
|
)
|
||||||
|
.then((res: any) =>
|
||||||
res.result.items.map((i: any) => i.line).join('\n')
|
res.result.items.map((i: any) => i.line).join('\n')
|
||||||
)
|
)
|
||||||
|
.catch((err) => {
|
||||||
|
throw err
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (jobStatus === 'failed' || jobStatus === 'error') {
|
if (jobStatus === 'failed' || jobStatus === 'error') {
|
||||||
@@ -527,6 +581,8 @@ export class SASViyaApiClient {
|
|||||||
|
|
||||||
if (expectWebout) {
|
if (expectWebout) {
|
||||||
resultLink = `/compute/sessions/${executionSessionId}/filerefs/_webout/content`
|
resultLink = `/compute/sessions/${executionSessionId}/filerefs/_webout/content`
|
||||||
|
} else {
|
||||||
|
return currentJob
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resultLink) {
|
if (resultLink) {
|
||||||
@@ -534,24 +590,53 @@ export class SASViyaApiClient {
|
|||||||
`${this.serverUrl}${resultLink}`,
|
`${this.serverUrl}${resultLink}`,
|
||||||
{ headers },
|
{ headers },
|
||||||
'text'
|
'text'
|
||||||
).catch((e) => ({
|
).catch(async (e) => {
|
||||||
|
if (e && e.status === 404) {
|
||||||
|
if (logLink) {
|
||||||
|
log = await this.request<any>(
|
||||||
|
`${this.serverUrl}${logLink.href}/content?limit=10000`,
|
||||||
|
{
|
||||||
|
headers
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then((res: any) =>
|
||||||
|
res.result.items.map((i: any) => i.line).join('\n')
|
||||||
|
)
|
||||||
|
.catch((err) => {
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
|
||||||
|
return Promise.reject(
|
||||||
|
new ErrorResponse('Job execution failed.', {
|
||||||
|
status: 500,
|
||||||
|
body: log
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
result: JSON.stringify(e)
|
result: JSON.stringify(e)
|
||||||
}))
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.sessionManager.clearSession(executionSessionId, accessToken)
|
await this.sessionManager
|
||||||
|
.clearSession(executionSessionId, accessToken)
|
||||||
|
.catch((err) => {
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
|
||||||
return { result: jobResult?.result, log }
|
return { result: jobResult?.result, log }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e && e.status === 404) {
|
if (e && e.status === 404) {
|
||||||
return this.executeScript(
|
return this.executeScript(
|
||||||
jobName,
|
jobPath,
|
||||||
linesOfCode,
|
linesOfCode,
|
||||||
contextName,
|
contextName,
|
||||||
accessToken,
|
accessToken,
|
||||||
silent,
|
|
||||||
data,
|
data,
|
||||||
debug
|
false,
|
||||||
|
true
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
throw e
|
throw e
|
||||||
@@ -583,8 +668,6 @@ export class SASViyaApiClient {
|
|||||||
if (!parentFolderUri && parentFolderPath) {
|
if (!parentFolderUri && parentFolderPath) {
|
||||||
parentFolderUri = await this.getFolderUri(parentFolderPath, accessToken)
|
parentFolderUri = await this.getFolderUri(parentFolderPath, accessToken)
|
||||||
if (!parentFolderUri) {
|
if (!parentFolderUri) {
|
||||||
if (isForced) this.isForceDeploy = true
|
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`Parent folder at path '${parentFolderPath}' is not present.`
|
`Parent folder at path '${parentFolderPath}' is not present.`
|
||||||
)
|
)
|
||||||
@@ -610,37 +693,16 @@ export class SASViyaApiClient {
|
|||||||
`Parent folder '${newFolderName}' has been successfully created.`
|
`Parent folder '${newFolderName}' has been successfully created.`
|
||||||
)
|
)
|
||||||
parentFolderUri = `/folders/folders/${parentFolder.id}`
|
parentFolderUri = `/folders/folders/${parentFolder.id}`
|
||||||
} else if (isForced && accessToken && !this.isForceDeploy) {
|
} else if (isForced && accessToken) {
|
||||||
this.isForceDeploy = true
|
const folderPath = parentFolderPath + '/' + folderName
|
||||||
|
const folderUri = await this.getFolderUri(folderPath, accessToken)
|
||||||
|
|
||||||
await this.deleteFolder(parentFolderPath, accessToken)
|
if (folderUri) {
|
||||||
|
await this.deleteFolder(
|
||||||
const newParentFolderPath = parentFolderPath.substring(
|
parentFolderPath + '/' + folderName,
|
||||||
0,
|
|
||||||
parentFolderPath.lastIndexOf('/')
|
|
||||||
)
|
|
||||||
const newFolderName = `${parentFolderPath.split('/').pop()}`
|
|
||||||
|
|
||||||
if (newParentFolderPath === '') {
|
|
||||||
throw new Error(`Root folder has to be present on the server.`)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`Creating parent folder:\n'${newFolderName}' in '${newParentFolderPath}'`
|
|
||||||
)
|
|
||||||
|
|
||||||
const parentFolder = await this.createFolder(
|
|
||||||
newFolderName,
|
|
||||||
newParentFolderPath,
|
|
||||||
undefined,
|
|
||||||
accessToken
|
accessToken
|
||||||
)
|
)
|
||||||
|
}
|
||||||
console.log(
|
|
||||||
`Parent folder '${newFolderName}' has been successfully created.`
|
|
||||||
)
|
|
||||||
|
|
||||||
parentFolderUri = `/folders/folders/${parentFolder.id}`
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -662,8 +724,11 @@ export class SASViyaApiClient {
|
|||||||
createFolderRequest
|
createFolderRequest
|
||||||
)
|
)
|
||||||
|
|
||||||
// updates rootFolderMap with newly created folder.
|
// update folder map with newly created folder.
|
||||||
await this.populateRootFolderMap(accessToken)
|
await this.populateFolderMap(
|
||||||
|
`${parentFolderPath}/${folderName}`,
|
||||||
|
accessToken
|
||||||
|
)
|
||||||
return createFolderResponse
|
return createFolderResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -884,38 +949,62 @@ export class SASViyaApiClient {
|
|||||||
* @param debug - sets the _debug flag in the job arguments.
|
* @param debug - sets the _debug flag in the job arguments.
|
||||||
* @param data - any data to be passed in as input to the job.
|
* @param data - any data to be passed in as input to the job.
|
||||||
* @param accessToken - an optional access token for an authorized user.
|
* @param accessToken - an optional access token for an authorized user.
|
||||||
|
* @param waitForResult - a boolean indicating if the function should wait for a result.
|
||||||
|
* @param expectWebout - a boolean indicating whether to expect a _webout response.
|
||||||
*/
|
*/
|
||||||
public async executeComputeJob(
|
public async executeComputeJob(
|
||||||
sasJob: string,
|
sasJob: string,
|
||||||
contextName: string,
|
contextName: string,
|
||||||
debug: boolean,
|
|
||||||
data?: any,
|
data?: any,
|
||||||
accessToken?: string
|
accessToken?: string,
|
||||||
|
waitForResult = true,
|
||||||
|
expectWebout = false
|
||||||
) {
|
) {
|
||||||
if (!this.rootFolder) {
|
if (isRelativePath(sasJob) && !this.rootFolderName) {
|
||||||
await this.populateRootFolder(accessToken)
|
|
||||||
}
|
|
||||||
if (!this.rootFolder) {
|
|
||||||
throw new Error(`Root folder was not found.`)
|
|
||||||
}
|
|
||||||
if (!this.rootFolderMap.size) {
|
|
||||||
await this.populateRootFolderMap(accessToken)
|
|
||||||
}
|
|
||||||
if (!this.rootFolderMap.size) {
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`The job '${sasJob}' was not found in '${this.rootFolderName}'.`
|
'Relative paths cannot be used without specifying a root folder name'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isRelativePath(sasJob)) {
|
||||||
|
const folderName = sasJob.split('/')[0]
|
||||||
|
await this.populateFolderMap(
|
||||||
|
`${this.rootFolderName}/${folderName}`,
|
||||||
|
accessToken
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!this.folderMap.get(`${this.rootFolderName}/${folderName}`)) {
|
||||||
|
throw new Error(
|
||||||
|
`The folder '${folderName}' was not found at '${this.serverUrl}/${this.rootFolderName}'`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const folderPathParts = sasJob.split('/')
|
||||||
|
folderPathParts.pop()
|
||||||
|
const folderPath = folderPathParts.join('/')
|
||||||
|
await this.populateFolderMap(folderPath, accessToken)
|
||||||
|
}
|
||||||
|
|
||||||
const headers: any = { 'Content-Type': 'application/json' }
|
const headers: any = { 'Content-Type': 'application/json' }
|
||||||
if (!!accessToken) {
|
if (!!accessToken) {
|
||||||
headers.Authorization = `Bearer ${accessToken}`
|
headers.Authorization = `Bearer ${accessToken}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let jobToExecute
|
||||||
|
if (isRelativePath(sasJob)) {
|
||||||
const folderName = sasJob.split('/')[0]
|
const folderName = sasJob.split('/')[0]
|
||||||
const jobName = sasJob.split('/')[1]
|
const jobName = sasJob.split('/')[1]
|
||||||
const jobFolder = this.rootFolderMap.get(folderName)
|
const jobFolder = this.folderMap.get(
|
||||||
const jobToExecute = jobFolder?.find((item) => item.name === jobName)
|
`${this.rootFolderName}/${folderName}`
|
||||||
|
)
|
||||||
|
jobToExecute = jobFolder?.find((item) => item.name === jobName)
|
||||||
|
} else {
|
||||||
|
const folderPathParts = sasJob.split('/')
|
||||||
|
const jobName = folderPathParts.pop()
|
||||||
|
const folderPath = folderPathParts.join('/')
|
||||||
|
const jobFolder = this.folderMap.get(folderPath)
|
||||||
|
jobToExecute = jobFolder?.find((item) => item.name === jobName)
|
||||||
|
}
|
||||||
|
|
||||||
if (!jobToExecute) {
|
if (!jobToExecute) {
|
||||||
throw new Error(`Job was not found.`)
|
throw new Error(`Job was not found.`)
|
||||||
@@ -943,22 +1032,23 @@ export class SASViyaApiClient {
|
|||||||
jobToExecute.code = code
|
jobToExecute.code = code
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!code) code = ''
|
||||||
|
|
||||||
const linesToExecute = code.replace(/\r\n/g, '\n').split('\n')
|
const linesToExecute = code.replace(/\r\n/g, '\n').split('\n')
|
||||||
return await this.executeScript(
|
return await this.executeScript(
|
||||||
sasJob,
|
sasJob,
|
||||||
linesToExecute,
|
linesToExecute,
|
||||||
contextName,
|
contextName,
|
||||||
accessToken,
|
accessToken,
|
||||||
true,
|
|
||||||
data,
|
data,
|
||||||
debug,
|
expectWebout,
|
||||||
true
|
waitForResult
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes a job via the SAS Viya Job Execution API.
|
* Executes a job via the SAS Viya Job Execution API
|
||||||
* @param sasJob - the relative path to the job.
|
* @param sasJob - the relative or absolute path to the job.
|
||||||
* @param contextName - the name of the context where the job is to be executed.
|
* @param contextName - the name of the context where the job is to be executed.
|
||||||
* @param debug - sets the _debug flag in the job arguments.
|
* @param debug - sets the _debug flag in the job arguments.
|
||||||
* @param data - any data to be passed in as input to the job.
|
* @param data - any data to be passed in as input to the job.
|
||||||
@@ -971,20 +1061,34 @@ export class SASViyaApiClient {
|
|||||||
data?: any,
|
data?: any,
|
||||||
accessToken?: string
|
accessToken?: string
|
||||||
) {
|
) {
|
||||||
if (!this.rootFolder) {
|
if (isRelativePath(sasJob) && !this.rootFolderName) {
|
||||||
await this.populateRootFolder(accessToken)
|
throw new Error(
|
||||||
|
'Relative paths cannot be used without specifying a root folder name.'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.rootFolder) {
|
if (isRelativePath(sasJob)) {
|
||||||
throw new Error(`Root folder was not found.`)
|
const folderName = sasJob.split('/')[0]
|
||||||
}
|
await this.populateFolderMap(
|
||||||
if (!this.rootFolderMap.size) {
|
`${this.rootFolderName}/${folderName}`,
|
||||||
await this.populateRootFolderMap(accessToken)
|
accessToken
|
||||||
}
|
|
||||||
if (!this.rootFolderMap.size) {
|
|
||||||
throw new Error(
|
|
||||||
`The job '${sasJob}' was not found in folder '${this.rootFolderName}'.`
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (!this.folderMap.get(`${this.rootFolderName}/${folderName}`)) {
|
||||||
|
throw new Error(
|
||||||
|
`The folder '${folderName}' was not found at '${this.serverUrl}/${this.rootFolderName}'.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const folderPathParts = sasJob.split('/')
|
||||||
|
folderPathParts.pop()
|
||||||
|
const folderPath = folderPathParts.join('/')
|
||||||
|
await this.populateFolderMap(folderPath, accessToken)
|
||||||
|
if (!this.folderMap.get(folderPath)) {
|
||||||
|
throw new Error(
|
||||||
|
`The folder '${folderPath}' was not found at '${this.serverUrl}'.`
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let files: any[] = []
|
let files: any[] = []
|
||||||
@@ -992,25 +1096,29 @@ export class SASViyaApiClient {
|
|||||||
files = await this.uploadTables(data, accessToken)
|
files = await this.uploadTables(data, accessToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
const jobName = path.basename(sasJob)
|
let jobToExecute: Job | undefined
|
||||||
const jobFolder = sasJob.replace(`/${jobName}`, '')
|
let jobName: string | undefined
|
||||||
const allJobsInFolder = this.rootFolderMap.get(jobFolder.replace('/', ''))
|
let jobPath: string | undefined
|
||||||
|
if (isRelativePath(sasJob)) {
|
||||||
if (allJobsInFolder) {
|
const folderName = sasJob.split('/')[0]
|
||||||
const jobSpec = allJobsInFolder.find((j: Job) => j.name === jobName)
|
jobName = sasJob.split('/')[1]
|
||||||
|
jobPath = `${this.rootFolderName}/${folderName}`
|
||||||
if (!jobSpec) {
|
const jobFolder = this.folderMap.get(jobPath)
|
||||||
throw new Error('Job was not found.')
|
jobToExecute = jobFolder?.find((item) => item.name === jobName)
|
||||||
|
} else {
|
||||||
|
const folderPathParts = sasJob.split('/')
|
||||||
|
jobName = folderPathParts.pop()
|
||||||
|
jobPath = folderPathParts.join('/')
|
||||||
|
const jobFolder = this.folderMap.get(jobPath)
|
||||||
|
jobToExecute = jobFolder?.find((item) => item.name === jobName)
|
||||||
}
|
}
|
||||||
|
|
||||||
const jobDefinitionLink = jobSpec?.links.find(
|
if (!jobToExecute) {
|
||||||
|
throw new Error(`Job was not found.`)
|
||||||
|
}
|
||||||
|
const jobDefinitionLink = jobToExecute?.links.find(
|
||||||
(l) => l.rel === 'getResource'
|
(l) => l.rel === 'getResource'
|
||||||
)?.href
|
)?.href
|
||||||
|
|
||||||
if (!jobDefinitionLink) {
|
|
||||||
throw new Error('Job definition URI was not found.')
|
|
||||||
}
|
|
||||||
|
|
||||||
const requestInfo: any = {
|
const requestInfo: any = {
|
||||||
method: 'GET'
|
method: 'GET'
|
||||||
}
|
}
|
||||||
@@ -1029,7 +1137,7 @@ export class SASViyaApiClient {
|
|||||||
|
|
||||||
const jobArguments: { [key: string]: any } = {
|
const jobArguments: { [key: string]: any } = {
|
||||||
_contextName: contextName,
|
_contextName: contextName,
|
||||||
_program: `${this.rootFolderName}/${sasJob}`,
|
_program: `${jobPath}/${jobName}`,
|
||||||
_webin_file_count: files.length,
|
_webin_file_count: files.length,
|
||||||
_OMITJSONLISTING: true,
|
_OMITJSONLISTING: true,
|
||||||
_OMITJSONLOG: true,
|
_OMITJSONLOG: true,
|
||||||
@@ -1065,18 +1173,14 @@ export class SASViyaApiClient {
|
|||||||
`${this.serverUrl}/jobExecution/jobs?_action=wait`,
|
`${this.serverUrl}/jobExecution/jobs?_action=wait`,
|
||||||
postJobRequest
|
postJobRequest
|
||||||
)
|
)
|
||||||
const jobStatus = await this.pollJobState(
|
const jobStatus = await this.pollJobState(postedJob, etag, accessToken)
|
||||||
postedJob,
|
|
||||||
etag,
|
|
||||||
accessToken,
|
|
||||||
true
|
|
||||||
)
|
|
||||||
const { result: currentJob } = await this.request<Job>(
|
const { result: currentJob } = await this.request<Job>(
|
||||||
`${this.serverUrl}/jobExecution/jobs/${postedJob.id}`,
|
`${this.serverUrl}/jobExecution/jobs/${postedJob.id}`,
|
||||||
{ headers }
|
{ headers }
|
||||||
)
|
)
|
||||||
|
|
||||||
let jobResult, log
|
let jobResult
|
||||||
|
let log
|
||||||
if (jobStatus === 'failed') {
|
if (jobStatus === 'failed') {
|
||||||
return Promise.reject(currentJob.error)
|
return Promise.reject(currentJob.error)
|
||||||
}
|
}
|
||||||
@@ -1095,21 +1199,20 @@ export class SASViyaApiClient {
|
|||||||
{
|
{
|
||||||
headers
|
headers
|
||||||
}
|
}
|
||||||
).then((res: any) =>
|
).then((res: any) => res.result.items.map((i: any) => i.line).join('\n'))
|
||||||
res.result.items.map((i: any) => i.line).join('\n')
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return { result: jobResult?.result, log }
|
return { result: jobResult?.result, log }
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
`The job '${sasJob}' was not found in folder '${this.rootFolderName}'.`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async populateRootFolderMap(accessToken?: string) {
|
private async populateFolderMap(folderPath: string, accessToken?: string) {
|
||||||
const allItems = new Map<string, Job[]>()
|
const path = isRelativePath(folderPath)
|
||||||
const url = '/folders/folders/@item?path=' + this.rootFolderName
|
? `${this.rootFolderName}/${folderPath}`
|
||||||
|
: folderPath
|
||||||
|
if (this.folderMap.get(path)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = '/folders/folders/@item?path=' + path
|
||||||
const requestInfo: any = {
|
const requestInfo: any = {
|
||||||
method: 'GET'
|
method: 'GET'
|
||||||
}
|
}
|
||||||
@@ -1121,9 +1224,7 @@ export class SASViyaApiClient {
|
|||||||
requestInfo
|
requestInfo
|
||||||
)
|
)
|
||||||
if (!folder) {
|
if (!folder) {
|
||||||
throw new Error(
|
throw new Error(`The path ${path} does not exist on ${this.serverUrl}`)
|
||||||
`Not able to populate root folder map, because folder '${this.rootFolderName}' does not exist.`
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
const { result: members } = await this.request<{ items: any[] }>(
|
const { result: members } = await this.request<{ items: any[] }>(
|
||||||
`${this.serverUrl}/folders/folders/${folder.id}/members`,
|
`${this.serverUrl}/folders/folders/${folder.id}/members`,
|
||||||
@@ -1131,62 +1232,13 @@ export class SASViyaApiClient {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const itemsAtRoot = members.items
|
const itemsAtRoot = members.items
|
||||||
allItems.set('', itemsAtRoot)
|
this.folderMap.set(path, itemsAtRoot)
|
||||||
const subfolderRequests = members.items
|
|
||||||
.filter((i: any) => i.contentType === 'folder')
|
|
||||||
.map(async (member: any) => {
|
|
||||||
const subFolderUrl =
|
|
||||||
'/folders/folders/@item?path=' +
|
|
||||||
this.rootFolderName +
|
|
||||||
'/' +
|
|
||||||
member.name
|
|
||||||
const { result: memberDetail } = await this.request<Folder>(
|
|
||||||
`${this.serverUrl}${subFolderUrl}`,
|
|
||||||
requestInfo
|
|
||||||
)
|
|
||||||
|
|
||||||
const membersLink = memberDetail.links.find(
|
|
||||||
(l: any) => l.rel === 'members'
|
|
||||||
)
|
|
||||||
|
|
||||||
const { result: memberContents } = await this.request<{ items: any[] }>(
|
|
||||||
`${this.serverUrl}${membersLink!.href}`,
|
|
||||||
requestInfo
|
|
||||||
)
|
|
||||||
const itemsInFolder = memberContents.items as any[]
|
|
||||||
allItems.set(member.name, itemsInFolder)
|
|
||||||
return itemsInFolder
|
|
||||||
})
|
|
||||||
await Promise.all(subfolderRequests)
|
|
||||||
|
|
||||||
this.rootFolderMap = allItems
|
|
||||||
}
|
|
||||||
|
|
||||||
private async populateRootFolder(accessToken?: string) {
|
|
||||||
const url = '/folders/folders/@item?path=' + this.rootFolderName
|
|
||||||
const requestInfo: RequestInit = {
|
|
||||||
method: 'GET'
|
|
||||||
}
|
|
||||||
if (accessToken) {
|
|
||||||
requestInfo.headers = { Authorization: `Bearer ${accessToken}` }
|
|
||||||
}
|
|
||||||
let error
|
|
||||||
const rootFolder = await this.request<Folder>(
|
|
||||||
`${this.serverUrl}${url}`,
|
|
||||||
requestInfo
|
|
||||||
)
|
|
||||||
|
|
||||||
this.rootFolder = rootFolder?.result || null
|
|
||||||
if (error) {
|
|
||||||
throw new Error(JSON.stringify(error))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async pollJobState(
|
private async pollJobState(
|
||||||
postedJob: any,
|
postedJob: any,
|
||||||
etag: string | null,
|
etag: string | null,
|
||||||
accessToken?: string,
|
accessToken?: string
|
||||||
silent = false
|
|
||||||
) {
|
) {
|
||||||
const MAX_POLL_COUNT = 1000
|
const MAX_POLL_COUNT = 1000
|
||||||
const POLL_INTERVAL = 100
|
const POLL_INTERVAL = 100
|
||||||
@@ -1225,7 +1277,7 @@ export class SASViyaApiClient {
|
|||||||
postedJobState === 'pending'
|
postedJobState === 'pending'
|
||||||
) {
|
) {
|
||||||
if (stateLink) {
|
if (stateLink) {
|
||||||
if (!silent) {
|
if (this.debug) {
|
||||||
console.log('Polling job status... \n')
|
console.log('Polling job status... \n')
|
||||||
}
|
}
|
||||||
const { result: jobState } = await this.request<string>(
|
const { result: jobState } = await this.request<string>(
|
||||||
@@ -1237,7 +1289,7 @@ export class SASViyaApiClient {
|
|||||||
)
|
)
|
||||||
|
|
||||||
postedJobState = jobState.trim()
|
postedJobState = jobState.trim()
|
||||||
if (!silent) {
|
if (this.debug) {
|
||||||
console.log(`Current state: ${postedJobState}\n`)
|
console.log(`Current state: ${postedJobState}\n`)
|
||||||
}
|
}
|
||||||
pollCount++
|
pollCount++
|
||||||
@@ -1253,49 +1305,6 @@ export class SASViyaApiClient {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private async waitForSession(
|
|
||||||
session: Session,
|
|
||||||
etag: string | null,
|
|
||||||
accessToken?: string,
|
|
||||||
silent = false
|
|
||||||
) {
|
|
||||||
let sessionState = session.state
|
|
||||||
let pollCount = 0
|
|
||||||
const headers: any = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'If-None-Match': etag
|
|
||||||
}
|
|
||||||
if (accessToken) {
|
|
||||||
headers.Authorization = `Bearer ${accessToken}`
|
|
||||||
}
|
|
||||||
const stateLink = session.links.find((l: any) => l.rel === 'state')
|
|
||||||
return new Promise(async (resolve, _) => {
|
|
||||||
if (sessionState === 'pending') {
|
|
||||||
if (stateLink) {
|
|
||||||
if (!silent) {
|
|
||||||
console.log('Polling session status... \n')
|
|
||||||
}
|
|
||||||
const { result: state } = await this.request<string>(
|
|
||||||
`${this.serverUrl}${stateLink.href}?wait=30`,
|
|
||||||
{
|
|
||||||
headers
|
|
||||||
},
|
|
||||||
'text'
|
|
||||||
)
|
|
||||||
|
|
||||||
sessionState = state.trim()
|
|
||||||
if (!silent) {
|
|
||||||
console.log(`Current state: ${sessionState}\n`)
|
|
||||||
}
|
|
||||||
pollCount++
|
|
||||||
resolve(sessionState)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
resolve(sessionState)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private async uploadTables(data: any, accessToken?: string) {
|
private async uploadTables(data: any, accessToken?: string) {
|
||||||
const uploadedFiles = []
|
const uploadedFiles = []
|
||||||
const headers: any = {
|
const headers: any = {
|
||||||
@@ -1321,7 +1330,9 @@ export class SASViyaApiClient {
|
|||||||
|
|
||||||
const uploadResponse = await this.request<any>(
|
const uploadResponse = await this.request<any>(
|
||||||
`${this.serverUrl}/files/files#rawUpload`,
|
`${this.serverUrl}/files/files#rawUpload`,
|
||||||
createFileRequest
|
createFileRequest,
|
||||||
|
'json',
|
||||||
|
'fileUpload'
|
||||||
)
|
)
|
||||||
|
|
||||||
uploadedFiles.push({ tableName, file: uploadResponse.result })
|
uploadedFiles.push({ tableName, file: uploadResponse.result })
|
||||||
@@ -1370,7 +1381,13 @@ export class SASViyaApiClient {
|
|||||||
return `/folders/folders/${folder.id}`
|
return `/folders/folders/${folder.id}`
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getContextByName(
|
/**
|
||||||
|
* Returns a JSON representation of a compute context.
|
||||||
|
* @example: { "createdBy": "admin", "links": [...], "id": "ID", "version": 2, "name": "context1" }
|
||||||
|
* @param contextName - the name of the context to return.
|
||||||
|
* @param accessToken - an access token for an authorized user.
|
||||||
|
*/
|
||||||
|
public async getComputeContextByName(
|
||||||
contextName: string,
|
contextName: string,
|
||||||
accessToken?: string
|
accessToken?: string
|
||||||
): Promise<Context> {
|
): Promise<Context> {
|
||||||
@@ -1385,9 +1402,7 @@ export class SASViyaApiClient {
|
|||||||
const { result: contexts } = await this.request<{ items: Context[] }>(
|
const { result: contexts } = await this.request<{ items: Context[] }>(
|
||||||
`${this.serverUrl}/compute/contexts?filter=eq(name, "${contextName}")`,
|
`${this.serverUrl}/compute/contexts?filter=eq(name, "${contextName}")`,
|
||||||
{ headers }
|
{ headers }
|
||||||
).catch((err) => {
|
)
|
||||||
throw err
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!contexts || !(contexts.items && contexts.items.length)) {
|
if (!contexts || !(contexts.items && contexts.items.length)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -1398,6 +1413,33 @@ export class SASViyaApiClient {
|
|||||||
return contexts.items[0]
|
return contexts.items[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a JSON representation of a compute context.
|
||||||
|
* @param contextId - an id of the context to return.
|
||||||
|
* @param accessToken - an access token for an authorized user.
|
||||||
|
*/
|
||||||
|
public async getComputeContextById(
|
||||||
|
contextId: string,
|
||||||
|
accessToken?: string
|
||||||
|
): Promise<ContextAllAttributes> {
|
||||||
|
const headers: any = {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accessToken) {
|
||||||
|
headers.Authorization = `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const { result: context } = await this.request<ContextAllAttributes>(
|
||||||
|
`${this.serverUrl}/compute/contexts/${contextId}`,
|
||||||
|
{ headers }
|
||||||
|
).catch((err) => {
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Moves a Viya folder to a new location. The folder may be renamed at the same time.
|
* Moves a Viya folder to a new location. The folder may be renamed at the same time.
|
||||||
* @param sourceFolder - the full path (eg `/Public/example/myFolder`) or URI of the source folder to be moved. Providing URI instead of path will save one extra request.
|
* @param sourceFolder - the full path (eg `/Public/example/myFolder`) or URI of the source folder to be moved. Providing URI instead of path will save one extra request.
|
||||||
@@ -1441,6 +1483,16 @@ export class SASViyaApiClient {
|
|||||||
`${this.serverUrl}${url}`,
|
`${this.serverUrl}${url}`,
|
||||||
requestInfo
|
requestInfo
|
||||||
).catch((err) => {
|
).catch((err) => {
|
||||||
|
if (err.code && err.code === 'ENOTFOUND') {
|
||||||
|
const notFoundError = {
|
||||||
|
body: JSON.stringify({
|
||||||
|
message: `Folder '${sourceFolder.split('/').pop()}' was not found.`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
throw notFoundError
|
||||||
|
}
|
||||||
|
|
||||||
throw err
|
throw err
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1476,22 +1528,36 @@ export class SASViyaApiClient {
|
|||||||
this.setCsrfToken(csrfToken)
|
this.setCsrfToken(csrfToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setFileUploadCsrfToken = (csrfToken: CsrfToken) => {
|
||||||
|
this.fileUploadCsrfToken = csrfToken
|
||||||
|
}
|
||||||
|
|
||||||
private async request<T>(
|
private async request<T>(
|
||||||
url: string,
|
url: string,
|
||||||
options: RequestInit,
|
options: RequestInit,
|
||||||
contentType: 'text' | 'json' = 'json'
|
contentType: 'text' | 'json' = 'json',
|
||||||
|
type: 'fileUpload' | 'other' = 'other'
|
||||||
) {
|
) {
|
||||||
|
const callback =
|
||||||
|
type === 'fileUpload'
|
||||||
|
? this.setFileUploadCsrfToken
|
||||||
|
: this.setCsrfTokenLocal
|
||||||
|
|
||||||
|
if (type === 'other') {
|
||||||
if (this.csrfToken) {
|
if (this.csrfToken) {
|
||||||
options.headers = {
|
options.headers = {
|
||||||
...options.headers,
|
...options.headers,
|
||||||
[this.csrfToken.headerName]: this.csrfToken.value
|
[this.csrfToken.headerName]: this.csrfToken.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return await makeRequest<T>(
|
} else {
|
||||||
url,
|
if (this.fileUploadCsrfToken) {
|
||||||
options,
|
options.headers = {
|
||||||
this.setCsrfTokenLocal,
|
...options.headers,
|
||||||
contentType
|
[this.fileUploadCsrfToken.headerName]: this.fileUploadCsrfToken.value
|
||||||
)
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return await makeRequest<T>(url, options, callback, contentType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
317
src/SASjs.ts
317
src/SASjs.ts
@@ -21,7 +21,8 @@ import {
|
|||||||
parseGeneratedCode,
|
parseGeneratedCode,
|
||||||
parseWeboutResponse,
|
parseWeboutResponse,
|
||||||
needsRetry,
|
needsRetry,
|
||||||
asyncForEach
|
asyncForEach,
|
||||||
|
isRelativePath
|
||||||
} from './utils'
|
} from './utils'
|
||||||
import {
|
import {
|
||||||
SASjsConfig,
|
SASjsConfig,
|
||||||
@@ -43,7 +44,7 @@ const defaultConfig: SASjsConfig = {
|
|||||||
pathSASViya: '/SASJobExecution',
|
pathSASViya: '/SASJobExecution',
|
||||||
appLoc: '/Public/seedapp',
|
appLoc: '/Public/seedapp',
|
||||||
serverType: ServerType.SASViya,
|
serverType: ServerType.SASViya,
|
||||||
debug: true,
|
debug: false,
|
||||||
contextName: 'SAS Job Execution compute context',
|
contextName: 'SAS Job Execution compute context',
|
||||||
useComputeApi: false
|
useComputeApi: false
|
||||||
}
|
}
|
||||||
@@ -112,16 +113,16 @@ export default class SASjs {
|
|||||||
* @param launchContextName - the name of the launcher context used by the compute service.
|
* @param launchContextName - the name of the launcher context used by the compute service.
|
||||||
* @param sharedAccountId - the ID of the account to run the servers for this context as.
|
* @param sharedAccountId - the ID of the account to run the servers for this context as.
|
||||||
* @param autoExecLines - the lines of code to execute during session initialization.
|
* @param autoExecLines - the lines of code to execute during session initialization.
|
||||||
* @param authorizedUsers - an optional list of authorized user IDs.
|
|
||||||
* @param accessToken - an access token for an authorized user.
|
* @param accessToken - an access token for an authorized user.
|
||||||
|
* @param authorizedUsers - an optional list of authorized user IDs.
|
||||||
*/
|
*/
|
||||||
public async createContext(
|
public async createContext(
|
||||||
contextName: string,
|
contextName: string,
|
||||||
launchContextName: string,
|
launchContextName: string,
|
||||||
sharedAccountId: string,
|
sharedAccountId: string,
|
||||||
autoExecLines: string[],
|
autoExecLines: string[],
|
||||||
authorizedUsers: string[],
|
accessToken: string,
|
||||||
accessToken: string
|
authorizedUsers?: string[]
|
||||||
) {
|
) {
|
||||||
this.isMethodSupported('createContext', ServerType.SASViya)
|
this.isMethodSupported('createContext', ServerType.SASViya)
|
||||||
|
|
||||||
@@ -130,8 +131,8 @@ export default class SASjs {
|
|||||||
launchContextName,
|
launchContextName,
|
||||||
sharedAccountId,
|
sharedAccountId,
|
||||||
autoExecLines,
|
autoExecLines,
|
||||||
authorizedUsers,
|
accessToken,
|
||||||
accessToken
|
authorizedUsers
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,6 +167,38 @@ export default class SASjs {
|
|||||||
return await this.sasViyaApiClient!.deleteContext(contextName, accessToken)
|
return await this.sasViyaApiClient!.deleteContext(contextName, accessToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a JSON representation of a compute context.
|
||||||
|
* @example: { "createdBy": "admin", "links": [...], "id": "ID", "version": 2, "name": "context1" }
|
||||||
|
* @param contextName - the name of the context to return.
|
||||||
|
* @param accessToken - an access token for an authorized user.
|
||||||
|
*/
|
||||||
|
public async getComputeContextByName(
|
||||||
|
contextName: string,
|
||||||
|
accessToken?: string
|
||||||
|
) {
|
||||||
|
this.isMethodSupported('getComputeContextByName', ServerType.SASViya)
|
||||||
|
|
||||||
|
return await this.sasViyaApiClient!.getComputeContextByName(
|
||||||
|
contextName,
|
||||||
|
accessToken
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a JSON representation of a compute context.
|
||||||
|
* @param contextId - an id of the context to return.
|
||||||
|
* @param accessToken - an access token for an authorized user.
|
||||||
|
*/
|
||||||
|
public async getComputeContextById(contextId: string, accessToken?: string) {
|
||||||
|
this.isMethodSupported('getComputeContextById', ServerType.SASViya)
|
||||||
|
|
||||||
|
return await this.sasViyaApiClient!.getComputeContextById(
|
||||||
|
contextId,
|
||||||
|
accessToken
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
public async createSession(contextName: string, accessToken: string) {
|
public async createSession(contextName: string, accessToken: string) {
|
||||||
this.isMethodSupported('createSession', ServerType.SASViya)
|
this.isMethodSupported('createSession', ServerType.SASViya)
|
||||||
|
|
||||||
@@ -176,9 +209,7 @@ export default class SASjs {
|
|||||||
fileName: string,
|
fileName: string,
|
||||||
linesOfCode: string[],
|
linesOfCode: string[],
|
||||||
contextName: string,
|
contextName: string,
|
||||||
accessToken?: string,
|
accessToken?: string
|
||||||
sessionId = '',
|
|
||||||
silent = false
|
|
||||||
) {
|
) {
|
||||||
this.isMethodSupported('executeScriptSASViya', ServerType.SASViya)
|
this.isMethodSupported('executeScriptSASViya', ServerType.SASViya)
|
||||||
|
|
||||||
@@ -187,9 +218,7 @@ export default class SASjs {
|
|||||||
linesOfCode,
|
linesOfCode,
|
||||||
contextName,
|
contextName,
|
||||||
accessToken,
|
accessToken,
|
||||||
silent,
|
null
|
||||||
null,
|
|
||||||
this.sasjsConfig.debug
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,8 +239,6 @@ export default class SASjs {
|
|||||||
sasApiClient?: SASViyaApiClient,
|
sasApiClient?: SASViyaApiClient,
|
||||||
isForced?: boolean
|
isForced?: boolean
|
||||||
) {
|
) {
|
||||||
this.isMethodSupported('createFolder', ServerType.SASViya)
|
|
||||||
|
|
||||||
if (sasApiClient)
|
if (sasApiClient)
|
||||||
return await sasApiClient.createFolder(
|
return await sasApiClient.createFolder(
|
||||||
folderName,
|
folderName,
|
||||||
@@ -228,6 +255,40 @@ export default class SASjs {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For performance (and in case of accidental error) the `deleteFolder` function does not actually delete the folder (and all its content and subfolder content). Instead the folder is simply moved to the recycle bin. Deletion time will be added to the folder name.
|
||||||
|
* @param folderPath - the full path (eg `/Public/example/deleteThis`) of the folder to be deleted.
|
||||||
|
* @param accessToken - an access token for authorizing the request.
|
||||||
|
*/
|
||||||
|
public async deleteFolder(folderPath: string, accessToken: string) {
|
||||||
|
this.isMethodSupported('deleteFolder', ServerType.SASViya)
|
||||||
|
|
||||||
|
return await this.sasViyaApiClient?.deleteFolder(folderPath, accessToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves folder to a new location. The folder may be renamed at the same time.
|
||||||
|
* @param sourceFolder - the full path (eg `/Public/example/myFolder`) or URI of the source folder to be moved. Providing URI instead of path will save one extra request.
|
||||||
|
* @param targetParentFolder - the full path or URI of the _parent_ folder to which the `sourceFolder` will be moved (eg `/Public/newDestination`). To move a folder, a user has to have write permissions in targetParentFolder. Providing URI instead of path will save one extra request.
|
||||||
|
* @param targetFolderName - the name of the "moved" folder. If left blank, the original folder name will be used (eg `myFolder` in `/Public/newDestination/myFolder` for the example above). Optional field.
|
||||||
|
* @param accessToken - an access token for authorizing the request.
|
||||||
|
*/
|
||||||
|
public async moveFolder(
|
||||||
|
sourceFolder: string,
|
||||||
|
targetParentFolder: string,
|
||||||
|
targetFolderName: string,
|
||||||
|
accessToken: string
|
||||||
|
) {
|
||||||
|
this.isMethodSupported('moveFolder', ServerType.SASViya)
|
||||||
|
|
||||||
|
return await this.sasViyaApiClient?.moveFolder(
|
||||||
|
sourceFolder,
|
||||||
|
targetParentFolder,
|
||||||
|
targetFolderName,
|
||||||
|
accessToken
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
public async createJobDefinition(
|
public async createJobDefinition(
|
||||||
jobName: string,
|
jobName: string,
|
||||||
code: string,
|
code: string,
|
||||||
@@ -345,6 +406,32 @@ export default class SASjs {
|
|||||||
*/
|
*/
|
||||||
public setDebugState(value: boolean) {
|
public setDebugState(value: boolean) {
|
||||||
this.sasjsConfig.debug = value
|
this.sasjsConfig.debug = value
|
||||||
|
if (this.sasViyaApiClient) {
|
||||||
|
this.sasViyaApiClient.debug = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getLoginForm(response: any) {
|
||||||
|
const pattern: RegExp = /<form.+action="(.*Logon[^"]*).*>/
|
||||||
|
const matches = pattern.exec(response)
|
||||||
|
const formInputs: any = {}
|
||||||
|
|
||||||
|
if (matches && matches.length) {
|
||||||
|
this.setLoginUrl(matches)
|
||||||
|
const inputs = response.match(/<input.*"hidden"[^>]*>/g)
|
||||||
|
|
||||||
|
if (inputs) {
|
||||||
|
inputs.forEach((inputStr: string) => {
|
||||||
|
const valueMatch = inputStr.match(/name="([^"]*)"\svalue="([^"]*)/)
|
||||||
|
|
||||||
|
if (valueMatch && valueMatch.length) {
|
||||||
|
formInputs[valueMatch[1]] = valueMatch[2]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(formInputs).length ? formInputs : null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -355,10 +442,16 @@ export default class SASjs {
|
|||||||
const loginResponse = await fetch(this.loginUrl.replace('.do', ''))
|
const loginResponse = await fetch(this.loginUrl.replace('.do', ''))
|
||||||
const responseText = await loginResponse.text()
|
const responseText = await loginResponse.text()
|
||||||
const isLoggedIn = /<button.+onClick.+logout/gm.test(responseText)
|
const isLoggedIn = /<button.+onClick.+logout/gm.test(responseText)
|
||||||
|
let loginForm: any = null
|
||||||
|
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
loginForm = await this.getLoginForm(responseText)
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
isLoggedIn,
|
isLoggedIn,
|
||||||
userName: this.userName
|
userName: this.userName,
|
||||||
|
loginForm
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,7 +469,7 @@ export default class SASjs {
|
|||||||
|
|
||||||
this.userName = loginParams.username
|
this.userName = loginParams.username
|
||||||
|
|
||||||
const { isLoggedIn } = await this.checkSession()
|
const { isLoggedIn, loginForm } = await this.checkSession()
|
||||||
if (isLoggedIn) {
|
if (isLoggedIn) {
|
||||||
this.resendWaitingRequests()
|
this.resendWaitingRequests()
|
||||||
|
|
||||||
@@ -386,15 +479,13 @@ export default class SASjs {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const loginForm = await this.getLoginForm()
|
|
||||||
|
|
||||||
for (const key in loginForm) {
|
for (const key in loginForm) {
|
||||||
loginParams[key] = loginForm[key]
|
loginParams[key] = loginForm[key]
|
||||||
}
|
}
|
||||||
const loginParamsStr = serialize(loginParams)
|
const loginParamsStr = serialize(loginParams)
|
||||||
|
|
||||||
return fetch(this.loginUrl, {
|
return fetch(this.loginUrl, {
|
||||||
method: 'post',
|
method: 'POST',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
referrerPolicy: 'same-origin',
|
referrerPolicy: 'same-origin',
|
||||||
body: loginParamsStr,
|
body: loginParamsStr,
|
||||||
@@ -501,8 +592,6 @@ export default class SASjs {
|
|||||||
...config
|
...config
|
||||||
}
|
}
|
||||||
|
|
||||||
sasJob = sasJob.startsWith('/') ? sasJob.replace('/', '') : sasJob
|
|
||||||
|
|
||||||
if (config.serverType === ServerType.SASViya && config.contextName) {
|
if (config.serverType === ServerType.SASViya && config.contextName) {
|
||||||
if (config.useComputeApi) {
|
if (config.useComputeApi) {
|
||||||
requestResponse = await this.executeJobViaComputeApi(
|
requestResponse = await this.executeJobViaComputeApi(
|
||||||
@@ -572,6 +661,7 @@ export default class SASjs {
|
|||||||
this.sasjsConfig.contextName,
|
this.sasjsConfig.contextName,
|
||||||
this.setCsrfTokenApi
|
this.setCsrfTokenApi
|
||||||
)
|
)
|
||||||
|
sasApiClient.debug = this.sasjsConfig.debug
|
||||||
} else if (this.sasjsConfig.serverType === ServerType.SAS9) {
|
} else if (this.sasjsConfig.serverType === ServerType.SAS9) {
|
||||||
sasApiClient = new SAS9ApiClient(serverUrl)
|
sasApiClient = new SAS9ApiClient(serverUrl)
|
||||||
}
|
}
|
||||||
@@ -607,6 +697,50 @@ export default class SASjs {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kicks off execution of the given job via the compute API.
|
||||||
|
* @returns an object representing the compute session created for the given job.
|
||||||
|
* @param sasJob - the path to the SAS program (ultimately resolves to
|
||||||
|
* the SAS `_program` parameter to run a Job Definition or SAS 9 Stored
|
||||||
|
* Process). Is prepended at runtime with the value of `appLoc`.
|
||||||
|
* @param data - a JSON object containing one or more tables to be sent to
|
||||||
|
* SAS. Can be `null` if no inputs required.
|
||||||
|
* @param config - provide any changes to the config here, for instance to
|
||||||
|
* enable/disable `debug`. Any change provided will override the global config,
|
||||||
|
* for that particular function call.
|
||||||
|
* @param accessToken - a valid access token that is authorised to execute compute jobs.
|
||||||
|
* The access token is not required when the user is authenticated via the browser.
|
||||||
|
* @param waitForResult - a boolean that indicates whether the function needs to wait for execution to complete.
|
||||||
|
*/
|
||||||
|
public async startComputeJob(
|
||||||
|
sasJob: string,
|
||||||
|
data: any,
|
||||||
|
config: any = {},
|
||||||
|
accessToken?: string,
|
||||||
|
waitForResult?: boolean
|
||||||
|
) {
|
||||||
|
config = {
|
||||||
|
...this.sasjsConfig,
|
||||||
|
...config
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isMethodSupported('startComputeJob', ServerType.SASViya)
|
||||||
|
if (!config.contextName) {
|
||||||
|
throw new Error(
|
||||||
|
'Context name is undefined. Please set a `contextName` in your SASjs or override config.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.sasViyaApiClient?.executeComputeJob(
|
||||||
|
sasJob,
|
||||||
|
config.contextName,
|
||||||
|
data,
|
||||||
|
accessToken,
|
||||||
|
!!waitForResult,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private async executeJobViaComputeApi(
|
private async executeJobViaComputeApi(
|
||||||
sasJob: string,
|
sasJob: string,
|
||||||
data: any,
|
data: any,
|
||||||
@@ -626,13 +760,16 @@ export default class SASjs {
|
|||||||
|
|
||||||
sasjsWaitingRequest.requestPromise.promise = new Promise(
|
sasjsWaitingRequest.requestPromise.promise = new Promise(
|
||||||
async (resolve, reject) => {
|
async (resolve, reject) => {
|
||||||
|
const waitForResult = true
|
||||||
|
const expectWebout = true
|
||||||
this.sasViyaApiClient
|
this.sasViyaApiClient
|
||||||
?.executeComputeJob(
|
?.executeComputeJob(
|
||||||
sasJob,
|
sasJob,
|
||||||
config.contextName,
|
config.contextName,
|
||||||
config.debug,
|
|
||||||
data,
|
data,
|
||||||
accessToken
|
accessToken,
|
||||||
|
waitForResult,
|
||||||
|
expectWebout
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (!config.debug) {
|
if (!config.debug) {
|
||||||
@@ -670,11 +807,23 @@ export default class SASjs {
|
|||||||
} else {
|
} else {
|
||||||
this.retryCountComputeApi = 0
|
this.retryCountComputeApi = 0
|
||||||
reject(
|
reject(
|
||||||
new ErrorResponse('Compute API retry requests limit reached')
|
new ErrorResponse('Compute API retry requests limit reached.')
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (response?.log) {
|
||||||
|
this.appendSasjsRequest(response.log, sasJob, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.toString().includes('Job was not found')) {
|
||||||
|
reject(
|
||||||
|
new ErrorResponse('Service not found on the server.', {
|
||||||
|
sasJob: sasJob
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (error && error.status === 401) {
|
if (error && error.status === 401) {
|
||||||
if (loginRequiredCallback) loginRequiredCallback(true)
|
if (loginRequiredCallback) loginRequiredCallback(true)
|
||||||
sasjsWaitingRequest.requestPromise.resolve = resolve
|
sasjsWaitingRequest.requestPromise.resolve = resolve
|
||||||
@@ -682,10 +831,8 @@ export default class SASjs {
|
|||||||
sasjsWaitingRequest.config = config
|
sasjsWaitingRequest.config = config
|
||||||
this.sasjsWaitingRequests.push(sasjsWaitingRequest)
|
this.sasjsWaitingRequests.push(sasjsWaitingRequest)
|
||||||
} else {
|
} else {
|
||||||
reject(new ErrorResponse('Job execution failed', error))
|
reject(new ErrorResponse('Job execution failed.', error))
|
||||||
}
|
}
|
||||||
|
|
||||||
this.appendSasjsRequest(response.log, sasJob, null)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -765,12 +912,24 @@ export default class SASjs {
|
|||||||
} else {
|
} else {
|
||||||
this.retryCountJeseApi = 0
|
this.retryCountJeseApi = 0
|
||||||
reject(
|
reject(
|
||||||
new ErrorResponse('Jes API retry requests limit reached')
|
new ErrorResponse('Jes API retry requests limit reached.')
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reject(new ErrorResponse('Job execution failed', e))
|
if (e?.log) {
|
||||||
|
this.appendSasjsRequest(e.log, sasJob, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.toString().includes('Job was not found')) {
|
||||||
|
reject(
|
||||||
|
new ErrorResponse('Service not found on the server.', {
|
||||||
|
sasJob: sasJob
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
reject(new ErrorResponse('Job execution failed.', e))
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -794,11 +953,15 @@ export default class SASjs {
|
|||||||
SASjob: sasJob,
|
SASjob: sasJob,
|
||||||
data
|
data
|
||||||
}
|
}
|
||||||
const program = config.appLoc
|
const program = isRelativePath(sasJob)
|
||||||
|
? config.appLoc
|
||||||
? config.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
|
? config.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
|
||||||
: sasJob
|
: sasJob
|
||||||
|
: sasJob
|
||||||
const jobUri =
|
const jobUri =
|
||||||
config.serverType === 'SASVIYA' ? await this.getJobUri(sasJob) : ''
|
config.serverType === ServerType.SASViya
|
||||||
|
? await this.getJobUri(sasJob)
|
||||||
|
: ''
|
||||||
const apiUrl = `${config.serverUrl}${this.jobsPath}/?${
|
const apiUrl = `${config.serverUrl}${this.jobsPath}/?${
|
||||||
jobUri.length > 0
|
jobUri.length > 0
|
||||||
? '__program=' + program + '&_job=' + jobUri
|
? '__program=' + program + '&_job=' + jobUri
|
||||||
@@ -950,7 +1113,7 @@ export default class SASjs {
|
|||||||
} else {
|
} else {
|
||||||
reject(
|
reject(
|
||||||
new ErrorResponse(
|
new ErrorResponse(
|
||||||
'Job WEB execution failed',
|
'Job WEB execution failed.',
|
||||||
this.parseSAS9ErrorResponse(responseText)
|
this.parseSAS9ErrorResponse(responseText)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -968,7 +1131,7 @@ export default class SASjs {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
reject(
|
reject(
|
||||||
new ErrorResponse(
|
new ErrorResponse(
|
||||||
'Job WEB debug response parsing failed',
|
'Job WEB debug response parsing failed.',
|
||||||
{ response: resText, exception: e }
|
{ response: resText, exception: e }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -977,7 +1140,7 @@ export default class SASjs {
|
|||||||
(err: any) => {
|
(err: any) => {
|
||||||
reject(
|
reject(
|
||||||
new ErrorResponse(
|
new ErrorResponse(
|
||||||
'Job WEB debug response parsing failed',
|
'Job WEB debug response parsing failed.',
|
||||||
err
|
err
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -986,19 +1149,34 @@ export default class SASjs {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
reject(
|
reject(
|
||||||
new ErrorResponse(
|
new ErrorResponse(
|
||||||
'Job WEB debug response parsing failed',
|
'Job WEB debug response parsing failed.',
|
||||||
{ response: responseText, exception: e }
|
{ response: responseText, exception: e }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.updateUsername(responseText)
|
this.updateUsername(responseText)
|
||||||
|
if (
|
||||||
|
responseText.includes(
|
||||||
|
'The requested URL /SASStoredProcess/do/ was not found on this server.'
|
||||||
|
) ||
|
||||||
|
responseText.includes('Stored process not found')
|
||||||
|
) {
|
||||||
|
reject(
|
||||||
|
new ErrorResponse(
|
||||||
|
'Service not found on the server.',
|
||||||
|
{ service: sasJob },
|
||||||
|
responseText
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsedJson = JSON.parse(responseText)
|
const parsedJson = JSON.parse(responseText)
|
||||||
resolve(parsedJson)
|
resolve(parsedJson)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
reject(
|
reject(
|
||||||
new ErrorResponse('Job WEB response parsing failed', {
|
new ErrorResponse('Job WEB response parsing failed.', {
|
||||||
response: responseText,
|
response: responseText,
|
||||||
exception: e
|
exception: e
|
||||||
})
|
})
|
||||||
@@ -1009,7 +1187,7 @@ export default class SASjs {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((e: Error) => {
|
.catch((e: Error) => {
|
||||||
reject(new ErrorResponse('Job WEB request failed', e))
|
reject(new ErrorResponse('Job WEB request failed.', e))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -1091,14 +1269,20 @@ export default class SASjs {
|
|||||||
|
|
||||||
private async getJobUri(sasJob: string) {
|
private async getJobUri(sasJob: string) {
|
||||||
if (!this.sasViyaApiClient) return ''
|
if (!this.sasViyaApiClient) return ''
|
||||||
const jobMap: any = await this.sasViyaApiClient.getAppLocMap()
|
|
||||||
let uri = ''
|
let uri = ''
|
||||||
|
|
||||||
if (jobMap.size) {
|
let folderPath
|
||||||
const jobKey = sasJob.split('/')[0]
|
let jobName: string
|
||||||
const jobName = sasJob.split('/')[1]
|
if (isRelativePath(sasJob)) {
|
||||||
|
folderPath = sasJob.split('/')[0]
|
||||||
|
jobName = sasJob.split('/')[1]
|
||||||
|
} else {
|
||||||
|
const folderPathParts = sasJob.split('/')
|
||||||
|
jobName = folderPathParts.pop() || ''
|
||||||
|
folderPath = folderPathParts.join('/')
|
||||||
|
}
|
||||||
|
|
||||||
const locJobs = jobMap.get(jobKey)
|
const locJobs = await this.sasViyaApiClient.getJobsInFolder(folderPath)
|
||||||
if (locJobs) {
|
if (locJobs) {
|
||||||
const job = locJobs.find(
|
const job = locJobs.find(
|
||||||
(el: any) => el.name === jobName && el.contentType === 'jobDefinition'
|
(el: any) => el.name === jobName && el.contentType === 'jobDefinition'
|
||||||
@@ -1107,7 +1291,6 @@ export default class SASjs {
|
|||||||
uri = job.uri
|
uri = job.uri
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return uri
|
return uri
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1145,10 +1328,20 @@ export default class SASjs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fetchLogFileContent(logLink: string) {
|
/**
|
||||||
|
* Fetches content of the log file
|
||||||
|
* @param logLink - url of the log file.
|
||||||
|
* @param accessToken - an access token for an authorized user.
|
||||||
|
*/
|
||||||
|
public fetchLogFileContent(logLink: string, accessToken?: string) {
|
||||||
|
const headers: any = { 'Content-Type': 'application/json' }
|
||||||
|
|
||||||
|
if (accessToken) headers.Authorization = 'Bearer ' + accessToken
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
fetch(logLink, {
|
fetch(logLink, {
|
||||||
method: 'GET'
|
method: 'GET',
|
||||||
|
headers
|
||||||
})
|
})
|
||||||
.then((response: any) => response.text())
|
.then((response: any) => response.text())
|
||||||
.then((response: any) => resolve(response))
|
.then((response: any) => resolve(response))
|
||||||
@@ -1246,11 +1439,15 @@ export default class SASjs {
|
|||||||
this.sasjsConfig.serverUrl === undefined ||
|
this.sasjsConfig.serverUrl === undefined ||
|
||||||
this.sasjsConfig.serverUrl === ''
|
this.sasjsConfig.serverUrl === ''
|
||||||
) {
|
) {
|
||||||
|
if (typeof location !== 'undefined') {
|
||||||
let url = `${location.protocol}//${location.hostname}`
|
let url = `${location.protocol}//${location.hostname}`
|
||||||
if (location.port) {
|
|
||||||
url = `${url}:${location.port}`
|
if (location.port) url = `${url}:${location.port}`
|
||||||
}
|
|
||||||
this.sasjsConfig.serverUrl = url
|
this.sasjsConfig.serverUrl = url
|
||||||
|
} else {
|
||||||
|
this.sasjsConfig.serverUrl = ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.sasjsConfig.serverUrl.slice(-1) === '/') {
|
if (this.sasjsConfig.serverUrl.slice(-1) === '/') {
|
||||||
@@ -1280,6 +1477,8 @@ export default class SASjs {
|
|||||||
this.sasjsConfig.contextName,
|
this.sasjsConfig.contextName,
|
||||||
this.setCsrfTokenApi
|
this.setCsrfTokenApi
|
||||||
)
|
)
|
||||||
|
|
||||||
|
this.sasViyaApiClient.debug = this.sasjsConfig.debug
|
||||||
}
|
}
|
||||||
if (this.sasjsConfig.serverType === ServerType.SAS9) {
|
if (this.sasjsConfig.serverType === ServerType.SAS9) {
|
||||||
if (this.sas9ApiClient)
|
if (this.sas9ApiClient)
|
||||||
@@ -1313,26 +1512,6 @@ export default class SASjs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getLoginForm() {
|
|
||||||
const pattern: RegExp = /<form.+action="(.*Logon[^"]*).*>/
|
|
||||||
const response = await fetch(this.loginUrl).then((r) => r.text())
|
|
||||||
const matches = pattern.exec(response)
|
|
||||||
const formInputs: any = {}
|
|
||||||
if (matches && matches.length) {
|
|
||||||
this.setLoginUrl(matches)
|
|
||||||
const inputs = response.match(/<input.*"hidden"[^>]*>/g)
|
|
||||||
if (inputs) {
|
|
||||||
inputs.forEach((inputStr: string) => {
|
|
||||||
const valueMatch = inputStr.match(/name="([^"]*)"\svalue="([^"]*)/)
|
|
||||||
if (valueMatch && valueMatch.length) {
|
|
||||||
formInputs[valueMatch[1]] = valueMatch[2]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Object.keys(formInputs).length ? formInputs : null
|
|
||||||
}
|
|
||||||
|
|
||||||
private async createFoldersAndServices(
|
private async createFoldersAndServices(
|
||||||
parentFolder: string,
|
parentFolder: string,
|
||||||
membersJson: any[],
|
membersJson: any[],
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user