mirror of
https://github.com/sasjs/adapter.git
synced 2025-12-11 09:24:35 +00:00
Compare commits
136 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fef65bbfd2 | ||
|
|
efeba71612 | ||
|
|
8f54002b1e | ||
|
|
9d6882799d | ||
|
|
73a3acee68 | ||
|
|
0a88220e04 | ||
|
|
c8e1779272 | ||
|
|
8bd3580e23 | ||
|
|
f732b32873 | ||
|
|
65b18f9148 | ||
|
|
10b1676a35 | ||
|
|
b9bd09d3e8 | ||
|
|
537f687b94 | ||
|
|
bfd532f813 | ||
|
|
4f2b4f46a8 | ||
|
|
077cc9458d | ||
|
|
0a7ab394a4 | ||
|
|
f873febfde | ||
|
|
55e8ce359b | ||
|
|
99d7c8f119 | ||
|
|
b3c90f09d6 | ||
|
|
2401962c53 | ||
|
|
362b4d4db3 | ||
|
|
8aea325139 | ||
|
|
bb370061a2 | ||
|
|
48442f7769 | ||
|
|
e67a8531ce | ||
|
|
ef4f020e2a | ||
|
|
2feceeb2f9 | ||
|
|
eaec922fea | ||
|
|
de94777fff | ||
|
|
0aa0ae65e0 | ||
|
|
4b0d62d59b | ||
|
|
b3ef50e9eb | ||
|
|
d30a1890a1 | ||
|
|
f1c2569de3 | ||
|
|
4826388cdd | ||
| e88736056a | |||
| 9da2a29a72 | |||
| dce8a08a86 | |||
| 1fabb9e610 | |||
| 23db0ac80d | |||
| 28370341d8 | |||
|
|
a023ffe850 | ||
|
|
a4e77ecf6e | ||
|
|
7efc0a1fb2 | ||
|
|
c3e2b2ce70 | ||
|
|
dde1228b1d | ||
|
|
b3474b6dfb | ||
|
|
179a04ae31 | ||
|
|
2bdcbda54c | ||
|
|
a123392c56 | ||
|
|
719135e366 | ||
|
|
ba619554b7 | ||
|
|
6a3ab7032f | ||
|
|
d818d14cb4 | ||
|
|
599c130395 | ||
|
|
9ef2759e27 | ||
|
|
43355c88d4 | ||
|
|
15e1acaf4f | ||
|
|
ec77ffdd88 | ||
|
|
9797c1ca84 | ||
|
|
bbe9633dc8 | ||
|
|
6f60ac5cc7 | ||
|
|
e7ba09793c | ||
|
|
c0c0800e61 | ||
|
|
0bd9d8f93f | ||
|
|
214fc2d5cd | ||
|
|
55b0e2f934 | ||
|
|
609cd4ed6d | ||
|
|
2b20bbdcc8 | ||
|
|
946a95bea1 | ||
|
|
7ec1c152e3 | ||
|
|
0cf1110018 | ||
|
|
9c5ada6fa1 | ||
|
|
0f5702e21b | ||
|
|
f10d6ec80a | ||
|
|
490215df90 | ||
|
|
9d27451813 | ||
|
|
a977f59675 | ||
|
|
51a09d049c | ||
|
|
47be5e77e5 | ||
|
|
d517e79ec0 | ||
|
|
8714ecdde8 | ||
|
|
2cffc57209 | ||
|
|
4e43687de2 | ||
|
|
f8dab83e37 | ||
|
|
655af03cf3 | ||
|
|
0a4dd00edb | ||
|
|
55b4929c54 | ||
|
|
8eb73a6b3c | ||
|
|
0aeb201625 | ||
|
|
e0140a23c2 | ||
|
|
ff6698a9d1 | ||
|
|
843d498b72 | ||
|
|
349612a065 | ||
|
|
e48b22128d | ||
|
|
14dfe4ec51 | ||
|
|
f679b17cbe | ||
|
|
36a0f0e743 | ||
|
|
ad563b9bc8 | ||
|
|
e0051bf276 | ||
|
|
7b72998e1c | ||
|
|
893cce7f21 | ||
|
|
bf7e8fd0e6 | ||
|
|
66d02cf1d1 | ||
|
|
f2c8e40430 | ||
|
|
4b28ee8e73 | ||
|
|
c7e54cfe9f | ||
|
|
50be3acc78 | ||
|
|
66c12a69bb | ||
|
|
e2058f0c71 | ||
|
|
75a74152a5 | ||
|
|
9344c4fd35 | ||
|
|
be0fe00076 | ||
|
|
a2069ee48b | ||
|
|
907885cf85 | ||
|
|
f10ed3236e | ||
|
|
a852a0af7c | ||
|
|
03f3550774 | ||
|
|
2e8d06f9e1 | ||
|
|
7a6e6e8333 | ||
|
|
cc594eca52 | ||
|
|
3fbca2835e | ||
|
|
61ed5c4fa7 | ||
|
|
9b6ba3548f | ||
|
|
efa4c71b8a | ||
|
|
ecfc1a4bf8 | ||
|
|
246b855c76 | ||
|
|
80aa848617 | ||
|
|
acf045965e | ||
|
|
5a7f64dc35 | ||
|
|
de447b0727 | ||
|
|
ace16efd93 | ||
|
|
13ae5ae756 | ||
|
|
f1b035032f |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -3,4 +3,6 @@ build
|
||||
|
||||
.env
|
||||
|
||||
/coverage
|
||||
/coverage
|
||||
|
||||
.DS_Store
|
||||
161
README.md
161
README.md
@@ -6,7 +6,7 @@ SASjs is a open-source framework for building Web Apps on SAS® platforms. You c
|
||||
|
||||
1 - `npm install @sasjs/adapter` - for use in a node project
|
||||
|
||||
2 - [Download](https://cdn.jsdelivr.net/npm/@sasjs/adapter@1/index.js) and use a copy of the latest JS file
|
||||
2 - [Download](https://cdn.jsdelivr.net/npm/@sasjs/adapter@2/index.js) and use a copy of the latest JS file
|
||||
|
||||
3 - Reference directly from the CDN - in which case click [here](https://www.jsdelivr.com/package/npm/@sasjs/adapter?tab=collection) and select "SRI" to get the script tag with the integrity hash.
|
||||
|
||||
@@ -41,8 +41,165 @@ parmcards4;
|
||||
|
||||
You now have a simple web app with a backend service!
|
||||
|
||||
## Detailed Overview
|
||||
|
||||
The SASjs adapter is a JS library and a set of SAS Macros that handle the communication between the frontend app and backend SAS services.
|
||||
|
||||
There are three parts to consider:
|
||||
|
||||
1. JS request / response
|
||||
2. SAS inputs / outputs
|
||||
3. Configuration
|
||||
|
||||
### JS Request / Response
|
||||
|
||||
To install the library you can simply run `npm i @sasjs/adapter` or include a `<script>` tag with a reference to our [CDN](https://www.jsdelivr.com/package/npm/@sasjs/adapter).
|
||||
|
||||
Full technical documentation is available [here](https://adapter.sasjs.io). The main parts are:
|
||||
|
||||
### Instantiation
|
||||
The following code will instantiate an instance of the adapter:
|
||||
|
||||
```javascript
|
||||
let sasJs = new SASjs.default(
|
||||
{
|
||||
appLoc: "/Your/SAS/Folder",
|
||||
serverType:"SAS9"
|
||||
}
|
||||
);
|
||||
```
|
||||
If you've installed it via NPM, you can import it as a default import like so:
|
||||
```
|
||||
import SASjs from '@sasjs/adapter';
|
||||
```
|
||||
You can then instantiate it with:
|
||||
```
|
||||
const sasJs = new SASjs({your config})
|
||||
```
|
||||
|
||||
More on the config later.
|
||||
|
||||
### SAS Logon
|
||||
The login process can be handled directly, as below, or as a callback function to a SAS request.
|
||||
|
||||
```javascript
|
||||
sasJs.logIn('USERNAME','PASSWORD'
|
||||
).then((response) => {
|
||||
if (response.isLoggedIn === true) {
|
||||
console.log('do stuff')
|
||||
} else {
|
||||
console.log('do other stuff')
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Request / Response
|
||||
A simple request can be sent to SAS in the following fashion:
|
||||
|
||||
```javascript
|
||||
sasJs.request("/path/to/my/service", dataObject)
|
||||
.then((response) => {
|
||||
// all tables are in the response object, eg:
|
||||
console.log(response.tablewith2cols1row[0].COL1.value)
|
||||
})
|
||||
```
|
||||
We supply the path to the SAS service, and a data object. The data object can be null (for services with no input), or can contain one or more tables in the following format:
|
||||
|
||||
```javascript
|
||||
let dataObject={
|
||||
"tablewith2cols1row": [{
|
||||
"col1": "val1",
|
||||
"col2": 42
|
||||
}],
|
||||
"tablewith1col2rows": [{
|
||||
"col": "row1"
|
||||
}, {
|
||||
"col": "row2"
|
||||
}]
|
||||
};
|
||||
```
|
||||
|
||||
There are optional parameters such as a config object and a callback login function.
|
||||
|
||||
The response object will contain returned tables and columns. Table names are always lowercase, and column names uppercase.
|
||||
|
||||
The adapter will also cache the logs (if debug enabled) and even the work tables. For performance, it is best to keep debug mode off.
|
||||
|
||||
## SAS Inputs / Outputs
|
||||
|
||||
The SAS side is handled by a number of macros in the [macro core](https://github.com/sasjs/core) library.
|
||||
|
||||
The following snippet shows the process of SAS tables arriving / leaving:
|
||||
```sas
|
||||
/* fetch all input tables sent from frontend - they arrive as work tables */
|
||||
%webout(FETCH)
|
||||
|
||||
/* some sas code */
|
||||
data some sas tables;
|
||||
set from js;
|
||||
run;
|
||||
|
||||
%webout(OPEN) /* open the JSON to be returned */
|
||||
%webout(OBJ,some) /* `some` table is sent in object format */
|
||||
%webout(ARR,sas) /* `sas` table is sent in array format, smaller filesize */
|
||||
%webout(OBJ,tables,fmt=N) /* unformatted (raw) data */
|
||||
%webout(OBJ,tables,label=newtable) /* rename tables on export */
|
||||
%webout(CLOSE) /* close the JSON and send some extra useful variables too */
|
||||
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuration on the client side involves passing an object on startup, which can also be passed with each request. Technical documentation on the SASjsConfig class is available [here](https://adapter.sasjs.io/classes/types.sasjsconfig.html). The main config items are:
|
||||
|
||||
* `appLoc` - this is the folder under which the SAS services will be created.
|
||||
* `serverType` - either `SAS9` or `SASVIYA`.
|
||||
* `serverUrl` - the location (including http protocol and port) of the SAS Server. Can be omitted, eg if serving directly from the SAS Web Server, or in streaming mode.
|
||||
* `debug` - if `true` then SAS Logs and extra debug information is returned.
|
||||
* `useComputeApi` - if `true` and the serverType is `SASVIYA` then the REST APIs will be called directly (rather than using the JES web service).
|
||||
* `contextName` - if missing or blank, and `useComputeApi` is `true` and `serverType` is `SASVIYA` then the JES API will be used.
|
||||
|
||||
The adapter supports a number of approaches for interfacing with Viya (`serverType` is `SASVIYA`). For maximum performance, be sure to [configure your compute context](https://sasjs.io/guide-viya/#shared-account-and-server-re-use) with `reuseServerProcesses` as `true` and a system account in `runServerAs`. This functionality is available since Viya 3.5. This configuration is supported when [creating contexts using the CLI](https://sasjs.io/sasjs-cli-context/#sasjs-context-create).
|
||||
|
||||
### Using JES Web App
|
||||
|
||||
In this setup, all requests are routed through the JES web app, at `YOURSERVER/SASJobExecution`. This is the most reliable method, and also the slowest. One request is made to the JES app, and remaining requests (getting job uri, session spawning, passing parameters, running the program, fetching the log) are made on the SAS server by the JES app.
|
||||
|
||||
```
|
||||
{
|
||||
appLoc:"/Your/Path",
|
||||
serverType:"SASVIYA"
|
||||
}
|
||||
```
|
||||
|
||||
### Using the JES API
|
||||
Here we are running Jobs using the Job Execution Service except this time we are making the requests directly using the REST API instead of through the JES Web App. This is helpful when we need to call web services outside of a browser (eg with the SASjs CLI or other commandline tools). To save one network request, the adapter prefetches the JOB URIs and passes them in the `__job` parameter.
|
||||
|
||||
```
|
||||
{
|
||||
appLoc:"/Your/Path",
|
||||
serverType:"SASVIYA",
|
||||
useComputeApi: true
|
||||
}
|
||||
```
|
||||
|
||||
### Using the Compute API
|
||||
This approach is by far the fastest, as a result of the optimisations we have built into the adapter. With this configuration, in the first sasjs request, we take a URI map of the services in the target folder, and create a session manager - which spawns an extra session. The next time a request is made, the adapter will use the 'hot' session. Sessions are deleted after every use, which actually makes this _less_ resource intensive than a typical JES web app, in which all sessions are kept alive by default for 15 minutes.
|
||||
|
||||
```
|
||||
{
|
||||
appLoc:"/Your/Path",
|
||||
serverType:"SASVIYA",
|
||||
useComputeApi: true,
|
||||
contextName: 'yourComputeContext'
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
# More resources
|
||||
|
||||
For more information and examples specific to this adapter you can check out the [user guide](https://sasjs.io/sasjs-adapter/) or the [technical](http://adapter.sasjs.io/) documentation.
|
||||
For more information and examples specific to this adapter you can check out the [user guide](https://sasjs.io/sasjs-adapter/) or the [technical](http://adapter.sasjs.io/) documentation.
|
||||
|
||||
For more information on building web apps in general, check out these [resources](https://sasjs.io/training/resources/) or contact the [author](https://www.linkedin.com/in/allanbowe/) directly.
|
||||
|
||||
If you are a SAS 9 or SAS Viya customer you can also request a copy of [Data Controller](https://datacontroller.io) - free for up to 5 users, this tool makes use of all parts of the SASjs framework.
|
||||
|
||||
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
328
docs/classes/auth.authmanager.html
Normal file
328
docs/classes/auth.authmanager.html
Normal file
File diff suppressed because one or more lines are too long
435
docs/classes/job_execution.basejobexecutor.html
Normal file
435
docs/classes/job_execution.basejobexecutor.html
Normal file
File diff suppressed because one or more lines are too long
438
docs/classes/job_execution.computejobexecutor.html
Normal file
438
docs/classes/job_execution.computejobexecutor.html
Normal file
File diff suppressed because one or more lines are too long
438
docs/classes/job_execution.jesjobexecutor.html
Normal file
438
docs/classes/job_execution.jesjobexecutor.html
Normal file
File diff suppressed because one or more lines are too long
444
docs/classes/job_execution.webjobexecutor.html
Normal file
444
docs/classes/job_execution.webjobexecutor.html
Normal file
File diff suppressed because one or more lines are too long
231
docs/classes/reflection-817.reflection-220.fileuploader.html
Normal file
231
docs/classes/reflection-817.reflection-220.fileuploader.html
Normal file
File diff suppressed because one or more lines are too long
312
docs/classes/reflection-817.reflection-220.sas9apiclient.html
Normal file
312
docs/classes/reflection-817.reflection-220.sas9apiclient.html
Normal file
File diff suppressed because one or more lines are too long
1715
docs/classes/reflection-817.reflection-220.sasjs.html
Normal file
1715
docs/classes/reflection-817.reflection-220.sasjs.html
Normal file
File diff suppressed because one or more lines are too long
1516
docs/classes/reflection-817.reflection-220.sasviyaapiclient.html
Normal file
1516
docs/classes/reflection-817.reflection-220.sasviyaapiclient.html
Normal file
File diff suppressed because one or more lines are too long
323
docs/classes/reflection-817.reflection-220.sessionmanager.html
Normal file
323
docs/classes/reflection-817.reflection-220.sessionmanager.html
Normal file
File diff suppressed because one or more lines are too long
231
docs/classes/reflection-830.reflection-222.fileuploader.html
Normal file
231
docs/classes/reflection-830.reflection-222.fileuploader.html
Normal file
File diff suppressed because one or more lines are too long
312
docs/classes/reflection-830.reflection-222.sas9apiclient.html
Normal file
312
docs/classes/reflection-830.reflection-222.sas9apiclient.html
Normal file
File diff suppressed because one or more lines are too long
1721
docs/classes/reflection-830.reflection-222.sasjs.html
Normal file
1721
docs/classes/reflection-830.reflection-222.sasjs.html
Normal file
File diff suppressed because one or more lines are too long
1528
docs/classes/reflection-830.reflection-222.sasviyaapiclient.html
Normal file
1528
docs/classes/reflection-830.reflection-222.sasviyaapiclient.html
Normal file
File diff suppressed because one or more lines are too long
360
docs/classes/reflection-830.reflection-222.sessionmanager.html
Normal file
360
docs/classes/reflection-830.reflection-222.sessionmanager.html
Normal file
File diff suppressed because one or more lines are too long
551
docs/classes/request.requestclient.html
Normal file
551
docs/classes/request.requestclient.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
328
docs/classes/types.authorizeerror.html
Normal file
328
docs/classes/types.authorizeerror.html
Normal file
File diff suppressed because one or more lines are too long
346
docs/classes/types.computejobexecutionerror.html
Normal file
346
docs/classes/types.computejobexecutionerror.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
367
docs/classes/types.jobexecutionerror.html
Normal file
367
docs/classes/types.jobexecutionerror.html
Normal file
File diff suppressed because one or more lines are too long
301
docs/classes/types.loginrequirederror.html
Normal file
301
docs/classes/types.loginrequirederror.html
Normal file
File diff suppressed because one or more lines are too long
325
docs/classes/types.notfounderror.html
Normal file
325
docs/classes/types.notfounderror.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
286
docs/classes/types_errors.authorizeerror.html
Normal file
286
docs/classes/types_errors.authorizeerror.html
Normal file
File diff suppressed because one or more lines are too long
304
docs/classes/types_errors.computejobexecutionerror.html
Normal file
304
docs/classes/types_errors.computejobexecutionerror.html
Normal file
File diff suppressed because one or more lines are too long
209
docs/classes/types_errors.errorresponse.html
Normal file
209
docs/classes/types_errors.errorresponse.html
Normal file
File diff suppressed because one or more lines are too long
259
docs/classes/types_errors.internalservererror.html
Normal file
259
docs/classes/types_errors.internalservererror.html
Normal file
File diff suppressed because one or more lines are too long
325
docs/classes/types_errors.jobexecutionerror.html
Normal file
325
docs/classes/types_errors.jobexecutionerror.html
Normal file
File diff suppressed because one or more lines are too long
259
docs/classes/types_errors.loginrequirederror.html
Normal file
259
docs/classes/types_errors.loginrequirederror.html
Normal file
File diff suppressed because one or more lines are too long
283
docs/classes/types_errors.notfounderror.html
Normal file
283
docs/classes/types_errors.notfounderror.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
133
docs/index.html
133
docs/index.html
File diff suppressed because one or more lines are too long
305
docs/interfaces/job_execution.jobexecutor.html
Normal file
305
docs/interfaces/job_execution.jobexecutor.html
Normal file
File diff suppressed because one or more lines are too long
200
docs/interfaces/job_execution.waitingrequstpromise.html
Normal file
200
docs/interfaces/job_execution.waitingrequstpromise.html
Normal file
File diff suppressed because one or more lines are too long
392
docs/interfaces/request.httpclient.html
Normal file
392
docs/interfaces/request.httpclient.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
209
docs/interfaces/types.logstatistics.html
Normal file
209
docs/interfaces/types.logstatistics.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
94
docs/modules/__mocks__.html
Normal file
94
docs/modules/__mocks__.html
Normal file
File diff suppressed because one or more lines are too long
179
docs/modules/auth.html
Normal file
179
docs/modules/auth.html
Normal file
File diff suppressed because one or more lines are too long
144
docs/modules/auth_spec.html
Normal file
144
docs/modules/auth_spec.html
Normal file
File diff suppressed because one or more lines are too long
184
docs/modules/file.html
Normal file
184
docs/modules/file.html
Normal file
File diff suppressed because one or more lines are too long
175
docs/modules/job_execution.html
Normal file
175
docs/modules/job_execution.html
Normal file
File diff suppressed because one or more lines are too long
106
docs/modules/reflection-817.html
Normal file
106
docs/modules/reflection-817.html
Normal file
File diff suppressed because one or more lines are too long
128
docs/modules/reflection-817.reflection-220.html
Normal file
128
docs/modules/reflection-817.reflection-220.html
Normal file
File diff suppressed because one or more lines are too long
106
docs/modules/reflection-830.html
Normal file
106
docs/modules/reflection-830.html
Normal file
File diff suppressed because one or more lines are too long
128
docs/modules/reflection-830.reflection-222.html
Normal file
128
docs/modules/reflection-830.reflection-222.html
Normal file
File diff suppressed because one or more lines are too long
118
docs/modules/request.html
Normal file
118
docs/modules/request.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
133
docs/modules/types_errors.html
Normal file
133
docs/modules/types_errors.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
3669
package-lock.json
generated
3669
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@@ -36,31 +36,31 @@
|
||||
},
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/jest": "^26.0.20",
|
||||
"@types/jest": "^26.0.22",
|
||||
"cp": "^0.2.0",
|
||||
"dotenv": "^8.2.0",
|
||||
"jest": "^26.6.3",
|
||||
"jest-extended": "^0.11.5",
|
||||
"path": "^0.12.7",
|
||||
"rimraf": "^3.0.2",
|
||||
"semantic-release": "^17.3.9",
|
||||
"semantic-release": "^17.4.2",
|
||||
"terser-webpack-plugin": "^4.2.3",
|
||||
"ts-jest": "^25.5.1",
|
||||
"ts-loader": "^8.0.17",
|
||||
"ts-loader": "^8.1.0",
|
||||
"tslint": "^6.1.3",
|
||||
"tslint-config-prettier": "^1.18.0",
|
||||
"typedoc": "^0.19.2",
|
||||
"typedoc": "^0.20.35",
|
||||
"typedoc-neo-theme": "^1.1.0",
|
||||
"typedoc-plugin-external-module-name": "^4.0.6",
|
||||
"typescript": "^3.9.9",
|
||||
"webpack": "^5.21.2",
|
||||
"webpack": "^5.33.2",
|
||||
"webpack-cli": "^4.5.0"
|
||||
},
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@sasjs/utils": "^2.10.2",
|
||||
"axios": "^0.21.1",
|
||||
"@sasjs/utils": "^2.5.0",
|
||||
"form-data": "^3.0.0",
|
||||
"form-data": "^4.0.0",
|
||||
"https": "^1.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
88
sasjs-tests/package-lock.json
generated
88
sasjs-tests/package-lock.json
generated
@@ -2004,21 +2004,19 @@
|
||||
}
|
||||
},
|
||||
"@sasjs/adapter": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/adapter/-/adapter-2.1.0.tgz",
|
||||
"integrity": "sha512-FdvxAPzXHHwCurH5B1RxSGeO6h+lVqDGAtrTAQDByT7HhsikAMFi6mW5dJR/1CKcI2rLOdE/479yYqjwfX8nwg==",
|
||||
"version": "file:../build/sasjs-adapter-5.0.0.tgz",
|
||||
"integrity": "sha512-1t+3LIL2BFw8HpZUPI9QM24801+JH4DCAu4eHoLLmytYhN72asMi1aVtgSDb1xiJYgpbTG7EK3qRpHIV8cEN8w==",
|
||||
"requires": {
|
||||
"@sasjs/utils": "^2.0.2",
|
||||
"es6-promise": "^4.2.8",
|
||||
"@sasjs/utils": "^2.5.0",
|
||||
"axios": "^0.21.1",
|
||||
"form-data": "^3.0.0",
|
||||
"https": "^1.0.0",
|
||||
"isomorphic-fetch": "^2.2.1"
|
||||
"https": "^1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"form-data": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz",
|
||||
"integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==",
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz",
|
||||
"integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==",
|
||||
"requires": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
@@ -2048,10 +2046,11 @@
|
||||
}
|
||||
},
|
||||
"@sasjs/utils": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.2.0.tgz",
|
||||
"integrity": "sha512-T01TYAFU+WQ1uAUaunZXg0MotQe5jn3auVuMPPbY5RU5VhIn4oVhr6qz3EHyNsYYKD1aj5pP7w2elnBzkgWhGw==",
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.5.1.tgz",
|
||||
"integrity": "sha512-a3ISiUX8Yz7au4XYxq2KWf9ODT6nsIDbE4FEqS+AQ3McxZkfuAk4v+REXjOmIlcyQd4R4bufEK8XoB6AROn9sA==",
|
||||
"requires": {
|
||||
"@types/prompts": "^2.0.9",
|
||||
"consola": "^2.15.0",
|
||||
"prompts": "^2.4.0",
|
||||
"valid-url": "^1.0.9"
|
||||
@@ -2366,6 +2365,14 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.1.6.tgz",
|
||||
"integrity": "sha512-6gOkRe7OIioWAXfnO/2lFiv+SJichKVSys1mSsgyrYHSEjk8Ctv4tSR/Odvnu+HWlH2C8j53dahU03XmQdd5fA=="
|
||||
},
|
||||
"@types/prompts": {
|
||||
"version": "2.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/prompts/-/prompts-2.0.9.tgz",
|
||||
"integrity": "sha512-TORZP+FSjTYMWwKadftmqEn6bziN5RnfygehByGsjxoK5ydnClddtv6GikGWPvCm24oI+YBwck5WDxIIyNxUrA==",
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/prop-types": {
|
||||
"version": "15.7.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz",
|
||||
@@ -3396,6 +3403,14 @@
|
||||
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.1.1.tgz",
|
||||
"integrity": "sha512-5Kgy8Cz6LPC9DJcNb3yjAXTu3XihQgEdnIg50c//zOC/MyLP0Clg+Y8Sh9ZjjnvBrDZU4DgXS9C3T9r4/scGZQ=="
|
||||
},
|
||||
"axios": {
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
|
||||
"integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.10.0"
|
||||
}
|
||||
},
|
||||
"axobject-query": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
|
||||
@@ -4677,9 +4692,9 @@
|
||||
"integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg=="
|
||||
},
|
||||
"consola": {
|
||||
"version": "2.15.2",
|
||||
"resolved": "https://registry.npmjs.org/consola/-/consola-2.15.2.tgz",
|
||||
"integrity": "sha512-VxqWw5C8O/mQpZYtfaaSCDJcVK3AxyvQ26rhgvyAI4j/QJISh8DLwFS8GQU+9154u4ngyCsSlnyIAYJme9kQug=="
|
||||
"version": "2.15.3",
|
||||
"resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz",
|
||||
"integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw=="
|
||||
},
|
||||
"console-browserify": {
|
||||
"version": "1.2.0",
|
||||
@@ -5722,24 +5737,6 @@
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
|
||||
},
|
||||
"encoding": {
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
|
||||
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
|
||||
"requires": {
|
||||
"iconv-lite": "^0.6.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"iconv-lite": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz",
|
||||
"integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==",
|
||||
"requires": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"end-of-stream": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
||||
@@ -5882,11 +5879,6 @@
|
||||
"es6-symbol": "^3.1.1"
|
||||
}
|
||||
},
|
||||
"es6-promise": {
|
||||
"version": "4.2.8",
|
||||
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
|
||||
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
|
||||
},
|
||||
"es6-symbol": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz",
|
||||
@@ -8584,15 +8576,6 @@
|
||||
"resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
|
||||
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8="
|
||||
},
|
||||
"isomorphic-fetch": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz",
|
||||
"integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=",
|
||||
"requires": {
|
||||
"node-fetch": "^1.0.1",
|
||||
"whatwg-fetch": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"isstream": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
|
||||
@@ -11423,15 +11406,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node-fetch": {
|
||||
"version": "1.7.3",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz",
|
||||
"integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==",
|
||||
"requires": {
|
||||
"encoding": "^0.1.11",
|
||||
"is-stream": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node-forge": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"homepage": ".",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@sasjs/adapter": "^2.1.0",
|
||||
"@sasjs/adapter": "^2.2.4",
|
||||
"@sasjs/test-framework": "^1.4.0",
|
||||
"@types/jest": "^26.0.20",
|
||||
"@types/node": "^14.14.25",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"appLoc": "/Public/app",
|
||||
"serverType": "SASVIYA",
|
||||
"debug": false,
|
||||
"contextName": "SharedCompute",
|
||||
"contextName": "sasjs adapter compute context",
|
||||
"useComputeApi": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import SASjs, { SASjsConfig } from "@sasjs/adapter";
|
||||
import { TestSuite } from "@sasjs/test-framework";
|
||||
import { ServerType } from "@sasjs/utils/types";
|
||||
|
||||
const stringData: any = { table1: [{ col1: "first col value" }] };
|
||||
|
||||
const defaultConfig: SASjsConfig = {
|
||||
serverUrl: window.location.origin,
|
||||
pathSAS9: "/SASStoredProcess/do",
|
||||
@@ -51,6 +53,36 @@ export const basicTests = (
|
||||
assertion: (response: any) =>
|
||||
response && response.isLoggedIn && response.userName === userName
|
||||
},
|
||||
{
|
||||
title: "Trigger login callback",
|
||||
description:
|
||||
"Should trigger required login callback and after successful login, it should finish the request",
|
||||
test: async () => {
|
||||
await adapter.logOut();
|
||||
|
||||
return await adapter.request("common/sendArr", stringData, null, () => {
|
||||
adapter.logIn(userName, password);
|
||||
});
|
||||
},
|
||||
assertion: (response: any) => {
|
||||
return response.table1[0][0] === stringData.table1[0].col1;
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Request with debug on",
|
||||
description:
|
||||
"Should complete successful request with debugging switched on",
|
||||
test: async () => {
|
||||
const config = {
|
||||
debug: true
|
||||
}
|
||||
|
||||
return await adapter.request("common/sendArr", stringData, config)
|
||||
},
|
||||
assertion: (response: any) => {
|
||||
return response.table1[0][0] === stringData.table1[0].col1;
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Default config",
|
||||
description:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { isUrl } from './utils'
|
||||
import { UploadFile } from './types/UploadFile'
|
||||
import { ErrorResponse, LoginRequiredError } from './types'
|
||||
import { ErrorResponse, LoginRequiredError } from './types/errors'
|
||||
import { RequestClient } from './request/RequestClient'
|
||||
|
||||
export class FileUploader {
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { convertToCSV, isRelativePath, isUri, isUrl } from './utils'
|
||||
import {
|
||||
convertToCSV,
|
||||
isRelativePath,
|
||||
isUri,
|
||||
isUrl,
|
||||
fetchLogByChunks
|
||||
} from './utils'
|
||||
import * as NodeFormData from 'form-data'
|
||||
import {
|
||||
Job,
|
||||
@@ -8,10 +14,13 @@ import {
|
||||
Folder,
|
||||
EditContextInput,
|
||||
JobDefinition,
|
||||
PollOptions,
|
||||
ComputeJobExecutionError,
|
||||
JobExecutionError
|
||||
PollOptions
|
||||
} from './types'
|
||||
import {
|
||||
ComputeJobExecutionError,
|
||||
JobExecutionError,
|
||||
NotFoundError
|
||||
} from './types/errors'
|
||||
import { formatDataForRequest } from './utils/formatDataForRequest'
|
||||
import { SessionManager } from './SessionManager'
|
||||
import { ContextManager } from './ContextManager'
|
||||
@@ -19,8 +28,8 @@ import { timestampToYYYYMMDDHHMMSS } from '@sasjs/utils/time'
|
||||
import { Logger, LogLevel } from '@sasjs/utils/logger'
|
||||
import { isAuthorizeFormRequired } from './auth/isAuthorizeFormRequired'
|
||||
import { RequestClient } from './request/RequestClient'
|
||||
import { NotFoundError } from './types/NotFoundError'
|
||||
import { SasAuthResponse } from '@sasjs/utils/types'
|
||||
import { prefixMessage } from '@sasjs/utils/error'
|
||||
|
||||
/**
|
||||
* A client for interfacing with the SAS Viya REST API.
|
||||
@@ -280,25 +289,24 @@ export class SASViyaApiClient {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`
|
||||
}
|
||||
if (accessToken) headers.Authorization = `Bearer ${accessToken}`
|
||||
|
||||
let executionSessionId: string
|
||||
|
||||
const session = await this.sessionManager
|
||||
.getSession(accessToken)
|
||||
.catch((err) => {
|
||||
throw err
|
||||
throw prefixMessage(err, 'Error while getting session. ')
|
||||
})
|
||||
|
||||
executionSessionId = session!.id
|
||||
|
||||
if (printPid) {
|
||||
const { result: jobIdVariable } = await this.sessionManager.getVariable(
|
||||
executionSessionId,
|
||||
'SYSJOBID',
|
||||
accessToken
|
||||
)
|
||||
const { result: jobIdVariable } = await this.sessionManager
|
||||
.getVariable(executionSessionId, 'SYSJOBID', accessToken)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while getting session variable. ')
|
||||
})
|
||||
|
||||
if (jobIdVariable && jobIdVariable.value) {
|
||||
const relativeJobPath = this.rootFolderName
|
||||
@@ -331,6 +339,7 @@ export class SASViyaApiClient {
|
||||
}
|
||||
|
||||
let fileName
|
||||
|
||||
if (isRelativePath(jobPath)) {
|
||||
fileName = `exec-${
|
||||
jobPath.includes('/') ? jobPath.split('/')[1] : jobPath
|
||||
@@ -352,7 +361,7 @@ export class SASViyaApiClient {
|
||||
if (data) {
|
||||
if (JSON.stringify(data).includes(';')) {
|
||||
files = await this.uploadTables(data, accessToken).catch((err) => {
|
||||
throw err
|
||||
throw prefixMessage(err, 'Error while uploading tables. ')
|
||||
})
|
||||
|
||||
jobVariables['_webin_file_count'] = files.length
|
||||
@@ -376,19 +385,18 @@ export class SASViyaApiClient {
|
||||
variables: jobVariables,
|
||||
arguments: jobArguments
|
||||
}
|
||||
|
||||
const { result: postedJob, etag } = await this.requestClient
|
||||
.post<Job>(
|
||||
`/compute/sessions/${executionSessionId}/jobs`,
|
||||
jobRequestBody,
|
||||
accessToken
|
||||
)
|
||||
.catch((err: any) => {
|
||||
throw err
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while posting job. ')
|
||||
})
|
||||
|
||||
if (!waitForResult) {
|
||||
return session
|
||||
}
|
||||
if (!waitForResult) return session
|
||||
|
||||
if (debug) {
|
||||
console.log(`Job has been submitted for '${fileName}'.`)
|
||||
@@ -404,7 +412,9 @@ export class SASViyaApiClient {
|
||||
etag,
|
||||
accessToken,
|
||||
pollOptions
|
||||
)
|
||||
).catch((err) => {
|
||||
throw prefixMessage(err, 'Error while polling job status. ')
|
||||
})
|
||||
|
||||
const { result: currentJob } = await this.requestClient
|
||||
.get<Job>(
|
||||
@@ -412,23 +422,23 @@ export class SASViyaApiClient {
|
||||
accessToken
|
||||
)
|
||||
.catch((err) => {
|
||||
throw err
|
||||
throw prefixMessage(err, 'Error while getting job. ')
|
||||
})
|
||||
|
||||
let jobResult
|
||||
let log
|
||||
let log = ''
|
||||
|
||||
const logLink = currentJob.links.find((l) => l.rel === 'log')
|
||||
|
||||
if (debug && logLink) {
|
||||
log = await this.requestClient
|
||||
.get<any>(`${logLink.href}/content?limit=10000`, accessToken)
|
||||
.then((res: any) =>
|
||||
res.result.items.map((i: any) => i.line).join('\n')
|
||||
)
|
||||
.catch((err) => {
|
||||
throw err
|
||||
})
|
||||
const logUrl = `${logLink.href}/content`
|
||||
const logCount = currentJob.logStatistics?.lineCount ?? 1000000
|
||||
log = await fetchLogByChunks(
|
||||
this.requestClient,
|
||||
accessToken!,
|
||||
logUrl,
|
||||
logCount
|
||||
)
|
||||
}
|
||||
|
||||
if (jobStatus === 'failed' || jobStatus === 'error') {
|
||||
@@ -449,14 +459,14 @@ export class SASViyaApiClient {
|
||||
.catch(async (e) => {
|
||||
if (e instanceof NotFoundError) {
|
||||
if (logLink) {
|
||||
log = await this.requestClient
|
||||
.get<any>(`${logLink.href}/content?limit=10000`, accessToken)
|
||||
.then((res: any) =>
|
||||
res.result.items.map((i: any) => i.line).join('\n')
|
||||
)
|
||||
.catch((err) => {
|
||||
throw err
|
||||
})
|
||||
const logUrl = `${logLink.href}/content`
|
||||
const logCount = currentJob.logStatistics?.lineCount ?? 1000000
|
||||
log = await fetchLogByChunks(
|
||||
this.requestClient,
|
||||
accessToken!,
|
||||
logUrl,
|
||||
logCount
|
||||
)
|
||||
|
||||
return Promise.reject({
|
||||
status: 500,
|
||||
@@ -464,6 +474,7 @@ export class SASViyaApiClient {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
result: JSON.stringify(e)
|
||||
}
|
||||
@@ -473,7 +484,7 @@ export class SASViyaApiClient {
|
||||
await this.sessionManager
|
||||
.clearSession(executionSessionId, accessToken)
|
||||
.catch((err) => {
|
||||
throw err
|
||||
throw prefixMessage(err, 'Error while clearing session. ')
|
||||
})
|
||||
|
||||
return { result: jobResult?.result, log }
|
||||
@@ -490,7 +501,7 @@ export class SASViyaApiClient {
|
||||
true
|
||||
)
|
||||
} else {
|
||||
throw e
|
||||
throw prefixMessage(e, 'Error while executing script. ')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -814,9 +825,12 @@ export class SASViyaApiClient {
|
||||
? `${this.rootFolderName}/${folderPath}`
|
||||
: folderPath
|
||||
|
||||
await this.populateFolderMap(fullFolderPath, accessToken)
|
||||
await this.populateFolderMap(fullFolderPath, accessToken).catch((err) => {
|
||||
throw prefixMessage(err, 'Error while populating folder map. ')
|
||||
})
|
||||
|
||||
const jobFolder = this.folderMap.get(fullFolderPath)
|
||||
|
||||
if (!jobFolder) {
|
||||
throw new Error(
|
||||
`The folder '${fullFolderPath}' was not found on '${this.serverUrl}'`
|
||||
@@ -824,6 +838,7 @@ export class SASViyaApiClient {
|
||||
}
|
||||
|
||||
const headers: any = { 'Content-Type': 'application/json' }
|
||||
|
||||
if (!!accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`
|
||||
}
|
||||
@@ -847,10 +862,14 @@ export class SASViyaApiClient {
|
||||
|
||||
const {
|
||||
result: jobDefinition
|
||||
} = await this.requestClient.get<JobDefinition>(
|
||||
`${this.serverUrl}${jobDefinitionLink.href}`,
|
||||
accessToken
|
||||
)
|
||||
} = await this.requestClient
|
||||
.get<JobDefinition>(
|
||||
`${this.serverUrl}${jobDefinitionLink.href}`,
|
||||
accessToken
|
||||
)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while getting job definition. ')
|
||||
})
|
||||
|
||||
code = jobDefinition.code
|
||||
|
||||
@@ -861,6 +880,7 @@ export class SASViyaApiClient {
|
||||
if (!code) code = ''
|
||||
|
||||
const linesToExecute = code.replace(/\r\n/g, '\n').split('\n')
|
||||
|
||||
return await this.executeScript(
|
||||
sasJob,
|
||||
linesToExecute,
|
||||
@@ -965,7 +985,13 @@ export class SASViyaApiClient {
|
||||
postJobRequestBody,
|
||||
accessToken
|
||||
)
|
||||
const jobStatus = await this.pollJobState(postedJob, etag, accessToken)
|
||||
const jobStatus = await this.pollJobState(
|
||||
postedJob,
|
||||
etag,
|
||||
accessToken
|
||||
).catch((err) => {
|
||||
throw prefixMessage(err, 'Error while polling job status. ')
|
||||
})
|
||||
const { result: currentJob } = await this.requestClient.get<Job>(
|
||||
`${this.serverUrl}/jobExecution/jobs/${postedJob.id}`,
|
||||
accessToken
|
||||
@@ -1007,19 +1033,27 @@ export class SASViyaApiClient {
|
||||
}
|
||||
|
||||
const url = '/folders/folders/@item?path=' + path
|
||||
const { result: folder } = await this.requestClient.get<Folder>(
|
||||
`${url}`,
|
||||
accessToken
|
||||
)
|
||||
const { result: folder } = await this.requestClient
|
||||
.get<Folder>(`${url}`, accessToken)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while getting folder. ')
|
||||
})
|
||||
|
||||
if (!folder) {
|
||||
throw new Error(`The path ${path} does not exist on ${this.serverUrl}`)
|
||||
}
|
||||
const { result: members } = await this.requestClient.get<{ items: any[] }>(
|
||||
`/folders/folders/${folder.id}/members?limit=${folder.memberCount}`,
|
||||
accessToken
|
||||
)
|
||||
|
||||
const { result: members } = await this.requestClient
|
||||
.get<{ items: any[] }>(
|
||||
`/folders/folders/${folder.id}/members?limit=${folder.memberCount}`,
|
||||
accessToken
|
||||
)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while getting members. ')
|
||||
})
|
||||
|
||||
const itemsAtRoot = members.items
|
||||
|
||||
this.folderMap.set(path, itemsAtRoot)
|
||||
}
|
||||
|
||||
@@ -1052,11 +1086,17 @@ export class SASViyaApiClient {
|
||||
Promise.reject(`Job state link was not found.`)
|
||||
}
|
||||
|
||||
const { result: state } = await this.requestClient.get<string>(
|
||||
`${this.serverUrl}${stateLink.href}?_action=wait&wait=30`,
|
||||
accessToken,
|
||||
'text/plain'
|
||||
)
|
||||
const { result: state } = await this.requestClient
|
||||
.get<string>(
|
||||
`${this.serverUrl}${stateLink.href}?_action=wait&wait=30`,
|
||||
accessToken,
|
||||
'text/plain',
|
||||
{},
|
||||
this.debug
|
||||
)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while getting job state. ')
|
||||
})
|
||||
|
||||
const currentState = state.trim()
|
||||
if (currentState === 'completed') {
|
||||
@@ -1073,11 +1113,20 @@ export class SASViyaApiClient {
|
||||
postedJobState === 'pending'
|
||||
) {
|
||||
if (stateLink) {
|
||||
const { result: jobState } = await this.requestClient.get<string>(
|
||||
`${this.serverUrl}${stateLink.href}?_action=wait&wait=30`,
|
||||
accessToken,
|
||||
'text/plain'
|
||||
)
|
||||
const { result: jobState } = await this.requestClient
|
||||
.get<string>(
|
||||
`${this.serverUrl}${stateLink.href}?_action=wait&wait=30`,
|
||||
accessToken,
|
||||
'text/plain',
|
||||
{},
|
||||
this.debug
|
||||
)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(
|
||||
err,
|
||||
'Error while getting job state after interval. '
|
||||
)
|
||||
})
|
||||
|
||||
postedJobState = jobState.trim()
|
||||
|
||||
@@ -1119,19 +1168,25 @@ export class SASViyaApiClient {
|
||||
)
|
||||
}
|
||||
|
||||
const uploadResponse = await this.requestClient.uploadFile(
|
||||
`${this.serverUrl}/files/files#rawUpload`,
|
||||
csv,
|
||||
accessToken
|
||||
)
|
||||
const uploadResponse = await this.requestClient
|
||||
.uploadFile(`${this.serverUrl}/files/files#rawUpload`, csv, accessToken)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while uploading file. ')
|
||||
})
|
||||
|
||||
uploadedFiles.push({ tableName, file: uploadResponse.result })
|
||||
}
|
||||
return uploadedFiles
|
||||
}
|
||||
|
||||
private async getFolderUri(folderPath: string, accessToken?: string) {
|
||||
const url = '/folders/folders/@item?path=' + folderPath
|
||||
private async getFolderDetails(
|
||||
folderPath: string,
|
||||
accessToken?: string
|
||||
): Promise<Folder | undefined> {
|
||||
const url = isUri(folderPath)
|
||||
? folderPath
|
||||
: `/folders/folders/@item?path=${folderPath}`
|
||||
|
||||
const { result: folder } = await this.requestClient
|
||||
.get<Folder>(`${this.serverUrl}${url}`, accessToken)
|
||||
.catch(() => {
|
||||
@@ -1139,7 +1194,15 @@ export class SASViyaApiClient {
|
||||
})
|
||||
|
||||
if (!folder) return undefined
|
||||
return `/folders/folders/${folder.id}`
|
||||
return folder
|
||||
}
|
||||
|
||||
private async getFolderUri(folderPath: string, accessToken?: string) {
|
||||
const folderDetails = await this.getFolderDetails(folderPath, accessToken)
|
||||
|
||||
if (!folderDetails) return undefined
|
||||
|
||||
return `/folders/folders/${folderDetails.id}`
|
||||
}
|
||||
|
||||
private async getRecycleBinUri(accessToken: string) {
|
||||
@@ -1188,9 +1251,44 @@ export class SASViyaApiClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves a Viya folder to a new location. The folder may be renamed at the same time.
|
||||
* Lists children folders for given Viya folder.
|
||||
* @param sourceFolder - the full path (eg `/Public/example/myFolder`) or URI of the source folder listed. Providing URI instead of path will save one extra request.
|
||||
* @param accessToken - an access token for authorizing the request.
|
||||
*/
|
||||
public async listFolder(
|
||||
sourceFolder: string,
|
||||
accessToken?: string,
|
||||
limit: number = 20
|
||||
) {
|
||||
// checks if 'sourceFolder' is already a URI
|
||||
const sourceFolderUri = isUri(sourceFolder)
|
||||
? sourceFolder
|
||||
: await this.getFolderUri(sourceFolder, accessToken)
|
||||
|
||||
const requestInfo = {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: 'Bearer ' + accessToken
|
||||
}
|
||||
}
|
||||
|
||||
const { result: members } = await this.requestClient.get<{ items: any[] }>(
|
||||
`${this.serverUrl}${sourceFolderUri}/members?limit=${limit}`,
|
||||
accessToken
|
||||
)
|
||||
|
||||
if (members && members.items) {
|
||||
return members.items.map((item: any) => item.name)
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves 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 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 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 the 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.
|
||||
*/
|
||||
@@ -1200,22 +1298,35 @@ export class SASViyaApiClient {
|
||||
targetFolderName: string,
|
||||
accessToken: string
|
||||
) {
|
||||
// checks if 'sourceFolder' is already a URI
|
||||
const sourceFolderUri = isUri(sourceFolder)
|
||||
? sourceFolder
|
||||
: await this.getFolderUri(sourceFolder, accessToken)
|
||||
// If target path is an existing folder, than keep source folder name, othervise rename it with given target folder name
|
||||
const sourceFolderName = sourceFolder.split('/').pop() as string
|
||||
const targetFolderDetails = await this.getFolderDetails(
|
||||
targetParentFolder,
|
||||
accessToken
|
||||
)
|
||||
|
||||
if (!targetFolderDetails) {
|
||||
let targetParentFolderArr = targetParentFolder.split('/')
|
||||
targetParentFolderArr.splice(targetParentFolderArr.length - 1, 1)
|
||||
targetParentFolder = targetParentFolderArr.join('/')
|
||||
} else {
|
||||
targetFolderName = sourceFolderName
|
||||
}
|
||||
|
||||
// checks if 'sourceFolder' is already an URI
|
||||
const sourceFolderUri = await this.getFolderUri(sourceFolder, accessToken)
|
||||
|
||||
// checks if 'targetParentFolder' is already a URI
|
||||
const targetParentFolderUri = isUri(targetParentFolder)
|
||||
? targetParentFolder
|
||||
: await this.getFolderUri(targetParentFolder, accessToken)
|
||||
const targetParentFolderUri = await this.getFolderUri(
|
||||
targetParentFolder,
|
||||
accessToken
|
||||
)
|
||||
|
||||
const sourceFolderId = sourceFolderUri?.split('/').pop()
|
||||
const url = sourceFolderUri
|
||||
|
||||
const { result: folder } = await this.requestClient
|
||||
.patch<Folder>(
|
||||
`${this.serverUrl}${url}`,
|
||||
`${this.serverUrl}${sourceFolderUri}`,
|
||||
{
|
||||
id: sourceFolderId,
|
||||
name: targetFolderName,
|
||||
|
||||
58
src/SASjs.ts
58
src/SASjs.ts
@@ -12,6 +12,7 @@ import {
|
||||
ComputeJobExecutor,
|
||||
JesJobExecutor
|
||||
} from './job-execution'
|
||||
import { ErrorResponse } from './types/errors'
|
||||
|
||||
const defaultConfig: SASjsConfig = {
|
||||
serverUrl: '',
|
||||
@@ -317,6 +318,25 @@ export default class SASjs {
|
||||
return await this.sasViyaApiClient?.deleteFolder(folderPath, accessToken)
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists children folders for given Viya folder.
|
||||
* @param sourceFolder - the full path (eg `/Public/example/myFolder`) or URI of the source folder listed. Providing URI instead of path will save one extra request.
|
||||
* @param accessToken - an access token for authorizing the request.
|
||||
*/
|
||||
public async listFolder(
|
||||
sourceFolder: string,
|
||||
accessToken?: string,
|
||||
limit?: number
|
||||
) {
|
||||
this.isMethodSupported('listFolder', ServerType.SasViya)
|
||||
|
||||
return await this.sasViyaApiClient?.listFolder(
|
||||
sourceFolder,
|
||||
accessToken,
|
||||
limit
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@@ -511,15 +531,19 @@ export default class SASjs {
|
||||
* @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 loginRequiredCallback - provide a function here to be called if the
|
||||
* @param loginRequiredCallback - a function that is called if the
|
||||
* user is not logged in (eg to display a login form). The request will be
|
||||
* resubmitted after logon.
|
||||
* resubmitted after successful login.
|
||||
* When using a `loginRequiredCallback`, the call to the request will look, for example, like so:
|
||||
* `await request(sasJobPath, data, config, () => setIsLoggedIn(false))`
|
||||
* If you are not passing in any data and configuration, it will look like so:
|
||||
* `await request(sasJobPath, {}, {}, () => setIsLoggedIn(false))`
|
||||
*/
|
||||
public async request(
|
||||
sasJob: string,
|
||||
data: any,
|
||||
config: any = {},
|
||||
loginRequiredCallback?: any,
|
||||
data: { [key: string]: any },
|
||||
config: { [key: string]: any } = {},
|
||||
loginRequiredCallback?: () => any,
|
||||
accessToken?: string
|
||||
) {
|
||||
config = {
|
||||
@@ -686,9 +710,27 @@ export default class SASjs {
|
||||
* @param accessToken - an access token for an authorized user.
|
||||
*/
|
||||
public async fetchLogFileContent(logUrl: string, accessToken?: string) {
|
||||
return await this.requestClient!.get(logUrl, accessToken).then((res) =>
|
||||
JSON.stringify(res.result)
|
||||
)
|
||||
return await this.requestClient!.get(logUrl, accessToken).then((res) => {
|
||||
if (!res)
|
||||
return Promise.reject(
|
||||
new ErrorResponse(
|
||||
'Error while fetching log. Response was not provided.'
|
||||
)
|
||||
)
|
||||
|
||||
try {
|
||||
const result = JSON.stringify(res.result)
|
||||
|
||||
return result
|
||||
} catch (err) {
|
||||
return Promise.reject(
|
||||
new ErrorResponse(
|
||||
'Error while fetching log. The result is not valid.',
|
||||
err
|
||||
)
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public getSasRequests() {
|
||||
|
||||
@@ -64,7 +64,7 @@ export class SessionManager {
|
||||
this.sessions = this.sessions.filter((s) => s.id !== id)
|
||||
})
|
||||
.catch((err) => {
|
||||
throw err
|
||||
throw prefixMessage(err, 'Error while deleting session. ')
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -82,17 +82,33 @@ export class AuthManager {
|
||||
* @returns - a promise which resolves with an object containing two values - a boolean `isLoggedIn`, and a string `userName`.
|
||||
*/
|
||||
public async checkSession() {
|
||||
const { result: loginResponse } = await this.requestClient.get<string>(
|
||||
this.loginUrl.replace('.do', ''),
|
||||
undefined,
|
||||
'text/plain'
|
||||
)
|
||||
const responseText = loginResponse
|
||||
const isLoggedIn = /<button.+onClick.+logout/gm.test(responseText)
|
||||
let loginForm: any = null
|
||||
//For VIYA we will send request on API endpoint. Which is faster then pinging SASJobExecution.
|
||||
//For SAS9 we will send request on SASStoredProcess
|
||||
const url =
|
||||
this.serverType === 'SASVIYA'
|
||||
? `${this.serverUrl}/identities`
|
||||
: `${this.serverUrl}/SASStoredProcess`
|
||||
|
||||
const { result: loginResponse } = await this.requestClient
|
||||
.get<string>(url, undefined, 'text/plain')
|
||||
.catch((err: any) => {
|
||||
return { result: 'authErr' }
|
||||
})
|
||||
|
||||
const isLoggedIn = loginResponse !== 'authErr'
|
||||
let loginForm = null
|
||||
|
||||
if (!isLoggedIn) {
|
||||
loginForm = await this.getLoginForm(responseText)
|
||||
//We will logout to make sure cookies are removed and login form is presented
|
||||
this.logOut()
|
||||
|
||||
const { result: formResponse } = await this.requestClient.get<string>(
|
||||
this.loginUrl.replace('.do', ''),
|
||||
undefined,
|
||||
'text/plain'
|
||||
)
|
||||
|
||||
loginForm = await this.getLoginForm(formResponse)
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
|
||||
@@ -176,41 +176,19 @@ describe('AuthManager', () => {
|
||||
|
||||
const response = await authManager.checkSession()
|
||||
expect(response.isLoggedIn).toBeTruthy()
|
||||
expect(mockedAxios.get).toHaveBeenNthCalledWith(1, `/SASLogon/login`, {
|
||||
withCredentials: true,
|
||||
responseType: 'text',
|
||||
transformResponse: undefined,
|
||||
headers: {
|
||||
Accept: '*/*',
|
||||
'Content-Type': 'text/plain'
|
||||
expect(mockedAxios.get).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
`http://test-server.com/identities`,
|
||||
{
|
||||
withCredentials: true,
|
||||
responseType: 'text',
|
||||
transformResponse: undefined,
|
||||
headers: {
|
||||
Accept: '*/*',
|
||||
'Content-Type': 'text/plain'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
done()
|
||||
})
|
||||
|
||||
it('should check and return session information if logged in', async (done) => {
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
serverType,
|
||||
requestClient,
|
||||
authCallback
|
||||
)
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.resolve({ data: '<button onClick="logout">' })
|
||||
)
|
||||
|
||||
const response = await authManager.checkSession()
|
||||
expect(response.isLoggedIn).toBeTruthy()
|
||||
expect(mockedAxios.get).toHaveBeenNthCalledWith(1, `/SASLogon/login`, {
|
||||
withCredentials: true,
|
||||
responseType: 'text',
|
||||
transformResponse: undefined,
|
||||
headers: {
|
||||
Accept: '*/*',
|
||||
'Content-Type': 'text/plain'
|
||||
}
|
||||
})
|
||||
|
||||
done()
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import SASjs from './SASjs'
|
||||
export * from './types'
|
||||
export * from './types/errors'
|
||||
export * from './SASViyaApiClient'
|
||||
export * from './SAS9ApiClient'
|
||||
export default SASjs
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { ServerType } from '@sasjs/utils/types'
|
||||
import { ErrorResponse } from '..'
|
||||
import { SASViyaApiClient } from '../SASViyaApiClient'
|
||||
import { ComputeJobExecutionError, LoginRequiredError } from '../types'
|
||||
import {
|
||||
ErrorResponse,
|
||||
ComputeJobExecutionError,
|
||||
LoginRequiredError
|
||||
} from '../types/errors'
|
||||
import { BaseJobExecutor } from './JobExecutor'
|
||||
|
||||
export class ComputeJobExecutor extends BaseJobExecutor {
|
||||
@@ -20,35 +23,52 @@ export class ComputeJobExecutor extends BaseJobExecutor {
|
||||
const waitForResult = true
|
||||
const expectWebout = true
|
||||
|
||||
return this.sasViyaApiClient
|
||||
?.executeComputeJob(
|
||||
sasJob,
|
||||
config.contextName,
|
||||
config.debug,
|
||||
data,
|
||||
accessToken,
|
||||
waitForResult,
|
||||
expectWebout
|
||||
)
|
||||
.then((response) => {
|
||||
this.appendRequest(response, sasJob, config.debug)
|
||||
let responseJson
|
||||
const requestPromise = new Promise((resolve, reject) => {
|
||||
this.sasViyaApiClient
|
||||
?.executeComputeJob(
|
||||
sasJob,
|
||||
config.contextName,
|
||||
config.debug,
|
||||
data,
|
||||
accessToken,
|
||||
waitForResult,
|
||||
expectWebout
|
||||
)
|
||||
.then((response) => {
|
||||
this.appendRequest(response, sasJob, config.debug)
|
||||
|
||||
return response.result
|
||||
resolve(response.result)
|
||||
})
|
||||
.catch(async (e: Error) => {
|
||||
if (e instanceof ComputeJobExecutionError) {
|
||||
this.appendRequest(e, sasJob, config.debug)
|
||||
|
||||
return responseJson
|
||||
})
|
||||
.catch(async (e: Error) => {
|
||||
if (e instanceof ComputeJobExecutionError) {
|
||||
this.appendRequest(e, sasJob, config.debug)
|
||||
}
|
||||
if (e instanceof LoginRequiredError) {
|
||||
await loginCallback()
|
||||
this.appendWaitingRequest(() =>
|
||||
this.execute(sasJob, data, config, loginRequiredCallback)
|
||||
)
|
||||
}
|
||||
return Promise.reject(new ErrorResponse(e?.message, e))
|
||||
})
|
||||
reject(new ErrorResponse(e?.message, e))
|
||||
}
|
||||
|
||||
if (e instanceof LoginRequiredError) {
|
||||
await loginCallback()
|
||||
this.appendWaitingRequest(() => {
|
||||
return this.execute(
|
||||
sasJob,
|
||||
data,
|
||||
config,
|
||||
loginRequiredCallback
|
||||
).then(
|
||||
(res: any) => {
|
||||
resolve(res)
|
||||
},
|
||||
(err: any) => {
|
||||
reject(err)
|
||||
}
|
||||
)
|
||||
})
|
||||
} else {
|
||||
reject(new ErrorResponse(e?.message, e))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return requestPromise
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { ServerType } from '@sasjs/utils/types'
|
||||
import { ErrorResponse } from '..'
|
||||
import { SASViyaApiClient } from '../SASViyaApiClient'
|
||||
import { JobExecutionError, LoginRequiredError } from '../types'
|
||||
import {
|
||||
ErrorResponse,
|
||||
JobExecutionError,
|
||||
LoginRequiredError
|
||||
} from '../types/errors'
|
||||
import { BaseJobExecutor } from './JobExecutor'
|
||||
|
||||
export class JesJobExecutor extends BaseJobExecutor {
|
||||
@@ -17,24 +20,52 @@ export class JesJobExecutor extends BaseJobExecutor {
|
||||
accessToken?: string
|
||||
) {
|
||||
const loginCallback = loginRequiredCallback || (() => Promise.resolve())
|
||||
return await this.sasViyaApiClient
|
||||
?.executeJob(sasJob, config.contextName, config.debug, data, accessToken)
|
||||
.then((response) => {
|
||||
this.appendRequest(response, sasJob, config.debug)
|
||||
|
||||
return response.result
|
||||
})
|
||||
.catch(async (e: Error) => {
|
||||
if (e instanceof JobExecutionError) {
|
||||
this.appendRequest(e, sasJob, config.debug)
|
||||
}
|
||||
if (e instanceof LoginRequiredError) {
|
||||
await loginCallback()
|
||||
this.appendWaitingRequest(() =>
|
||||
this.execute(sasJob, data, config, loginRequiredCallback)
|
||||
)
|
||||
}
|
||||
return Promise.reject(new ErrorResponse(e?.message, e))
|
||||
})
|
||||
const requestPromise = new Promise((resolve, reject) => {
|
||||
this.sasViyaApiClient
|
||||
?.executeJob(
|
||||
sasJob,
|
||||
config.contextName,
|
||||
config.debug,
|
||||
data,
|
||||
accessToken
|
||||
)
|
||||
.then((response) => {
|
||||
this.appendRequest(response, sasJob, config.debug)
|
||||
|
||||
resolve(response.result)
|
||||
})
|
||||
.catch(async (e: Error) => {
|
||||
if (e instanceof JobExecutionError) {
|
||||
this.appendRequest(e, sasJob, config.debug)
|
||||
|
||||
reject(new ErrorResponse(e?.message, e))
|
||||
}
|
||||
|
||||
if (e instanceof LoginRequiredError) {
|
||||
await loginCallback()
|
||||
|
||||
this.appendWaitingRequest(() => {
|
||||
return this.execute(
|
||||
sasJob,
|
||||
data,
|
||||
config,
|
||||
loginRequiredCallback
|
||||
).then(
|
||||
(res: any) => {
|
||||
resolve(res)
|
||||
},
|
||||
(err: any) => {
|
||||
reject(err)
|
||||
}
|
||||
)
|
||||
})
|
||||
} else {
|
||||
reject(new ErrorResponse(e?.message, e))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return requestPromise
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { ServerType } from '@sasjs/utils/types'
|
||||
import { ErrorResponse, JobExecutionError, LoginRequiredError } from '..'
|
||||
import {
|
||||
ErrorResponse,
|
||||
JobExecutionError,
|
||||
LoginRequiredError
|
||||
} from '../types/errors'
|
||||
import { generateFileUploadForm } from '../file/generateFileUploadForm'
|
||||
import { generateTableUploadForm } from '../file/generateTableUploadForm'
|
||||
import { RequestClient } from '../request/RequestClient'
|
||||
@@ -7,6 +11,11 @@ import { SASViyaApiClient } from '../SASViyaApiClient'
|
||||
import { isRelativePath } from '../utils'
|
||||
import { BaseJobExecutor } from './JobExecutor'
|
||||
|
||||
export interface WaitingRequstPromise {
|
||||
promise: Promise<any> | null
|
||||
resolve: any
|
||||
reject: any
|
||||
}
|
||||
export class WebJobExecutor extends BaseJobExecutor {
|
||||
constructor(
|
||||
serverUrl: string,
|
||||
@@ -80,30 +89,51 @@ export class WebJobExecutor extends BaseJobExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
return this.requestClient!.post(apiUrl, formData, undefined)
|
||||
.then(async (res) => {
|
||||
if (this.serverType === ServerType.SasViya && config.debug) {
|
||||
const jsonResponse = await this.parseSasViyaDebugResponse(
|
||||
res.result as string
|
||||
)
|
||||
const requestPromise = new Promise((resolve, reject) => {
|
||||
this.requestClient!.post(apiUrl, formData, undefined)
|
||||
.then(async (res) => {
|
||||
if (this.serverType === ServerType.SasViya && config.debug) {
|
||||
const jsonResponse = await this.parseSasViyaDebugResponse(
|
||||
res.result as string
|
||||
)
|
||||
this.appendRequest(res, sasJob, config.debug)
|
||||
resolve(jsonResponse)
|
||||
}
|
||||
this.appendRequest(res, sasJob, config.debug)
|
||||
return jsonResponse
|
||||
}
|
||||
this.appendRequest(res, sasJob, config.debug)
|
||||
return res.result
|
||||
})
|
||||
.catch(async (e: Error) => {
|
||||
if (e instanceof JobExecutionError) {
|
||||
this.appendRequest(e, sasJob, config.debug)
|
||||
}
|
||||
if (e instanceof LoginRequiredError) {
|
||||
await loginCallback()
|
||||
this.appendWaitingRequest(() =>
|
||||
this.execute(sasJob, data, config, loginRequiredCallback)
|
||||
)
|
||||
}
|
||||
return Promise.reject(new ErrorResponse(e?.message, e))
|
||||
})
|
||||
resolve(res.result)
|
||||
})
|
||||
.catch(async (e: Error) => {
|
||||
if (e instanceof JobExecutionError) {
|
||||
this.appendRequest(e, sasJob, config.debug)
|
||||
|
||||
reject(new ErrorResponse(e?.message, e))
|
||||
}
|
||||
|
||||
if (e instanceof LoginRequiredError) {
|
||||
await loginCallback()
|
||||
|
||||
this.appendWaitingRequest(() => {
|
||||
return this.execute(
|
||||
sasJob,
|
||||
data,
|
||||
config,
|
||||
loginRequiredCallback
|
||||
).then(
|
||||
(res: any) => {
|
||||
resolve(res)
|
||||
},
|
||||
(err: any) => {
|
||||
reject(err)
|
||||
}
|
||||
)
|
||||
})
|
||||
} else {
|
||||
reject(new ErrorResponse(e?.message, e))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return requestPromise
|
||||
}
|
||||
|
||||
private parseSasViyaDebugResponse = async (response: string) => {
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
||||
import { CsrfToken, JobExecutionError } from '..'
|
||||
import { CsrfToken } from '..'
|
||||
import { isAuthorizeFormRequired, isLogInRequired } from '../auth'
|
||||
import { LoginRequiredError } from '../types'
|
||||
import { AuthorizeError } from '../types/AuthorizeError'
|
||||
import { NotFoundError } from '../types/NotFoundError'
|
||||
import {
|
||||
AuthorizeError,
|
||||
LoginRequiredError,
|
||||
NotFoundError,
|
||||
InternalServerError,
|
||||
JobExecutionError
|
||||
} from '../types/errors'
|
||||
import { parseWeboutResponse } from '../utils/parseWeboutResponse'
|
||||
import { prefixMessage } from '@sasjs/utils/error'
|
||||
|
||||
export interface HttpClient {
|
||||
get<T>(
|
||||
@@ -72,7 +77,8 @@ export class RequestClient implements HttpClient {
|
||||
url: string,
|
||||
accessToken: string | undefined,
|
||||
contentType: string = 'application/json',
|
||||
overrideHeaders: { [key: string]: string | number } = {}
|
||||
overrideHeaders: { [key: string]: string | number } = {},
|
||||
debug: boolean = false
|
||||
): Promise<{ result: T; etag: string }> {
|
||||
const headers = {
|
||||
...this.getHeaders(accessToken, contentType),
|
||||
@@ -92,12 +98,25 @@ export class RequestClient implements HttpClient {
|
||||
.get<T>(url, requestConfig)
|
||||
.then((response) => {
|
||||
throwIfError(response)
|
||||
|
||||
return this.parseResponse<T>(response)
|
||||
})
|
||||
.catch(async (e) => {
|
||||
return await this.handleError(e, () =>
|
||||
this.get<T>(url, accessToken, contentType, overrideHeaders)
|
||||
)
|
||||
return await this.handleError(
|
||||
e,
|
||||
() =>
|
||||
this.get<T>(url, accessToken, contentType, overrideHeaders).catch(
|
||||
(err) => {
|
||||
throw prefixMessage(
|
||||
err,
|
||||
'Error while executing handle error callback. '
|
||||
)
|
||||
}
|
||||
),
|
||||
debug
|
||||
).catch((err) => {
|
||||
throw prefixMessage(err, 'Error while handling error. ')
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -329,37 +348,68 @@ export class RequestClient implements HttpClient {
|
||||
}
|
||||
}
|
||||
|
||||
private handleError = async (e: any, callback: any) => {
|
||||
private handleError = async (
|
||||
e: any,
|
||||
callback: any,
|
||||
debug: boolean = false
|
||||
) => {
|
||||
const response = e.response as AxiosResponse
|
||||
|
||||
if (e instanceof AuthorizeError) {
|
||||
const res = await this.httpClient.get(e.confirmUrl, {
|
||||
responseType: 'text',
|
||||
headers: { 'Content-Type': 'text/plain', Accept: '*/*' }
|
||||
})
|
||||
const res = await this.httpClient
|
||||
.get(e.confirmUrl, {
|
||||
responseType: 'text',
|
||||
headers: { 'Content-Type': 'text/plain', Accept: '*/*' }
|
||||
})
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while getting error confirmUrl. ')
|
||||
})
|
||||
|
||||
if (isAuthorizeFormRequired(res?.data as string)) {
|
||||
await this.authorize(res.data as string)
|
||||
await this.authorize(res.data as string).catch((err) => {
|
||||
throw prefixMessage(err, 'Error while authorizing request. ')
|
||||
})
|
||||
}
|
||||
return await callback()
|
||||
|
||||
return await callback().catch((err: any) => {
|
||||
throw prefixMessage(
|
||||
err,
|
||||
'Error while executing callback in handleError. '
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
if (e instanceof LoginRequiredError) {
|
||||
this.clearCsrfTokens()
|
||||
}
|
||||
|
||||
if (response?.status === 403 || response?.status === 449) {
|
||||
this.parseAndSetCsrfToken(response)
|
||||
|
||||
if (this.csrfToken.headerName && this.csrfToken.value) {
|
||||
return await callback()
|
||||
return await callback().catch((err: any) => {
|
||||
throw prefixMessage(
|
||||
err,
|
||||
'Error while executing callback in handleError. '
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
throw e
|
||||
} else if (response?.status === 404) {
|
||||
throw new NotFoundError(response.config.url!)
|
||||
} else if (response?.status === 502) {
|
||||
if (debug) throw new InternalServerError()
|
||||
else return
|
||||
}
|
||||
|
||||
throw e
|
||||
}
|
||||
|
||||
private async parseResponse<T>(response: AxiosResponse<any>) {
|
||||
private parseResponse<T>(response: AxiosResponse<any>) {
|
||||
const etag = response?.headers ? response.headers['etag'] : ''
|
||||
let parsedResponse
|
||||
let includeSAS9Log: boolean = false
|
||||
|
||||
try {
|
||||
if (typeof response.data === 'string') {
|
||||
@@ -373,11 +423,20 @@ export class RequestClient implements HttpClient {
|
||||
} catch {
|
||||
parsedResponse = response.data
|
||||
}
|
||||
|
||||
includeSAS9Log = true
|
||||
}
|
||||
return {
|
||||
|
||||
let responseToReturn: { result: T; etag: any; log?: string } = {
|
||||
result: parsedResponse as T,
|
||||
etag
|
||||
}
|
||||
|
||||
if (includeSAS9Log) {
|
||||
responseToReturn.log = response.data
|
||||
}
|
||||
|
||||
return responseToReturn
|
||||
}
|
||||
}
|
||||
|
||||
@@ -413,6 +472,7 @@ const throwIfError = (response: AxiosResponse) => {
|
||||
}
|
||||
|
||||
const error = parseError(response.data as string)
|
||||
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
|
||||
83
src/test/FolderOperations.spec.ts
Normal file
83
src/test/FolderOperations.spec.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { RequestClient } from '../request/RequestClient'
|
||||
import { SASViyaApiClient } from '../SASViyaApiClient'
|
||||
import axios from 'axios'
|
||||
jest.mock('axios')
|
||||
const mockedAxios = axios as jest.Mocked<typeof axios>
|
||||
|
||||
describe('FolderOperations', () => {
|
||||
const sasViyaApiClient = new SASViyaApiClient(
|
||||
'https://sample.server.com',
|
||||
'/Public',
|
||||
'Context',
|
||||
new RequestClient('https://sample.server.com')
|
||||
)
|
||||
|
||||
beforeEach(() => {})
|
||||
|
||||
it('should move and rename folder', async (done) => {
|
||||
mockFetchResponse(false)
|
||||
|
||||
let res: any = await sasViyaApiClient.moveFolder(
|
||||
'/Test/fromFolder/oldName',
|
||||
'/Test/toFolder/newName',
|
||||
'newName',
|
||||
'token'
|
||||
)
|
||||
|
||||
expect(res.folder.name).toEqual('newName')
|
||||
expect(res.folder.parentFolderUri.split('=')[1]).toEqual('/Test/toFolder')
|
||||
|
||||
done()
|
||||
})
|
||||
|
||||
it('should move and keep the name of folder', async (done) => {
|
||||
mockFetchResponse(true)
|
||||
|
||||
let res: any = await sasViyaApiClient.moveFolder(
|
||||
'/Test/fromFolder/oldName',
|
||||
'/Test/toFolder',
|
||||
'toFolder',
|
||||
'token'
|
||||
)
|
||||
|
||||
expect(res.folder.name).toEqual('oldName')
|
||||
expect(res.folder.parentFolderUri.split('=')[1]).toEqual('/Test/toFolder')
|
||||
|
||||
done()
|
||||
})
|
||||
|
||||
it('should only rename folder', async (done) => {
|
||||
mockFetchResponse(false)
|
||||
|
||||
let res: any = await sasViyaApiClient.moveFolder(
|
||||
'/Test/toFolder/oldName',
|
||||
'/Test/toFolder/newName',
|
||||
'newName',
|
||||
'token'
|
||||
)
|
||||
|
||||
expect(res.folder.name).toEqual('newName')
|
||||
expect(res.folder.parentFolderUri.split('=')[1]).toEqual('/Test/toFolder')
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
const mockFetchResponse = (targetFolderExists: boolean) => {
|
||||
mockedAxios.patch.mockImplementation((url: any, request: any) => {
|
||||
return Promise.resolve({ status: 200, data: { folder: request } })
|
||||
})
|
||||
|
||||
mockedAxios.get.mockImplementation((url: any, request: any) => {
|
||||
if (!targetFolderExists && url.includes('newName')) {
|
||||
return Promise.resolve(undefined)
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
status: 200,
|
||||
data: {
|
||||
id: url
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -29,7 +29,10 @@ describe('SessionManager', () => {
|
||||
Promise.resolve({ data: sampleResponse })
|
||||
)
|
||||
|
||||
const expectedResponse = { etag: '', result: sampleResponse }
|
||||
const expectedResponse = {
|
||||
etag: '',
|
||||
result: sampleResponse
|
||||
}
|
||||
|
||||
await expect(
|
||||
sessionManager.getVariable(
|
||||
|
||||
39
src/test/utils/isUrl.spec.ts
Normal file
39
src/test/utils/isUrl.spec.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { isUrl } from '../../utils/isUrl'
|
||||
|
||||
describe('urlValidator', () => {
|
||||
it('should return true with an HTTP URL', () => {
|
||||
const url = 'http://google.com'
|
||||
|
||||
expect(isUrl(url)).toEqual(true)
|
||||
})
|
||||
|
||||
it('should return true with an HTTPS URL', () => {
|
||||
const url = 'https://google.com'
|
||||
|
||||
expect(isUrl(url)).toEqual(true)
|
||||
})
|
||||
|
||||
it('should return true when the URL is blank', () => {
|
||||
const url = ''
|
||||
|
||||
expect(isUrl(url)).toEqual(false)
|
||||
})
|
||||
|
||||
it('should return false when the URL has not supported protocol', () => {
|
||||
const url = 'htpps://google.com'
|
||||
|
||||
expect(isUrl(url)).toEqual(false)
|
||||
})
|
||||
|
||||
it('should return false when the URL is null', () => {
|
||||
const url = null
|
||||
|
||||
expect(isUrl((url as unknown) as string)).toEqual(false)
|
||||
})
|
||||
|
||||
it('should return false when the URL is undefined', () => {
|
||||
const url = undefined
|
||||
|
||||
expect(isUrl((url as unknown) as string)).toEqual(false)
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Link } from './Link'
|
||||
import { JobResult } from './JobResult'
|
||||
import { LogStatistics } from './LogStatistics'
|
||||
|
||||
export interface Job {
|
||||
id: string
|
||||
@@ -10,4 +11,5 @@ export interface Job {
|
||||
links: Link[]
|
||||
results: JobResult
|
||||
error?: any
|
||||
logStatistics: LogStatistics
|
||||
}
|
||||
|
||||
4
src/types/LogStatistics.ts
Normal file
4
src/types/LogStatistics.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface LogStatistics {
|
||||
lineCount: number
|
||||
modifiedTimeStamp: string
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Job } from './Job'
|
||||
import { Job } from '../Job'
|
||||
|
||||
export class ComputeJobExecutionError extends Error {
|
||||
constructor(public job: Job, public log: string) {
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user