mirror of
https://github.com/sasjs/adapter.git
synced 2025-12-11 09:24:35 +00:00
Compare commits
99 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a52bbe560 | ||
|
|
a5c725e677 | ||
|
|
f5e1907e28 | ||
|
|
f7a9b0cbb6 | ||
|
|
1258a1a180 | ||
|
|
0bb343a1de | ||
|
|
929c89b70f | ||
|
|
169ca35238 | ||
|
|
60be28f149 | ||
|
|
14daa55184 | ||
|
|
f763f05b5e | ||
|
|
b6aced5bad | ||
|
|
7bb7db0f27 | ||
|
|
36ea148446 | ||
|
|
762254d8c4 | ||
|
|
8474b222ea | ||
|
|
c1750c014e | ||
|
|
d7a7909529 | ||
|
|
31b60a985e | ||
|
|
a6b13d9cb9 | ||
|
|
55fcbf2e36 | ||
|
|
fad8549d92 | ||
|
|
95c03e5d07 | ||
|
|
e241a47c23 | ||
|
|
ed7f36dbed | ||
|
|
74f0c263db | ||
|
|
5a2a4bf39c | ||
|
|
0ea91ddd3b | ||
|
|
4422c37827 | ||
|
|
5fad9d01bc | ||
|
|
fb02c77a3a | ||
|
|
5de84c07a8 | ||
|
|
54e2319183 | ||
|
|
261913d2d7 | ||
|
|
187917cb32 | ||
|
|
d22b6c77b3 | ||
|
|
785b276741 | ||
|
|
2ce3669b10 | ||
|
|
3a1ea1614f | ||
|
|
3c5988aacf | ||
|
|
1a25c354fa | ||
|
|
eb1668d6c1 | ||
|
|
69d088a9c6 | ||
|
|
ccd44c31c7 | ||
|
|
dec7c18ecb | ||
|
|
c8a5eb5993 | ||
|
|
700a67a600 | ||
|
|
a2778bed52 | ||
|
|
2b04fe0c3e | ||
|
|
1c1e6b8efd | ||
|
|
c92a0a53f2 | ||
|
|
ee024c67ab | ||
|
|
27301651be | ||
|
|
9742f53dde | ||
|
|
38a950a036 | ||
|
|
e41c54e37e | ||
|
|
405a19f0cf | ||
|
|
c7d6c66093 | ||
|
|
8bf74d17e9 | ||
|
|
a12244cf78 | ||
|
|
a579f481c5 | ||
|
|
2ecd57169f | ||
|
|
7d84033ad4 | ||
|
|
c4109b225b | ||
|
|
2ec8696615 | ||
|
|
c5339276e4 | ||
|
|
97db960f62 | ||
|
|
0ac7f8892e | ||
|
|
68f5e5bec5 | ||
|
|
69a14ff6d7 | ||
|
|
70e461224a | ||
|
|
4da22ee6b4 | ||
|
|
2c8ba09578 | ||
|
|
c12d6f1c9c | ||
|
|
92504b0c16 | ||
|
|
e5fb7a7698 | ||
|
|
c22b9066d8 | ||
|
|
77d7e03de5 | ||
|
|
b614bafd03 | ||
|
|
34cabcde2d | ||
|
|
de82058850 | ||
|
|
327be9e141 | ||
|
|
f1502c0773 | ||
|
|
c8a2df2d1f | ||
|
|
2be6200b90 | ||
|
|
333289cd20 | ||
|
|
204139cd01 | ||
|
|
2a38b68e69 | ||
|
|
39cc20b680 | ||
|
|
8b3c9746fc | ||
|
|
7a76f5f343 | ||
|
|
2bbcd7dee7 | ||
|
|
b02ce07ddf | ||
|
|
41400bea86 | ||
|
|
991ac100f6 | ||
|
|
66c156d299 | ||
|
|
7d0d830391 | ||
|
|
0b038380c7 | ||
|
|
162ba5e837 |
@@ -1,6 +1,6 @@
|
|||||||
# Contributing
|
# Contributing
|
||||||
|
|
||||||
Contributions to SASjs are very welcome! When making a PR, test cases should be included. To help in unit testing, be sure to run the following when making changes:
|
Contributions to SASjs are very welcome! When making a PR, test cases should be included. To help in unit testing, be sure to run the following when making changes:
|
||||||
|
|
||||||
```
|
```
|
||||||
# the following creates a tarball in the build folder of SASjs
|
# the following creates a tarball in the build folder of SASjs
|
||||||
@@ -10,12 +10,13 @@ npm run-script package:lib
|
|||||||
npm install ../sasjs/build/<tarball filename>
|
npm install ../sasjs/build/<tarball filename>
|
||||||
```
|
```
|
||||||
|
|
||||||
Tests are run using cypress. Before running tests, you need to define the following backend services:
|
Tests are run using cypress. Before running tests, you need to define the following backend services:
|
||||||
|
|
||||||
# SAS 9
|
# SAS 9
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
filename mc url "https://raw.githubusercontent.com/macropeople/macrocore/master/mc_all.sas?_=1";
|
filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
|
||||||
%inc mc;
|
%inc mc;
|
||||||
filename ft15f001 temp;
|
filename ft15f001 temp;
|
||||||
parmcards4;
|
parmcards4;
|
||||||
@@ -37,19 +38,15 @@ parmcards4;
|
|||||||
```
|
```
|
||||||
|
|
||||||
# Viya
|
# Viya
|
||||||
```
|
|
||||||
filename mc url "https://raw.githubusercontent.com/macropeople/macrocore/master/mc_all.sas";
|
|
||||||
%inc mc;
|
|
||||||
|
|
||||||
|
```
|
||||||
|
filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
|
||||||
|
%inc mc;
|
||||||
filename ft15f001 temp;
|
filename ft15f001 temp;
|
||||||
parmcards4;
|
parmcards4;
|
||||||
|
%webout(FETCH)
|
||||||
%webout(OPEN)
|
%webout(OPEN)
|
||||||
%global sasjs_tables;
|
|
||||||
%let sasjs_tables=&sasjs_tables;
|
|
||||||
%put &=sasjs_tables;
|
|
||||||
%let sasjs_tables=&sasjs_tables;
|
|
||||||
%macro x();
|
%macro x();
|
||||||
%global sasjs_tables;
|
|
||||||
%do i=1 %to %sysfunc(countw(&sasjs_tables));
|
%do i=1 %to %sysfunc(countw(&sasjs_tables));
|
||||||
%let table=%scan(&sasjs_tables,&i);
|
%let table=%scan(&sasjs_tables,&i);
|
||||||
%webout(OBJ,&table)
|
%webout(OBJ,&table)
|
||||||
@@ -58,13 +55,11 @@ parmcards4;
|
|||||||
%x()
|
%x()
|
||||||
%webout(CLOSE)
|
%webout(CLOSE)
|
||||||
;;;;
|
;;;;
|
||||||
%mv_createwebservice(path=/Public/app/common,name=sendObj)
|
%mp_createwebservice(path=/Public/app/common,name=sendObj)
|
||||||
filename ft15f001 temp;
|
filename ft15f001 temp;
|
||||||
parmcards4;
|
parmcards4;
|
||||||
|
%webout(FETCH)
|
||||||
%webout(OPEN)
|
%webout(OPEN)
|
||||||
%global sasjs_tables;
|
|
||||||
%let sasjs_tables=&sasjs_tables;
|
|
||||||
%put &=sasjs_tables;
|
|
||||||
%macro x();
|
%macro x();
|
||||||
%do i=1 %to %sysfunc(countw(&sasjs_tables));
|
%do i=1 %to %sysfunc(countw(&sasjs_tables));
|
||||||
%let table=%scan(&sasjs_tables,&i);
|
%let table=%scan(&sasjs_tables,&i);
|
||||||
@@ -74,7 +69,7 @@ parmcards4;
|
|||||||
%x()
|
%x()
|
||||||
%webout(CLOSE)
|
%webout(CLOSE)
|
||||||
;;;;
|
;;;;
|
||||||
%mv_createwebservice(path=/Public/app/common,name=sendArr)
|
%mp_createwebservice(path=/Public/app/common,name=sendArr)
|
||||||
```
|
```
|
||||||
|
|
||||||
The above services will return anything you send. To run the tests simply launch `npm run cypress`.
|
The above services will return anything you send. To run the tests simply launch `npm run cypress`.
|
||||||
|
|||||||
15
README.md
15
README.md
@@ -1,6 +1,6 @@
|
|||||||
[](https://www.jsdelivr.com/package/npm/@sasjs/adapter)
|
[](https://www.jsdelivr.com/package/npm/@sasjs/adapter)
|
||||||
|
|
||||||
# SASjs
|
# @sasjs/adapter
|
||||||
|
|
||||||
SASjs is a open-source framework for building Web Apps on SAS® platforms. You can use as much or as little of it as you like. This repository contains the JS adapter, the part that handles the to/from SAS communication on the client side. There are 3 ways to install it:
|
SASjs is a open-source framework for building Web Apps on SAS® platforms. You can use as much or as little of it as you like. This repository contains the JS adapter, the part that handles the to/from SAS communication on the client side. There are 3 ways to install it:
|
||||||
|
|
||||||
@@ -10,21 +10,20 @@ SASjs is a open-source framework for building Web Apps on SAS® platforms. You c
|
|||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
If you are short on time and just need to build an app quickly, then check out [this video](https://vimeo.com/393161794) and the [react-seed-app](https://github.com/macropeople/react-seed-app) which provides some boilerplate.
|
If you are short on time and just need to build an app quickly, then check out [this video](https://vimeo.com/393161794) and the [react-seed-app](https://github.com/sasjs/react-seed-app) which provides some boilerplate.
|
||||||
|
|
||||||
For more information on building web apps with SAS, check out [sasjs.io](https://sasjs.io)
|
For more information on building web apps with SAS, check out [sasjs.io](https://sasjs.io)
|
||||||
|
|
||||||
## None of this makes sense. How do I build an app with it?
|
## None of this makes sense. How do I build an app with it?
|
||||||
|
|
||||||
Ok ok. Deploy this [example.html](https://github.com/sasjs/adapter/blob/master/example.html) file to your web server, and update `servertype` to `SAS9` or `SASVIYA` depending on your backend.
|
Ok ok. Deploy this [example.html](https://raw.githubusercontent.com/sasjs/adapter/master/example.html) file to your web server, and update `servertype` to `SAS9` or `SASVIYA` depending on your backend.
|
||||||
|
|
||||||
The backend part can be deployed as follows:
|
The backend part can be deployed as follows:
|
||||||
|
|
||||||
```
|
```
|
||||||
%let appLoc=/Public/app/readme; /* Metadata or Viya Folder location as per SASjs config */
|
%let appLoc=/Public/app/readme; /* Metadata or Viya Folder per SASjs config */
|
||||||
/* compile macros (can also be downloaded & compiled seperately) */
|
filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
|
||||||
filename mc url "https://raw.githubusercontent.com/macropeople/macrocore/master/mc_all.sas";
|
%inc mc; /* compile macros (can also be downloaded & compiled seperately) */
|
||||||
%inc mc;
|
|
||||||
filename ft15f001 temp;
|
filename ft15f001 temp;
|
||||||
parmcards4;
|
parmcards4;
|
||||||
%webout(FETCH) /* receive all data as SAS datasets */
|
%webout(FETCH) /* receive all data as SAS datasets */
|
||||||
@@ -44,6 +43,6 @@ You now have a simple web app with a backend service!
|
|||||||
|
|
||||||
# More resources
|
# More resources
|
||||||
|
|
||||||
For more information specific to this adapter you can check out this [user guide](https://sasjs.io/sasjs/sasjs-adapter/) or the [technical](http://adapter.sasjs.io/) documentation.
|
For more information specific to this adapter you can check out this [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.
|
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.
|
||||||
|
|||||||
1
docs/_config.yml
Normal file
1
docs/_config.yml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
theme: jekyll-theme-minimal
|
||||||
File diff suppressed because one or more lines are too long
287
docs/classes/reflection-445.reflection-1.sas9apiclient.html
Normal file
287
docs/classes/reflection-445.reflection-1.sas9apiclient.html
Normal file
File diff suppressed because one or more lines are too long
919
docs/classes/reflection-445.reflection-1.sasjs.html
Normal file
919
docs/classes/reflection-445.reflection-1.sasjs.html
Normal file
File diff suppressed because one or more lines are too long
782
docs/classes/reflection-445.reflection-1.sasviyaapiclient.html
Normal file
782
docs/classes/reflection-445.reflection-1.sasviyaapiclient.html
Normal file
File diff suppressed because one or more lines are too long
294
docs/classes/reflection-560.reflection-1.sas9apiclient.html
Normal file
294
docs/classes/reflection-560.reflection-1.sas9apiclient.html
Normal file
File diff suppressed because one or more lines are too long
934
docs/classes/reflection-560.reflection-1.sasjs.html
Normal file
934
docs/classes/reflection-560.reflection-1.sasjs.html
Normal file
File diff suppressed because one or more lines are too long
892
docs/classes/reflection-560.reflection-1.sasviyaapiclient.html
Normal file
892
docs/classes/reflection-560.reflection-1.sasviyaapiclient.html
Normal file
File diff suppressed because one or more lines are too long
206
docs/classes/reflection-560.reflection-1.sessionmanager.html
Normal file
206
docs/classes/reflection-560.reflection-1.sessionmanager.html
Normal file
File diff suppressed because one or more lines are too long
294
docs/classes/reflection-588.reflection-1.sas9apiclient.html
Normal file
294
docs/classes/reflection-588.reflection-1.sas9apiclient.html
Normal file
File diff suppressed because one or more lines are too long
1017
docs/classes/reflection-588.reflection-1.sasjs.html
Normal file
1017
docs/classes/reflection-588.reflection-1.sasjs.html
Normal file
File diff suppressed because one or more lines are too long
913
docs/classes/reflection-588.reflection-1.sasviyaapiclient.html
Normal file
913
docs/classes/reflection-588.reflection-1.sasviyaapiclient.html
Normal file
File diff suppressed because one or more lines are too long
227
docs/classes/reflection-588.reflection-1.sessionmanager.html
Normal file
227
docs/classes/reflection-588.reflection-1.sessionmanager.html
Normal file
File diff suppressed because one or more lines are too long
224
docs/classes/reflection-607.reflection-145.fileuploader.html
Normal file
224
docs/classes/reflection-607.reflection-145.fileuploader.html
Normal file
File diff suppressed because one or more lines are too long
297
docs/classes/reflection-607.reflection-145.sas9apiclient.html
Normal file
297
docs/classes/reflection-607.reflection-145.sas9apiclient.html
Normal file
File diff suppressed because one or more lines are too long
1021
docs/classes/reflection-607.reflection-145.sasjs.html
Normal file
1021
docs/classes/reflection-607.reflection-145.sasjs.html
Normal file
File diff suppressed because one or more lines are too long
916
docs/classes/reflection-607.reflection-145.sasviyaapiclient.html
Normal file
916
docs/classes/reflection-607.reflection-145.sasviyaapiclient.html
Normal file
File diff suppressed because one or more lines are too long
230
docs/classes/reflection-607.reflection-145.sessionmanager.html
Normal file
230
docs/classes/reflection-607.reflection-145.sessionmanager.html
Normal file
File diff suppressed because one or more lines are too long
221
docs/classes/reflection-610.reflection-149.fileuploader.html
Normal file
221
docs/classes/reflection-610.reflection-149.fileuploader.html
Normal file
File diff suppressed because one or more lines are too long
297
docs/classes/reflection-610.reflection-149.sas9apiclient.html
Normal file
297
docs/classes/reflection-610.reflection-149.sas9apiclient.html
Normal file
File diff suppressed because one or more lines are too long
1015
docs/classes/reflection-610.reflection-149.sasjs.html
Normal file
1015
docs/classes/reflection-610.reflection-149.sasjs.html
Normal file
File diff suppressed because one or more lines are too long
916
docs/classes/reflection-610.reflection-149.sasviyaapiclient.html
Normal file
916
docs/classes/reflection-610.reflection-149.sasviyaapiclient.html
Normal file
File diff suppressed because one or more lines are too long
230
docs/classes/reflection-610.reflection-149.sessionmanager.html
Normal file
230
docs/classes/reflection-610.reflection-149.sessionmanager.html
Normal file
File diff suppressed because one or more lines are too long
221
docs/classes/reflection-611.reflection-149.fileuploader.html
Normal file
221
docs/classes/reflection-611.reflection-149.fileuploader.html
Normal file
File diff suppressed because one or more lines are too long
297
docs/classes/reflection-611.reflection-149.sas9apiclient.html
Normal file
297
docs/classes/reflection-611.reflection-149.sas9apiclient.html
Normal file
File diff suppressed because one or more lines are too long
1012
docs/classes/reflection-611.reflection-149.sasjs.html
Normal file
1012
docs/classes/reflection-611.reflection-149.sasjs.html
Normal file
File diff suppressed because one or more lines are too long
916
docs/classes/reflection-611.reflection-149.sasviyaapiclient.html
Normal file
916
docs/classes/reflection-611.reflection-149.sasviyaapiclient.html
Normal file
File diff suppressed because one or more lines are too long
230
docs/classes/reflection-611.reflection-149.sessionmanager.html
Normal file
230
docs/classes/reflection-611.reflection-149.sessionmanager.html
Normal file
File diff suppressed because one or more lines are too long
305
docs/classes/types.sasjsconfig.html
Normal file
305
docs/classes/types.sasjsconfig.html
Normal file
File diff suppressed because one or more lines are too long
190
docs/enums/types.servertype.html
Normal file
190
docs/enums/types.servertype.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
221
docs/interfaces/types.context.html
Normal file
221
docs/interfaces/types.context.html
Normal file
File diff suppressed because one or more lines are too long
193
docs/interfaces/types.csrftoken.html
Normal file
193
docs/interfaces/types.csrftoken.html
Normal file
File diff suppressed because one or more lines are too long
207
docs/interfaces/types.folder.html
Normal file
207
docs/interfaces/types.folder.html
Normal file
File diff suppressed because one or more lines are too long
263
docs/interfaces/types.job.html
Normal file
263
docs/interfaces/types.job.html
Normal file
File diff suppressed because one or more lines are too long
179
docs/interfaces/types.jobdefinition.html
Normal file
179
docs/interfaces/types.jobdefinition.html
Normal file
File diff suppressed because one or more lines are too long
179
docs/interfaces/types.jobresult.html
Normal file
179
docs/interfaces/types.jobresult.html
Normal file
File diff suppressed because one or more lines are too long
235
docs/interfaces/types.link.html
Normal file
235
docs/interfaces/types.link.html
Normal file
File diff suppressed because one or more lines are too long
254
docs/interfaces/types.sasjsrequest.html
Normal file
254
docs/interfaces/types.sasjsrequest.html
Normal file
File diff suppressed because one or more lines are too long
240
docs/interfaces/types.sasjswaitingrequest.html
Normal file
240
docs/interfaces/types.sasjswaitingrequest.html
Normal file
File diff suppressed because one or more lines are too long
207
docs/interfaces/types.session.html
Normal file
207
docs/interfaces/types.session.html
Normal file
File diff suppressed because one or more lines are too long
198
docs/interfaces/types.uploadfile.html
Normal file
198
docs/interfaces/types.uploadfile.html
Normal file
File diff suppressed because one or more lines are too long
107
docs/modules/reflection-445.html
Normal file
107
docs/modules/reflection-445.html
Normal file
File diff suppressed because one or more lines are too long
121
docs/modules/reflection-445.reflection-1.html
Normal file
121
docs/modules/reflection-445.reflection-1.html
Normal file
File diff suppressed because one or more lines are too long
107
docs/modules/reflection-560.html
Normal file
107
docs/modules/reflection-560.html
Normal file
File diff suppressed because one or more lines are too long
125
docs/modules/reflection-560.reflection-1.html
Normal file
125
docs/modules/reflection-560.reflection-1.html
Normal file
File diff suppressed because one or more lines are too long
107
docs/modules/reflection-588.html
Normal file
107
docs/modules/reflection-588.html
Normal file
File diff suppressed because one or more lines are too long
125
docs/modules/reflection-588.reflection-1.html
Normal file
125
docs/modules/reflection-588.reflection-1.html
Normal file
File diff suppressed because one or more lines are too long
107
docs/modules/reflection-607.html
Normal file
107
docs/modules/reflection-607.html
Normal file
File diff suppressed because one or more lines are too long
129
docs/modules/reflection-607.reflection-145.html
Normal file
129
docs/modules/reflection-607.reflection-145.html
Normal file
File diff suppressed because one or more lines are too long
107
docs/modules/reflection-610.html
Normal file
107
docs/modules/reflection-610.html
Normal file
File diff suppressed because one or more lines are too long
129
docs/modules/reflection-610.reflection-149.html
Normal file
129
docs/modules/reflection-610.reflection-149.html
Normal file
File diff suppressed because one or more lines are too long
107
docs/modules/reflection-611.html
Normal file
107
docs/modules/reflection-611.html
Normal file
File diff suppressed because one or more lines are too long
129
docs/modules/reflection-611.reflection-149.html
Normal file
129
docs/modules/reflection-611.reflection-149.html
Normal file
File diff suppressed because one or more lines are too long
168
docs/modules/types.html
Normal file
168
docs/modules/types.html
Normal file
File diff suppressed because one or more lines are too long
579
docs/modules/utils.html
Normal file
579
docs/modules/utils.html
Normal file
File diff suppressed because one or more lines are too long
12
example.html
12
example.html
@@ -1,12 +1,12 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" http-equiv="X-UA-Compatible" content="IE=edge" />
|
<script src="https://cdn.jsdelivr.net/combine/npm/chart.js@2.9.3,npm/jquery@3.5.1,npm/@sasjs/adapter@1.0.6"></script>
|
||||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
|
|
||||||
<script src="https://cdn.jsdelivr.net/combine/npm/chart.js@2.9.3,npm/jquery@3.5.1,npm/sasjs@2.11.0"></script>
|
|
||||||
<script>
|
<script>
|
||||||
var sasJs = new SASjs.default({appLoc: "/Products/demo/readme"
|
var sasJs = new SASjs.default({
|
||||||
,serverType:"SAS9", debug: "false"
|
appLoc: "/Public/app/readme"
|
||||||
|
,serverType:"SAS9"
|
||||||
|
,debug: false
|
||||||
});
|
});
|
||||||
function initSasJs() {
|
function initSasJs() {
|
||||||
$('#loading-spinner').show()
|
$('#loading-spinner').show()
|
||||||
@@ -70,6 +70,8 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
<meta charset="utf-8" http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container-fluid" style="text-align: center; margin-top: 10px;">
|
<div class="container-fluid" style="text-align: center; margin-top: 10px;">
|
||||||
|
|||||||
3602
package-lock.json
generated
3602
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@@ -36,26 +36,24 @@
|
|||||||
},
|
},
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cypress/webpack-preprocessor": "^4.1.5",
|
|
||||||
"@types/isomorphic-fetch": "0.0.35",
|
"@types/isomorphic-fetch": "0.0.35",
|
||||||
"@types/jest": "^26.0.3",
|
"@types/jest": "^26.0.9",
|
||||||
"cp": "^0.2.0",
|
"cp": "^0.2.0",
|
||||||
"cypress": "^4.9.0",
|
|
||||||
"jest": "^25.5.4",
|
"jest": "^25.5.4",
|
||||||
"path": "^0.12.7",
|
"path": "^0.12.7",
|
||||||
"prettier": "^2.0.5",
|
"prettier": "^2.0.5",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"semantic-release": "^17.1.1",
|
"semantic-release": "^17.1.1",
|
||||||
"ts-jest": "^25.5.1",
|
"ts-jest": "^25.5.1",
|
||||||
"ts-loader": "^7.0.5",
|
"ts-loader": "^8.0.2",
|
||||||
"tslint": "^6.1.2",
|
"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.8",
|
"typedoc-neo-theme": "^1.0.9",
|
||||||
"typedoc-plugin-external-module-name": "^4.0.3",
|
"typedoc-plugin-external-module-name": "^4.0.3",
|
||||||
"typescript": "^3.9.6",
|
"typescript": "^3.9.7",
|
||||||
"uglifyjs-webpack-plugin": "^2.2.0",
|
"uglifyjs-webpack-plugin": "^2.2.0",
|
||||||
"webpack": "^4.43.0",
|
"webpack": "^4.44.1",
|
||||||
"webpack-cli": "^3.3.12"
|
"webpack-cli": "^3.3.12"
|
||||||
},
|
},
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
|
|||||||
252
sasjs-tests/package-lock.json
generated
252
sasjs-tests/package-lock.json
generated
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sasjs-tests",
|
"name": "@sasjs/tests",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1356,11 +1356,81 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz",
|
||||||
"integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw=="
|
"integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw=="
|
||||||
},
|
},
|
||||||
|
"@sasjs/adapter": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sasjs/adapter/-/adapter-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-PcQcmb7TsfPJ94tzFnvycm+tMYD3wKx2a6niwHfsV9+g6XHtmwReVV3EPZZ5XB4s565vU6Qc+ZnFbMIAeik8QA==",
|
||||||
|
"requires": {
|
||||||
|
"es6-promise": "^4.2.8",
|
||||||
|
"form-data": "^3.0.0",
|
||||||
|
"isomorphic-fetch": "^2.2.1"
|
||||||
|
},
|
||||||
|
"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==",
|
||||||
|
"requires": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@sasjs/test-framework": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sasjs/test-framework/-/test-framework-1.3.3.tgz",
|
||||||
|
"integrity": "sha512-Ou4UXlxBAVR8jv7boVvJ/eKLHRTQvDi9LouPAasLCO2EC4AD0wX1hLMwVhmydCvsdgVEeXs6InvX3ROHiKSADg==",
|
||||||
|
"requires": {
|
||||||
|
"@types/react-highlight.js": "^1.0.0",
|
||||||
|
"immer": "^7.0.7",
|
||||||
|
"moment": "^2.27.0",
|
||||||
|
"react-highlight.js": "^1.0.7",
|
||||||
|
"semantic-ui-css": "^2.4.1",
|
||||||
|
"semantic-ui-react": "^1.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"immer": {
|
||||||
|
"version": "7.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/immer/-/immer-7.0.7.tgz",
|
||||||
|
"integrity": "sha512-Q8yYwVADJXrNfp1ZUAh4XDHkcoE3wpdpb4mC5abDSajs2EbW8+cGdPyAnglMyLnm7EF6ojD2xBFX7L5i4TIytw=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@semantic-ui-react/event-stack": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@semantic-ui-react/event-stack/-/event-stack-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-SA7VOu/tY3OkooR++mm9voeQrJpYXjJaMHO1aFCcSouS2xhqMR9Gnz0LEGLOR0h9ueWPBKaQzKIrx3FTTJZmUQ==",
|
||||||
|
"requires": {
|
||||||
|
"exenv": "^1.2.2",
|
||||||
|
"prop-types": "^15.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@sheerun/mutationobserver-shim": {
|
"@sheerun/mutationobserver-shim": {
|
||||||
"version": "0.3.3",
|
"version": "0.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.3.tgz",
|
||||||
"integrity": "sha512-DetpxZw1fzPD5xUBrIAoplLChO2VB8DlL5Gg+I1IR9b2wPqYIca2WSUxL5g1vLeR4MsQq1NeWriXAVffV+U1Fw=="
|
"integrity": "sha512-DetpxZw1fzPD5xUBrIAoplLChO2VB8DlL5Gg+I1IR9b2wPqYIca2WSUxL5g1vLeR4MsQq1NeWriXAVffV+U1Fw=="
|
||||||
},
|
},
|
||||||
|
"@stardust-ui/react-component-event-listener": {
|
||||||
|
"version": "0.38.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@stardust-ui/react-component-event-listener/-/react-component-event-listener-0.38.0.tgz",
|
||||||
|
"integrity": "sha512-sIP/e0dyOrrlb8K7KWumfMxj/gAifswTBC4o68Aa+C/GA73ccRp/6W1VlHvF/dlOR4KLsA+5SKnhjH36xzPsWg==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.1.2",
|
||||||
|
"prop-types": "^15.7.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@stardust-ui/react-component-ref": {
|
||||||
|
"version": "0.38.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@stardust-ui/react-component-ref/-/react-component-ref-0.38.0.tgz",
|
||||||
|
"integrity": "sha512-xjs6WnvJVueSIXMWw0C3oWIgAPpcD03qw43oGOjUXqFktvpNkB73JoKIhS4sCrtQxBdct75qqr4ZL6JiyPcESw==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.1.2",
|
||||||
|
"prop-types": "^15.7.2",
|
||||||
|
"react-is": "^16.6.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@svgr/babel-plugin-add-jsx-attribute": {
|
"@svgr/babel-plugin-add-jsx-attribute": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-4.2.0.tgz",
|
||||||
@@ -1836,6 +1906,14 @@
|
|||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/react-highlight.js": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-highlight.js/-/react-highlight.js-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-5VXEuo2O9L66y/2GDQSGFTggQkpOvDc/p2ma1KHadu7o/H720HK3Fr83epd4wtQky7B/RoCPat0SKyhlhiUo7A==",
|
||||||
|
"requires": {
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/react-router": {
|
"@types/react-router": {
|
||||||
"version": "5.1.8",
|
"version": "5.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.8.tgz",
|
||||||
@@ -3754,6 +3832,11 @@
|
|||||||
"shallow-clone": "^0.1.2"
|
"shallow-clone": "^0.1.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"clsx": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA=="
|
||||||
|
},
|
||||||
"co": {
|
"co": {
|
||||||
"version": "4.6.0",
|
"version": "4.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
||||||
@@ -4095,6 +4178,15 @@
|
|||||||
"sha.js": "^2.4.8"
|
"sha.js": "^2.4.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"create-react-context": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/create-react-context/-/create-react-context-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-dNldIoSuNSvlTJ7slIKC/ZFGKexBMBrrcc+TTe1NdmROnaASuLPvqpwj9v4XS4uXZ8+YPu0sNmShX2rXI5LNsw==",
|
||||||
|
"requires": {
|
||||||
|
"gud": "^1.0.0",
|
||||||
|
"warning": "^4.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"cross-spawn": {
|
"cross-spawn": {
|
||||||
"version": "6.0.5",
|
"version": "6.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
|
||||||
@@ -4912,11 +5004,21 @@
|
|||||||
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
|
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
|
||||||
},
|
},
|
||||||
"encoding": {
|
"encoding": {
|
||||||
"version": "0.1.12",
|
"version": "0.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
|
||||||
"integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=",
|
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"iconv-lite": "~0.4.13"
|
"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": {
|
"end-of-stream": {
|
||||||
@@ -5609,6 +5711,11 @@
|
|||||||
"strip-eof": "^1.0.0"
|
"strip-eof": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"exenv": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz",
|
||||||
|
"integrity": "sha1-KueOhdmJQVhnCwPUe+wfA72Ru50="
|
||||||
|
},
|
||||||
"exit": {
|
"exit": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
|
||||||
@@ -6478,6 +6585,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz",
|
||||||
"integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE="
|
"integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE="
|
||||||
},
|
},
|
||||||
|
"gud": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw=="
|
||||||
|
},
|
||||||
"gzip-size": {
|
"gzip-size": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz",
|
||||||
@@ -6622,6 +6734,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz",
|
||||||
"integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ=="
|
"integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ=="
|
||||||
},
|
},
|
||||||
|
"highlight.js": {
|
||||||
|
"version": "9.18.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.18.3.tgz",
|
||||||
|
"integrity": "sha512-zBZAmhSupHIl5sITeMqIJnYCDfAEc3Gdkqj65wC1lpI468MMQeeQkhcIAvk+RylAkxrCcI9xy9piHiXeQ1BdzQ=="
|
||||||
|
},
|
||||||
"history": {
|
"history": {
|
||||||
"version": "4.10.1",
|
"version": "4.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
|
||||||
@@ -7974,6 +8091,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"jquery": {
|
||||||
|
"version": "3.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz",
|
||||||
|
"integrity": "sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg=="
|
||||||
|
},
|
||||||
"js-base64": {
|
"js-base64": {
|
||||||
"version": "2.6.2",
|
"version": "2.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.2.tgz",
|
||||||
@@ -8123,6 +8245,11 @@
|
|||||||
"object.assign": "^4.1.0"
|
"object.assign": "^4.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"keyboard-key": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/keyboard-key/-/keyboard-key-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-qkBzPTi3rlAKvX7k0/ub44sqOfXeLc/jcnGGmj5c7BJpU8eDrEVPyhCvNYAaoubbsLm9uGWwQJO1ytQK1a9/dQ=="
|
||||||
|
},
|
||||||
"killable": {
|
"killable": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
|
||||||
@@ -8887,6 +9014,11 @@
|
|||||||
"minimist": "^1.2.5"
|
"minimist": "^1.2.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"moment": {
|
||||||
|
"version": "2.27.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/moment/-/moment-2.27.0.tgz",
|
||||||
|
"integrity": "sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ=="
|
||||||
|
},
|
||||||
"move-concurrently": {
|
"move-concurrently": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
|
||||||
@@ -9880,6 +10012,11 @@
|
|||||||
"ts-pnp": "^1.1.6"
|
"ts-pnp": "^1.1.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"popper.js": {
|
||||||
|
"version": "1.16.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
|
||||||
|
"integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ=="
|
||||||
|
},
|
||||||
"portfinder": {
|
"portfinder": {
|
||||||
"version": "1.0.26",
|
"version": "1.0.26",
|
||||||
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.26.tgz",
|
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.26.tgz",
|
||||||
@@ -11321,11 +11458,34 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.7.tgz",
|
||||||
"integrity": "sha512-TAv1KJFh3RhqxNvhzxj6LeT5NWklP6rDr2a0jaTfsZ5wSZWHOGeqQyejUp3xxLfPt2UpyJEcVQB/zyPcmonNFA=="
|
"integrity": "sha512-TAv1KJFh3RhqxNvhzxj6LeT5NWklP6rDr2a0jaTfsZ5wSZWHOGeqQyejUp3xxLfPt2UpyJEcVQB/zyPcmonNFA=="
|
||||||
},
|
},
|
||||||
|
"react-highlight.js": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-highlight.js/-/react-highlight.js-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-OVPKnV0ZvU+V//HExwbV8M9CWy49Eo/9y9pBN2OsNWUFPN6dE4YZBLmJW/5sM2DxI5v/QQLyxOnTnSSfGCP+9Q==",
|
||||||
|
"requires": {
|
||||||
|
"highlight.js": "^9.3.0",
|
||||||
|
"prop-types": "^15.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-is": {
|
"react-is": {
|
||||||
"version": "16.13.1",
|
"version": "16.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||||
},
|
},
|
||||||
|
"react-popper": {
|
||||||
|
"version": "1.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.7.tgz",
|
||||||
|
"integrity": "sha512-nmqYTx7QVjCm3WUZLeuOomna138R1luC4EqkW3hxJUrAe+3eNz3oFCLYdnPwILfn0mX1Ew2c3wctrjlUMYYUww==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.1.2",
|
||||||
|
"create-react-context": "^0.3.0",
|
||||||
|
"deep-equal": "^1.1.1",
|
||||||
|
"popper.js": "^1.14.4",
|
||||||
|
"prop-types": "^15.6.1",
|
||||||
|
"typed-styles": "^0.0.7",
|
||||||
|
"warning": "^4.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-router": {
|
"react-router": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz",
|
||||||
@@ -11950,27 +12110,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/sanitize.css/-/sanitize.css-10.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/sanitize.css/-/sanitize.css-10.0.0.tgz",
|
||||||
"integrity": "sha512-vTxrZz4dX5W86M6oVWVdOVe72ZiPs41Oi7Z6Km4W5Turyz28mrXSJhhEBZoRtzJWIv3833WKVwLSDWWkEfupMg=="
|
"integrity": "sha512-vTxrZz4dX5W86M6oVWVdOVe72ZiPs41Oi7Z6Km4W5Turyz28mrXSJhhEBZoRtzJWIv3833WKVwLSDWWkEfupMg=="
|
||||||
},
|
},
|
||||||
"sasjs": {
|
|
||||||
"version": "file:../build/sasjs-5.0.0.tgz",
|
|
||||||
"integrity": "sha512-8Ez2iS8BKzu2GG1Cwf/pe5PgNvdhowFodQNCTHIxMlDYgLqmg1mcpwRjJjnXF9A73gX0NkR65olYYAesp8cMMA==",
|
|
||||||
"requires": {
|
|
||||||
"es6-promise": "^4.2.8",
|
|
||||||
"form-data": "^3.0.0",
|
|
||||||
"isomorphic-fetch": "^2.2.1"
|
|
||||||
},
|
|
||||||
"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==",
|
|
||||||
"requires": {
|
|
||||||
"asynckit": "^0.4.0",
|
|
||||||
"combined-stream": "^1.0.8",
|
|
||||||
"mime-types": "^2.1.12"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sass-graph": {
|
"sass-graph": {
|
||||||
"version": "2.2.5",
|
"version": "2.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.5.tgz",
|
||||||
@@ -12086,6 +12225,47 @@
|
|||||||
"node-forge": "0.9.0"
|
"node-forge": "0.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"semantic-ui-css": {
|
||||||
|
"version": "2.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/semantic-ui-css/-/semantic-ui-css-2.4.1.tgz",
|
||||||
|
"integrity": "sha512-Pkp0p9oWOxlH0kODx7qFpIRYpK1T4WJOO4lNnpNPOoWKCrYsfHqYSKgk5fHfQtnWnsAKy7nLJMW02bgDWWFZFg==",
|
||||||
|
"requires": {
|
||||||
|
"jquery": "x.*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"semantic-ui-react": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/semantic-ui-react/-/semantic-ui-react-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-QtzLNkK4MUe1HQo4S7/tIkSp4NFtxSGDzTMKxmvztMJ6jt+nKGmMyjpyxJsrm3ohU8Z3sTyBUyiBsDYW4jNtjw==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.10.5",
|
||||||
|
"@semantic-ui-react/event-stack": "^3.1.0",
|
||||||
|
"@stardust-ui/react-component-event-listener": "~0.38.0",
|
||||||
|
"@stardust-ui/react-component-ref": "~0.38.0",
|
||||||
|
"clsx": "^1.1.1",
|
||||||
|
"keyboard-key": "^1.1.0",
|
||||||
|
"lodash": "^4.17.19",
|
||||||
|
"prop-types": "^15.7.2",
|
||||||
|
"react-is": "^16.8.6",
|
||||||
|
"react-popper": "^1.3.7",
|
||||||
|
"shallowequal": "^1.1.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": {
|
||||||
|
"version": "7.11.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.11.2.tgz",
|
||||||
|
"integrity": "sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==",
|
||||||
|
"requires": {
|
||||||
|
"regenerator-runtime": "^0.13.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lodash": {
|
||||||
|
"version": "4.17.19",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
|
||||||
|
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"semver": {
|
"semver": {
|
||||||
"version": "6.3.0",
|
"version": "6.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||||
@@ -12275,6 +12455,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"shallowequal": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="
|
||||||
|
},
|
||||||
"shebang-command": {
|
"shebang-command": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
|
||||||
@@ -13443,6 +13628,11 @@
|
|||||||
"mime-types": "~2.1.24"
|
"mime-types": "~2.1.24"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"typed-styles": {
|
||||||
|
"version": "0.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/typed-styles/-/typed-styles-0.0.7.tgz",
|
||||||
|
"integrity": "sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q=="
|
||||||
|
},
|
||||||
"typedarray": {
|
"typedarray": {
|
||||||
"version": "0.0.6",
|
"version": "0.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
||||||
@@ -13744,6 +13934,14 @@
|
|||||||
"makeerror": "1.0.x"
|
"makeerror": "1.0.x"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"warning": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
|
||||||
|
"requires": {
|
||||||
|
"loose-envify": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"watchpack": {
|
"watchpack": {
|
||||||
"version": "1.7.2",
|
"version": "1.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.2.tgz",
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "sasjs-tests",
|
"name": "@sasjs/tests",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"homepage": ".",
|
"homepage": ".",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@sasjs/adapter": "^1.2.0",
|
||||||
|
"@sasjs/test-framework": "^1.3.3",
|
||||||
"@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",
|
||||||
"@testing-library/user-event": "^7.2.1",
|
"@testing-library/user-event": "^7.2.1",
|
||||||
@@ -16,7 +18,6 @@
|
|||||||
"react-dom": "^16.13.1",
|
"react-dom": "^16.13.1",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-scripts": "3.4.1",
|
"react-scripts": "3.4.1",
|
||||||
"sasjs": "file:../build/sasjs-5.0.0.tgz",
|
|
||||||
"typescript": "^3.9.6"
|
"typescript": "^3.9.6"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -24,7 +25,7 @@
|
|||||||
"build": "react-scripts build",
|
"build": "react-scripts build",
|
||||||
"test": "react-scripts test",
|
"test": "react-scripts test",
|
||||||
"eject": "react-scripts eject",
|
"eject": "react-scripts eject",
|
||||||
"deploy": "rsync -avhe ssh ./build/* --delete kriaco@sas.analytium.co.uk:/var/www/html/kriaco/sasjs-tests"
|
"deploy": "cd .. && npm run package:lib && cd sasjs-tests && npm i ../build/sasjs-adapter-5.0.0.tgz && npm run build && rsync -avhe ssh ./build/* --delete kriaco@sas.analytium.co.uk:/var/www/html/kriaco/sasjs-tests"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": "react-app"
|
"extends": "react-app"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"appLoc": "/Public/app",
|
"appLoc": "/Public/app",
|
||||||
"serverType": "SASVIYA",
|
"serverType": "SASVIYA",
|
||||||
"debug": false,
|
"debug": false,
|
||||||
"contextName": null
|
"contextName": "SharedCompute",
|
||||||
|
"useComputeApi": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
.app {
|
|
||||||
padding: 16px;
|
|
||||||
|
|
||||||
.controls {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
.debug-toggle,
|
|
||||||
.app-loc-input,
|
|
||||||
.submit-button {
|
|
||||||
margin: 16px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row {
|
|
||||||
margin: 16px;
|
|
||||||
|
|
||||||
&.app-loc {
|
|
||||||
width: 20vw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-button {
|
|
||||||
padding: 16px;
|
|
||||||
font-size: 1.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-loc-input {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.debug-toggle {
|
|
||||||
display: inline-flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.label {
|
|
||||||
padding: 0 8px;
|
|
||||||
font-size: 1.25em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$height: 40px;
|
|
||||||
$width: 70px;
|
|
||||||
.switch {
|
|
||||||
position: relative;
|
|
||||||
display: inline-flex;
|
|
||||||
width: $width;
|
|
||||||
height: $height;
|
|
||||||
|
|
||||||
input[type="checkbox"] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
input:checked + .knob {
|
|
||||||
animation: colorChange 0.4s linear forwards;
|
|
||||||
}
|
|
||||||
input:checked + .knob:before {
|
|
||||||
animation: turnON 0.4s linear forwards;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes colorChange {
|
|
||||||
from {
|
|
||||||
background-color: #ccc;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
background-color: #a4d9ad;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
background-color: #4bd663;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes turnON {
|
|
||||||
from {
|
|
||||||
transform: translateX(0px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: translateX($width - ($height * 0.99));
|
|
||||||
box-shadow: -10px 0px 44px 0px #434343;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.knob {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: #ccc;
|
|
||||||
border-radius: $height;
|
|
||||||
}
|
|
||||||
|
|
||||||
.knob:before {
|
|
||||||
position: absolute;
|
|
||||||
background-color: white;
|
|
||||||
content: "";
|
|
||||||
left: $height * 0.1;
|
|
||||||
top: $height * 0.1;
|
|
||||||
width: ($height * 0.8);
|
|
||||||
height: ($height * 0.8);
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +1,30 @@
|
|||||||
import React, { ReactElement, useState, useContext, useEffect } from "react";
|
import React, { ReactElement, useState, useContext, useEffect } from "react";
|
||||||
import "./App.scss";
|
import { TestSuiteRunner, TestSuite, AppContext } from "@sasjs/test-framework";
|
||||||
import TestSuiteRunner from "./TestSuiteRunner";
|
import { basicTests } from "./testSuites/Basic";
|
||||||
import { AppContext } from "./context/AppContext";
|
import { sendArrTests, sendObjTests } from "./testSuites/RequestData";
|
||||||
|
import { specialCaseTests } from "./testSuites/SpecialCases";
|
||||||
|
import { sasjsRequestTests } from "./testSuites/SasjsRequests";
|
||||||
|
import "@sasjs/test-framework/dist/index.css";
|
||||||
|
|
||||||
const App = (): ReactElement<{}> => {
|
const App = (): ReactElement<{}> => {
|
||||||
const [appLoc, setAppLoc] = useState("");
|
const { adapter, config } = useContext(AppContext);
|
||||||
const [debug, setDebug] = useState(false);
|
const [testSuites, setTestSuites] = useState<TestSuite[]>([]);
|
||||||
const { adapter } = useContext(AppContext);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (adapter) {
|
if (adapter) {
|
||||||
adapter.setDebugState(debug);
|
setTestSuites([
|
||||||
|
basicTests(adapter, config.userName, config.password),
|
||||||
|
sendArrTests(adapter),
|
||||||
|
sendObjTests(adapter),
|
||||||
|
specialCaseTests(adapter),
|
||||||
|
sasjsRequestTests(adapter),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}, [debug, adapter]);
|
}, [adapter, config]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (appLoc && adapter) {
|
|
||||||
adapter.setSASjsConfig({ ...adapter.getSasjsConfig(), appLoc });
|
|
||||||
}
|
|
||||||
}, [appLoc, adapter]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setAppLoc(adapter.getSasjsConfig().appLoc);
|
|
||||||
}, [adapter]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app">
|
<div className="app">
|
||||||
<div className="controls">
|
{adapter && testSuites && <TestSuiteRunner testSuites={testSuites} />}
|
||||||
<div className="row">
|
|
||||||
<label>Debug</label>
|
|
||||||
<div className="debug-toggle">
|
|
||||||
<label className="switch">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
onChange={(e) => setDebug(e.target.checked)}
|
|
||||||
/>
|
|
||||||
<span className="knob"></span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="row app-loc">
|
|
||||||
<label>App Loc</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="app-loc-input"
|
|
||||||
value={appLoc}
|
|
||||||
onChange={(e) => setAppLoc(e.target.value)}
|
|
||||||
placeholder="AppLoc"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{adapter && <TestSuiteRunner adapter={adapter} />}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { ReactElement, useState, useCallback, useContext } from "react";
|
import React, { ReactElement, useState, useCallback, useContext } from "react";
|
||||||
import "./Login.scss";
|
import "./Login.scss";
|
||||||
import { AppContext } from "./context/AppContext";
|
import { AppContext } from "@sasjs/test-framework";
|
||||||
import { Redirect } from "react-router-dom";
|
import { Redirect } from "react-router-dom";
|
||||||
|
|
||||||
const Login = (): ReactElement<{}> => {
|
const Login = (): ReactElement<{}> => {
|
||||||
@@ -11,8 +11,8 @@ const Login = (): ReactElement<{}> => {
|
|||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
(e) => {
|
(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
appContext.adapter.logIn(username, password).then(() => {
|
appContext.adapter.logIn(username, password).then((res) => {
|
||||||
appContext.setIsLoggedIn(true);
|
appContext.setIsLoggedIn(res.isLoggedIn);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[username, password, appContext]
|
[username, password, appContext]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { ReactElement, useContext, FunctionComponent } from "react";
|
import React, { ReactElement, useContext, FunctionComponent } from "react";
|
||||||
import { Redirect, Route } from "react-router-dom";
|
import { Redirect, Route } from "react-router-dom";
|
||||||
import { AppContext } from "./context/AppContext";
|
import { AppContext } from "@sasjs/test-framework";
|
||||||
|
|
||||||
interface PrivateRouteProps {
|
interface PrivateRouteProps {
|
||||||
component: FunctionComponent;
|
component: FunctionComponent;
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
.button-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 16px;
|
|
||||||
|
|
||||||
.loading-spinner {
|
|
||||||
margin: 0 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-button {
|
|
||||||
padding: 10px;
|
|
||||||
min-height: 80px;
|
|
||||||
font-size: 2em;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
import React, { useEffect, useState, ReactElement, useContext } from "react";
|
|
||||||
import TestSuiteComponent from "./components/TestSuite";
|
|
||||||
import TestSuiteCard from "./components/TestSuiteCard";
|
|
||||||
import { TestSuite, Test } from "./types";
|
|
||||||
import { basicTests } from "./testSuites/Basic";
|
|
||||||
import "./TestSuiteRunner.scss";
|
|
||||||
import SASjs from "sasjs";
|
|
||||||
import { AppContext } from "./context/AppContext";
|
|
||||||
import { sendArrTests, sendObjTests } from "./testSuites/RequestData";
|
|
||||||
import { specialCaseTests } from "./testSuites/SpecialCases";
|
|
||||||
import { sasjsRequestTests } from "./testSuites/SasjsRequests";
|
|
||||||
|
|
||||||
interface TestSuiteRunnerProps {
|
|
||||||
adapter: SASjs;
|
|
||||||
}
|
|
||||||
const TestSuiteRunner = (
|
|
||||||
props: TestSuiteRunnerProps
|
|
||||||
): ReactElement<TestSuiteRunnerProps> => {
|
|
||||||
const { adapter } = props;
|
|
||||||
const { config } = useContext(AppContext);
|
|
||||||
const [testSuites, setTestSuites] = useState<TestSuite[]>([]);
|
|
||||||
const [runTests, setRunTests] = useState(false);
|
|
||||||
const [completedTestSuites, setCompletedTestSuites] = useState<
|
|
||||||
{
|
|
||||||
name: string;
|
|
||||||
completedTests: {
|
|
||||||
test: Test;
|
|
||||||
result: boolean;
|
|
||||||
error: Error | null;
|
|
||||||
executionTime: number;
|
|
||||||
}[];
|
|
||||||
}[]
|
|
||||||
>([]);
|
|
||||||
const [currentTestSuite, setCurrentTestSuite] = useState<TestSuite | null>(
|
|
||||||
(null as unknown) as TestSuite
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (adapter) {
|
|
||||||
setTestSuites([
|
|
||||||
// basicTests(adapter, config.userName, config.password),
|
|
||||||
// sendArrTests(adapter),
|
|
||||||
// sendObjTests(adapter),
|
|
||||||
specialCaseTests(adapter),
|
|
||||||
// sasjsRequestTests(adapter),
|
|
||||||
]);
|
|
||||||
setCompletedTestSuites([]);
|
|
||||||
}
|
|
||||||
}, [adapter]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (testSuites.length) {
|
|
||||||
setCurrentTestSuite(testSuites[0]);
|
|
||||||
}
|
|
||||||
}, [testSuites]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (runTests) {
|
|
||||||
setCompletedTestSuites([]);
|
|
||||||
setCurrentTestSuite(testSuites[0]);
|
|
||||||
}
|
|
||||||
}, [runTests, testSuites]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="button-container">
|
|
||||||
<button
|
|
||||||
className={runTests ? "submit-button disabled" : "submit-button"}
|
|
||||||
onClick={() => setRunTests(true)}
|
|
||||||
disabled={runTests}
|
|
||||||
>
|
|
||||||
{runTests ? (
|
|
||||||
<>
|
|
||||||
<div className="loading-spinner"></div>Running tests...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
"Run tests!"
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{completedTestSuites.map((completedTestSuite, index) => {
|
|
||||||
return (
|
|
||||||
<TestSuiteCard
|
|
||||||
key={index}
|
|
||||||
tests={completedTestSuite.completedTests}
|
|
||||||
name={completedTestSuite.name}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{currentTestSuite && runTests && (
|
|
||||||
<TestSuiteComponent
|
|
||||||
{...currentTestSuite}
|
|
||||||
onCompleted={(
|
|
||||||
name,
|
|
||||||
completedTests: {
|
|
||||||
test: Test;
|
|
||||||
result: boolean;
|
|
||||||
error: Error | null;
|
|
||||||
executionTime: number;
|
|
||||||
}[]
|
|
||||||
) => {
|
|
||||||
const currentIndex = testSuites.indexOf(currentTestSuite);
|
|
||||||
const nextIndex =
|
|
||||||
currentIndex < testSuites.length - 1 ? currentIndex + 1 : -1;
|
|
||||||
if (nextIndex >= 0) {
|
|
||||||
setCurrentTestSuite(testSuites[nextIndex]);
|
|
||||||
} else {
|
|
||||||
setCurrentTestSuite(null);
|
|
||||||
}
|
|
||||||
const newCompletedTestSuites = [
|
|
||||||
...completedTestSuites,
|
|
||||||
{ name, completedTests },
|
|
||||||
];
|
|
||||||
setCompletedTestSuites(newCompletedTestSuites);
|
|
||||||
|
|
||||||
if (newCompletedTestSuites.length === testSuites.length) {
|
|
||||||
setRunTests(false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TestSuiteRunner;
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
import React, { ReactElement, useEffect, useState } from "react";
|
|
||||||
import TestCard from "./TestCard";
|
|
||||||
import { start } from "repl";
|
|
||||||
|
|
||||||
interface TestProps {
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
beforeTest?: (...args: any) => Promise<any>;
|
|
||||||
afterTest?: (...args: any) => Promise<any>;
|
|
||||||
test: (context: any) => Promise<any>;
|
|
||||||
assertion: (...args: any) => boolean;
|
|
||||||
onCompleted: (payload: {
|
|
||||||
result: boolean;
|
|
||||||
error: Error | null;
|
|
||||||
executionTime: number;
|
|
||||||
}) => void;
|
|
||||||
context: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getStatus = (isRunning: boolean, isPassed: boolean): string => {
|
|
||||||
return isRunning ? "running" : isPassed ? "passed" : "failed";
|
|
||||||
};
|
|
||||||
|
|
||||||
const Test = (props: TestProps): ReactElement<TestProps> => {
|
|
||||||
const {
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
test,
|
|
||||||
beforeTest,
|
|
||||||
afterTest,
|
|
||||||
assertion,
|
|
||||||
onCompleted,
|
|
||||||
context,
|
|
||||||
} = props;
|
|
||||||
const beforeTestFunction = beforeTest ? beforeTest : () => Promise.resolve();
|
|
||||||
const afterTestFunction = afterTest ? afterTest : () => Promise.resolve();
|
|
||||||
const [isRunning, setIsRunning] = useState(false);
|
|
||||||
const [isPassed, setIsPassed] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (test && assertion) {
|
|
||||||
const startTime = new Date().valueOf();
|
|
||||||
setIsRunning(true);
|
|
||||||
setIsPassed(false);
|
|
||||||
beforeTestFunction()
|
|
||||||
.then(() => test(context))
|
|
||||||
.then((res) => {
|
|
||||||
setIsRunning(false);
|
|
||||||
setIsPassed(assertion(res, context));
|
|
||||||
return Promise.resolve(assertion(res, context));
|
|
||||||
})
|
|
||||||
.then((testResult) => {
|
|
||||||
afterTestFunction();
|
|
||||||
const endTime = new Date().valueOf();
|
|
||||||
const executionTime = (endTime - startTime) / 1000;
|
|
||||||
onCompleted({ result: testResult, error: null, executionTime });
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
setIsRunning(false);
|
|
||||||
setIsPassed(false);
|
|
||||||
console.error(e);
|
|
||||||
const endTime = new Date().valueOf();
|
|
||||||
const executionTime = (endTime - startTime) / 1000;
|
|
||||||
onCompleted({ result: false, error: e, executionTime });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [test, assertion]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TestCard
|
|
||||||
title={title}
|
|
||||||
description={description}
|
|
||||||
status={getStatus(isRunning, isPassed)}
|
|
||||||
error={null}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Test;
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
.test {
|
|
||||||
display: inline-flex;
|
|
||||||
padding: 8px;
|
|
||||||
margin: 8px;
|
|
||||||
flex-direction: column;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 5px;
|
|
||||||
width: 20%;
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-weight: bold;
|
|
||||||
color: #eee;
|
|
||||||
font-size: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.description,
|
|
||||||
.execution-time {
|
|
||||||
color: #c6c0c0;
|
|
||||||
padding: 8px 0;
|
|
||||||
font-size: 0.8em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.description {
|
|
||||||
min-height: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.execution-time {
|
|
||||||
color: #f9e804;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
margin-right: 8px;
|
|
||||||
display: inline-block;
|
|
||||||
|
|
||||||
&.running {
|
|
||||||
background-color: yellow;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.passed {
|
|
||||||
background-color: green;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.failed {
|
|
||||||
background-color: red;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 900px) {
|
|
||||||
.test {
|
|
||||||
width: 90%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (min-width: 901px) and (max-width: 1280px) {
|
|
||||||
.test {
|
|
||||||
width: 30%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import React, { ReactElement } from "react";
|
|
||||||
import "./TestCard.scss";
|
|
||||||
|
|
||||||
interface TestCardProps {
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
status: string;
|
|
||||||
error: Error | null;
|
|
||||||
executionTime?: number;
|
|
||||||
}
|
|
||||||
const TestCard = (props: TestCardProps): ReactElement<TestCardProps> => {
|
|
||||||
const { title, description, status, error, executionTime } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="test">
|
|
||||||
<code className="title">{title}</code>
|
|
||||||
<span className="description">{description}</span>
|
|
||||||
<span className="execution-time">
|
|
||||||
{executionTime ? executionTime.toFixed(2) + "s" : ""}
|
|
||||||
</span>
|
|
||||||
{status === "running" && (
|
|
||||||
<div>
|
|
||||||
<span className="icon running"></span>Running...
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{status === "passed" && (
|
|
||||||
<div>
|
|
||||||
<span className="icon passed"></span>Passed
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{status === "failed" && (
|
|
||||||
<>
|
|
||||||
<div>
|
|
||||||
<span className="icon failed"></span>Failed
|
|
||||||
</div>
|
|
||||||
{!!error && <code>{error.message}</code>}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TestCard;
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
import React, { ReactElement, useState, useEffect } from "react";
|
|
||||||
import "./TestSuiteCard.scss";
|
|
||||||
import { Test } from "../types";
|
|
||||||
import TestComponent from "./Test";
|
|
||||||
import TestCard from "./TestCard";
|
|
||||||
|
|
||||||
interface TestSuiteProps {
|
|
||||||
name: string;
|
|
||||||
tests: Test[];
|
|
||||||
beforeAll?: (...args: any) => Promise<any>;
|
|
||||||
afterAll?: (...args: any) => Promise<any>;
|
|
||||||
onCompleted: (
|
|
||||||
name: string,
|
|
||||||
completedTests: {
|
|
||||||
test: Test;
|
|
||||||
result: boolean;
|
|
||||||
error: Error | null;
|
|
||||||
executionTime: number;
|
|
||||||
}[]
|
|
||||||
) => void;
|
|
||||||
}
|
|
||||||
const TestSuite = (props: TestSuiteProps): ReactElement<TestSuiteProps> => {
|
|
||||||
const { name, tests, beforeAll, afterAll, onCompleted } = props;
|
|
||||||
const [context, setContext] = useState<any>(null);
|
|
||||||
const [completedTests, setCompletedTests] = useState<
|
|
||||||
{
|
|
||||||
test: Test;
|
|
||||||
result: boolean;
|
|
||||||
error: Error | null;
|
|
||||||
executionTime: number;
|
|
||||||
}[]
|
|
||||||
>([]);
|
|
||||||
const [currentTest, setCurrentTest] = useState<Test | null>(
|
|
||||||
(null as unknown) as Test
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (beforeAll) {
|
|
||||||
beforeAll().then((data) => setContext({ data }));
|
|
||||||
}
|
|
||||||
}, [beforeAll]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (tests.length) {
|
|
||||||
setCurrentTest(tests[0]);
|
|
||||||
}
|
|
||||||
setCompletedTests([]);
|
|
||||||
setContext(null);
|
|
||||||
}, [tests]);
|
|
||||||
|
|
||||||
return (!!beforeAll && !!context) || !beforeAll ? (
|
|
||||||
<div className="test-suite">
|
|
||||||
<div className="test-suite-name running">{name}</div>
|
|
||||||
{currentTest && (
|
|
||||||
<TestComponent
|
|
||||||
{...currentTest}
|
|
||||||
context={context}
|
|
||||||
onCompleted={(completedTest) => {
|
|
||||||
const newCompleteTests = [
|
|
||||||
...completedTests,
|
|
||||||
{
|
|
||||||
test: currentTest,
|
|
||||||
result: completedTest.result,
|
|
||||||
error: completedTest.error,
|
|
||||||
executionTime: completedTest.executionTime,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
setCompletedTests(newCompleteTests);
|
|
||||||
const currentIndex = tests.indexOf(currentTest);
|
|
||||||
const nextIndex =
|
|
||||||
currentIndex < tests.length - 1 ? currentIndex + 1 : -1;
|
|
||||||
if (nextIndex >= 0) {
|
|
||||||
setCurrentTest(tests[nextIndex]);
|
|
||||||
} else {
|
|
||||||
setCurrentTest(null);
|
|
||||||
}
|
|
||||||
if (newCompleteTests.length === tests.length) {
|
|
||||||
if (afterAll) {
|
|
||||||
afterAll().then(() => onCompleted(name, newCompleteTests));
|
|
||||||
} else {
|
|
||||||
onCompleted(name, newCompleteTests);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{completedTests.map((completedTest, index) => {
|
|
||||||
const { test, result, error } = completedTest;
|
|
||||||
const { title, description } = test;
|
|
||||||
return (
|
|
||||||
<TestCard
|
|
||||||
key={index}
|
|
||||||
title={title}
|
|
||||||
description={description}
|
|
||||||
status={result === true ? "passed" : "failed"}
|
|
||||||
error={error}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TestSuite;
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
.test-suite {
|
|
||||||
.test-suite-name {
|
|
||||||
font-size: 1.5em;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #1f2027;
|
|
||||||
|
|
||||||
&.passed {
|
|
||||||
color: green;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.failed {
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.running {
|
|
||||||
color: yellow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import React, { ReactElement } from "react";
|
|
||||||
import "./TestSuiteCard.scss";
|
|
||||||
import { Test } from "../types";
|
|
||||||
import TestCard from "./TestCard";
|
|
||||||
|
|
||||||
interface TestSuiteCardProps {
|
|
||||||
name: string;
|
|
||||||
tests: {
|
|
||||||
test: Test;
|
|
||||||
result: boolean;
|
|
||||||
error: Error | null;
|
|
||||||
executionTime: number;
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
const TestSuiteCard = (
|
|
||||||
props: TestSuiteCardProps
|
|
||||||
): ReactElement<TestSuiteCardProps> => {
|
|
||||||
const { name, tests } = props;
|
|
||||||
const overallStatus = tests.map((t) => t.result).reduce((x, y) => x && y);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="test-suite">
|
|
||||||
<div className={`test-suite-name ${overallStatus ? "passed" : "failed"}`}>
|
|
||||||
{name}
|
|
||||||
</div>
|
|
||||||
{tests.map((completedTest, index) => {
|
|
||||||
const { test, result, error, executionTime } = completedTest;
|
|
||||||
const { title, description } = test;
|
|
||||||
return (
|
|
||||||
<TestCard
|
|
||||||
key={index}
|
|
||||||
title={title}
|
|
||||||
description={description}
|
|
||||||
status={result === true ? "passed" : "failed"}
|
|
||||||
error={error}
|
|
||||||
executionTime={executionTime}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TestSuiteCard;
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import React, { createContext, useState, useEffect, ReactNode } from "react";
|
|
||||||
import SASjs from "sasjs";
|
|
||||||
|
|
||||||
export const AppContext = createContext<{
|
|
||||||
config: any;
|
|
||||||
sasJsConfig: any;
|
|
||||||
isLoggedIn: boolean;
|
|
||||||
setIsLoggedIn: (value: boolean) => void;
|
|
||||||
adapter: SASjs;
|
|
||||||
}>({
|
|
||||||
config: null,
|
|
||||||
sasJsConfig: null,
|
|
||||||
isLoggedIn: false,
|
|
||||||
setIsLoggedIn: (null as unknown) as (value: boolean) => void,
|
|
||||||
adapter: (null as unknown) as SASjs,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const AppProvider = (props: { children: ReactNode }) => {
|
|
||||||
const [config, setConfig] = useState<{ sasJsConfig: any }>({
|
|
||||||
sasJsConfig: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [adapter, setAdapter] = useState<SASjs>((null as unknown) as SASjs);
|
|
||||||
|
|
||||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetch("config.json")
|
|
||||||
.then((res) => res.json())
|
|
||||||
.then((configJson: any) => {
|
|
||||||
setConfig(configJson);
|
|
||||||
const sasjs = new SASjs(configJson.sasJsConfig);
|
|
||||||
setAdapter(sasjs);
|
|
||||||
sasjs.checkSession().then((response) => {
|
|
||||||
setIsLoggedIn(response.isLoggedIn);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AppContext.Provider
|
|
||||||
value={{
|
|
||||||
config,
|
|
||||||
sasJsConfig: config.sasJsConfig,
|
|
||||||
isLoggedIn,
|
|
||||||
setIsLoggedIn,
|
|
||||||
adapter,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</AppContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -3,7 +3,7 @@ import ReactDOM from "react-dom";
|
|||||||
import { Route, HashRouter, Switch } from "react-router-dom";
|
import { Route, HashRouter, Switch } from "react-router-dom";
|
||||||
import "./index.scss";
|
import "./index.scss";
|
||||||
import * as serviceWorker from "./serviceWorker";
|
import * as serviceWorker from "./serviceWorker";
|
||||||
import { AppProvider } from "./context/AppContext";
|
import { AppProvider } from "@sasjs/test-framework";
|
||||||
import PrivateRoute from "./PrivateRoute";
|
import PrivateRoute from "./PrivateRoute";
|
||||||
import Login from "./Login";
|
import Login from "./Login";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import SASjs, { ServerType, SASjsConfig } from "sasjs";
|
import SASjs, { ServerType, SASjsConfig } from "@sasjs/adapter";
|
||||||
import { TestSuite } from "../types";
|
import { TestSuite } from "@sasjs/test-framework";
|
||||||
|
|
||||||
const defaultConfig: SASjsConfig = {
|
const defaultConfig: SASjsConfig = {
|
||||||
serverUrl: window.location.origin,
|
serverUrl: window.location.origin,
|
||||||
@@ -9,6 +9,7 @@ const defaultConfig: SASjsConfig = {
|
|||||||
serverType: ServerType.SASViya,
|
serverType: ServerType.SASViya,
|
||||||
debug: true,
|
debug: true,
|
||||||
contextName: "SAS Job Execution compute context",
|
contextName: "SAS Job Execution compute context",
|
||||||
|
useComputeApi: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const customConfig = {
|
const customConfig = {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import SASjs from "sasjs";
|
import SASjs from "@sasjs/adapter";
|
||||||
import { TestSuite } from "../types";
|
import { TestSuite } from "@sasjs/test-framework";
|
||||||
|
|
||||||
const stringData: any = { table1: [{ col1: "first col value" }] };
|
const stringData: any = { table1: [{ col1: "first col value" }] };
|
||||||
const numericData: any = { table1: [{ col1: 3.14159265 }] };
|
const numericData: any = { table1: [{ col1: 3.14159265 }] };
|
||||||
@@ -74,9 +74,8 @@ export const sendArrTests = (adapter: SASjs): TestSuite => ({
|
|||||||
description:
|
description:
|
||||||
"Should error out with long string values over 32765 characters",
|
"Should error out with long string values over 32765 characters",
|
||||||
test: () => {
|
test: () => {
|
||||||
return adapter
|
const data = getLongStringData(32767);
|
||||||
.request("common/sendArr", getLongStringData(32767))
|
return adapter.request("common/sendArr", data).catch((e) => e);
|
||||||
.catch((e) => e);
|
|
||||||
},
|
},
|
||||||
assertion: (error: any) => {
|
assertion: (error: any) => {
|
||||||
return !!error && !!error.MESSAGE;
|
return !!error && !!error.MESSAGE;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import SASjs from "sasjs";
|
import SASjs from "@sasjs/adapter";
|
||||||
import { TestSuite } from "../types";
|
import { TestSuite } from "@sasjs/test-framework";
|
||||||
|
|
||||||
const data: any = { table1: [{ col1: "first col value" }] };
|
const data: any = { table1: [{ col1: "first col value" }] };
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ export const sasjsRequestTests = (adapter: SASjs): TestSuite => ({
|
|||||||
test: async () => {
|
test: async () => {
|
||||||
return adapter.request("common/sendArr", data);
|
return adapter.request("common/sendArr", data);
|
||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: () => {
|
||||||
const requests = adapter.getSasRequests();
|
const requests = adapter.getSasRequests();
|
||||||
if (adapter.getSasjsConfig().debug) {
|
if (adapter.getSasjsConfig().debug) {
|
||||||
return requests[0].SASWORK !== null;
|
return requests[0].SASWORK !== null;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import SASjs from "sasjs";
|
import SASjs from "@sasjs/adapter";
|
||||||
import { TestSuite } from "../types";
|
import { TestSuite } from "@sasjs/test-framework";
|
||||||
|
|
||||||
const specialCharData: any = {
|
const specialCharData: any = {
|
||||||
table1: [
|
table1: [
|
||||||
@@ -102,86 +102,86 @@ export const specialCaseTests = (adapter: SASjs): TestSuite => ({
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// title: "Other special characters",
|
|
||||||
// description: "Should handle other special characters",
|
|
||||||
// test: () => {
|
|
||||||
// return adapter.request("common/sendArr", moreSpecialCharData);
|
|
||||||
// },
|
|
||||||
// assertion: (res: any) => {
|
|
||||||
// return (
|
|
||||||
// res.table1[0][0] === moreSpecialCharData.table1[0].speech0 &&
|
|
||||||
// res.table1[0][1] === moreSpecialCharData.table1[0].pct &&
|
|
||||||
// res.table1[0][2] === moreSpecialCharData.table1[0].speech &&
|
|
||||||
// res.table1[0][3] === moreSpecialCharData.table1[0].slash &&
|
|
||||||
// res.table1[0][4] === moreSpecialCharData.table1[0].slashWithSpecial &&
|
|
||||||
// res.table1[0][5] === moreSpecialCharData.table1[0].macvar &&
|
|
||||||
// res.table1[0][6] === moreSpecialCharData.table1[0].chinese &&
|
|
||||||
// res.table1[0][7] === moreSpecialCharData.table1[0].sigma &&
|
|
||||||
// res.table1[0][8] === moreSpecialCharData.table1[0].at &&
|
|
||||||
// res.table1[0][9] === moreSpecialCharData.table1[0].serbian &&
|
|
||||||
// res.table1[0][10] === moreSpecialCharData.table1[0].dollar
|
|
||||||
// );
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// title: "Wide table with sendArr",
|
|
||||||
// description: "Should handle data with 10000 columns",
|
|
||||||
// test: () => {
|
|
||||||
// return adapter.request("common/sendArr", getWideData());
|
|
||||||
// },
|
|
||||||
// assertion: (res: any) => {
|
|
||||||
// const data = getWideData();
|
|
||||||
// let result = true;
|
|
||||||
// for (let i = 0; i <= 10; i++) {
|
|
||||||
// result =
|
|
||||||
// result && res.table1[0][i] === data.table1[0]["col" + (i + 1)];
|
|
||||||
// }
|
|
||||||
// return result;
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// title: "Wide table with sendObj",
|
|
||||||
// description: "Should handle data with 10000 columns",
|
|
||||||
// test: () => {
|
|
||||||
// return adapter.request("common/sendObj", getWideData());
|
|
||||||
// },
|
|
||||||
// assertion: (res: any) => {
|
|
||||||
// const data = getWideData();
|
|
||||||
// let result = true;
|
|
||||||
// for (let i = 0; i <= 10; i++) {
|
|
||||||
// result =
|
|
||||||
// result &&
|
|
||||||
// res.table1[0]["COL" + (i + 1)] === data.table1[0]["col" + (i + 1)];
|
|
||||||
// }
|
|
||||||
// return result;
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// title: "Multiple tables",
|
|
||||||
// description: "Should handle data with 100 tables",
|
|
||||||
// test: () => {
|
|
||||||
// return adapter.request("common/sendArr", getTables());
|
|
||||||
// },
|
|
||||||
// assertion: (res: any) => {
|
|
||||||
// const data = getTables();
|
|
||||||
// return (
|
|
||||||
// res.table1[0][0] === data.table1[0].col1 &&
|
|
||||||
// res.table1[0][1] === data.table1[0].col2 &&
|
|
||||||
// res.table1[0][2] === data.table1[0].col3 &&
|
|
||||||
// res.table1[0][3] === data.table1[0].col4 &&
|
|
||||||
// res.table50[0][0] === data.table50[0].col1 &&
|
|
||||||
// res.table50[0][1] === data.table50[0].col2 &&
|
|
||||||
// res.table50[0][2] === data.table50[0].col3 &&
|
|
||||||
// res.table50[0][3] === data.table50[0].col4
|
|
||||||
// );
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
title: "Large dataset",
|
title: "Other special characters",
|
||||||
|
description: "Should handle other special characters",
|
||||||
|
test: () => {
|
||||||
|
return adapter.request("common/sendArr", moreSpecialCharData);
|
||||||
|
},
|
||||||
|
assertion: (res: any) => {
|
||||||
|
return (
|
||||||
|
res.table1[0][0] === moreSpecialCharData.table1[0].speech0 &&
|
||||||
|
res.table1[0][1] === moreSpecialCharData.table1[0].pct &&
|
||||||
|
res.table1[0][2] === moreSpecialCharData.table1[0].speech &&
|
||||||
|
res.table1[0][3] === moreSpecialCharData.table1[0].slash &&
|
||||||
|
res.table1[0][4] === moreSpecialCharData.table1[0].slashWithSpecial &&
|
||||||
|
res.table1[0][5] === moreSpecialCharData.table1[0].macvar &&
|
||||||
|
res.table1[0][6] === moreSpecialCharData.table1[0].chinese &&
|
||||||
|
res.table1[0][7] === moreSpecialCharData.table1[0].sigma &&
|
||||||
|
res.table1[0][8] === moreSpecialCharData.table1[0].at &&
|
||||||
|
res.table1[0][9] === moreSpecialCharData.table1[0].serbian &&
|
||||||
|
res.table1[0][10] === moreSpecialCharData.table1[0].dollar
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Wide table with sendArr",
|
||||||
|
description: "Should handle data with 10000 columns",
|
||||||
|
test: () => {
|
||||||
|
return adapter.request("common/sendArr", getWideData());
|
||||||
|
},
|
||||||
|
assertion: (res: any) => {
|
||||||
|
const data = getWideData();
|
||||||
|
let result = true;
|
||||||
|
for (let i = 0; i <= 10; i++) {
|
||||||
|
result =
|
||||||
|
result && res.table1[0][i] === data.table1[0]["col" + (i + 1)];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Wide table with sendObj",
|
||||||
|
description: "Should handle data with 10000 columns",
|
||||||
|
test: () => {
|
||||||
|
return adapter.request("common/sendObj", getWideData());
|
||||||
|
},
|
||||||
|
assertion: (res: any) => {
|
||||||
|
const data = getWideData();
|
||||||
|
let result = true;
|
||||||
|
for (let i = 0; i <= 10; i++) {
|
||||||
|
result =
|
||||||
|
result &&
|
||||||
|
res.table1[0]["COL" + (i + 1)] === data.table1[0]["col" + (i + 1)];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Multiple tables",
|
||||||
|
description: "Should handle data with 100 tables",
|
||||||
|
test: () => {
|
||||||
|
return adapter.request("common/sendArr", getTables());
|
||||||
|
},
|
||||||
|
assertion: (res: any) => {
|
||||||
|
const data = getTables();
|
||||||
|
return (
|
||||||
|
res.table1[0][0] === data.table1[0].col1 &&
|
||||||
|
res.table1[0][1] === data.table1[0].col2 &&
|
||||||
|
res.table1[0][2] === data.table1[0].col3 &&
|
||||||
|
res.table1[0][3] === data.table1[0].col4 &&
|
||||||
|
res.table50[0][0] === data.table50[0].col1 &&
|
||||||
|
res.table50[0][1] === data.table50[0].col2 &&
|
||||||
|
res.table50[0][2] === data.table50[0].col3 &&
|
||||||
|
res.table50[0][3] === data.table50[0].col4
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Large dataset with sendObj",
|
||||||
description: "Should handle 5mb of data",
|
description: "Should handle 5mb of data",
|
||||||
test: () => {
|
test: () => {
|
||||||
return adapter.request("common/sendArr", getLargeDataset());
|
return adapter.request("common/sendObj", getLargeDataset());
|
||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => {
|
||||||
const data = getLargeDataset();
|
const data = getLargeDataset();
|
||||||
@@ -192,44 +192,59 @@ export const specialCaseTests = (adapter: SASjs): TestSuite => ({
|
|||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
// {
|
title: "Large dataset with sendArr",
|
||||||
// title: "Error and _csrf tables with sendArr",
|
description: "Should handle 5mb of data",
|
||||||
// description: "Should handle error and _csrf tables",
|
test: () => {
|
||||||
// test: () => {
|
return adapter.request("common/sendArr", getLargeDataset());
|
||||||
// return adapter.request("common/sendArr", errorAndCsrfData);
|
},
|
||||||
// },
|
assertion: (res: any) => {
|
||||||
// assertion: (res: any) => {
|
const data = getLargeDataset();
|
||||||
// return (
|
let result = true;
|
||||||
// res.error[0][0] === errorAndCsrfData.error[0].col1 &&
|
for (let i = 0; i <= 10; i++) {
|
||||||
// res.error[0][1] === errorAndCsrfData.error[0].col2 &&
|
result =
|
||||||
// res.error[0][2] === errorAndCsrfData.error[0].col3 &&
|
result && res.table1[i][0] === Object.values(data.table1[i])[0];
|
||||||
// res.error[0][3] === errorAndCsrfData.error[0].col4 &&
|
}
|
||||||
// res._csrf[0][0] === errorAndCsrfData._csrf[0].col1 &&
|
return result;
|
||||||
// res._csrf[0][1] === errorAndCsrfData._csrf[0].col2 &&
|
},
|
||||||
// res._csrf[0][2] === errorAndCsrfData._csrf[0].col3 &&
|
},
|
||||||
// res._csrf[0][3] === errorAndCsrfData._csrf[0].col4
|
{
|
||||||
// );
|
title: "Error and _csrf tables with sendArr",
|
||||||
// },
|
description: "Should handle error and _csrf tables",
|
||||||
// },
|
test: () => {
|
||||||
// {
|
return adapter.request("common/sendArr", errorAndCsrfData);
|
||||||
// title: "Error and _csrf tables with sendObj",
|
},
|
||||||
// description: "Should handle error and _csrf tables",
|
assertion: (res: any) => {
|
||||||
// test: () => {
|
return (
|
||||||
// return adapter.request("common/sendObj", errorAndCsrfData);
|
res.error[0][0] === errorAndCsrfData.error[0].col1 &&
|
||||||
// },
|
res.error[0][1] === errorAndCsrfData.error[0].col2 &&
|
||||||
// assertion: (res: any) => {
|
res.error[0][2] === errorAndCsrfData.error[0].col3 &&
|
||||||
// return (
|
res.error[0][3] === errorAndCsrfData.error[0].col4 &&
|
||||||
// res.error[0].COL1 === errorAndCsrfData.error[0].col1 &&
|
res._csrf[0][0] === errorAndCsrfData._csrf[0].col1 &&
|
||||||
// res.error[0].COL2 === errorAndCsrfData.error[0].col2 &&
|
res._csrf[0][1] === errorAndCsrfData._csrf[0].col2 &&
|
||||||
// res.error[0].COL3 === errorAndCsrfData.error[0].col3 &&
|
res._csrf[0][2] === errorAndCsrfData._csrf[0].col3 &&
|
||||||
// res.error[0].COL4 === errorAndCsrfData.error[0].col4 &&
|
res._csrf[0][3] === errorAndCsrfData._csrf[0].col4
|
||||||
// res._csrf[0].COL1 === errorAndCsrfData._csrf[0].col1 &&
|
);
|
||||||
// res._csrf[0].COL2 === errorAndCsrfData._csrf[0].col2 &&
|
},
|
||||||
// res._csrf[0].COL3 === errorAndCsrfData._csrf[0].col3 &&
|
},
|
||||||
// res._csrf[0].COL4 === errorAndCsrfData._csrf[0].col4
|
{
|
||||||
// );
|
title: "Error and _csrf tables with sendObj",
|
||||||
// },
|
description: "Should handle error and _csrf tables",
|
||||||
// },
|
test: () => {
|
||||||
|
return adapter.request("common/sendObj", errorAndCsrfData);
|
||||||
|
},
|
||||||
|
assertion: (res: any) => {
|
||||||
|
return (
|
||||||
|
res.error[0].COL1 === errorAndCsrfData.error[0].col1 &&
|
||||||
|
res.error[0].COL2 === errorAndCsrfData.error[0].col2 &&
|
||||||
|
res.error[0].COL3 === errorAndCsrfData.error[0].col3 &&
|
||||||
|
res.error[0].COL4 === errorAndCsrfData.error[0].col4 &&
|
||||||
|
res._csrf[0].COL1 === errorAndCsrfData._csrf[0].col1 &&
|
||||||
|
res._csrf[0].COL2 === errorAndCsrfData._csrf[0].col2 &&
|
||||||
|
res._csrf[0].COL3 === errorAndCsrfData._csrf[0].col3 &&
|
||||||
|
res._csrf[0].COL4 === errorAndCsrfData._csrf[0].col4
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
export interface Test {
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
beforeTest?: (...args: any) => Promise<any>;
|
|
||||||
afterTest?: (...args: any) => Promise<any>;
|
|
||||||
test: (context?: any) => Promise<any>;
|
|
||||||
assertion: (...args: any) => boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TestSuite {
|
|
||||||
name: string;
|
|
||||||
tests: Test[];
|
|
||||||
beforeAll?: (...args: any) => Promise<any>;
|
|
||||||
afterAll?: (...args: any) => Promise<any>;
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
export const uploadFile = (file: File, fileName: string, url: string) => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const data = new FormData();
|
|
||||||
data.append("file", file);
|
|
||||||
data.append("filename", fileName);
|
|
||||||
const xhr = new XMLHttpRequest();
|
|
||||||
xhr.withCredentials = true;
|
|
||||||
xhr.addEventListener("readystatechange", function () {
|
|
||||||
if (this.readyState === 4) {
|
|
||||||
let response: any;
|
|
||||||
try {
|
|
||||||
response = JSON.parse(this.responseText);
|
|
||||||
} catch (e) {
|
|
||||||
reject(e);
|
|
||||||
}
|
|
||||||
resolve(response);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
xhr.open("POST", url);
|
|
||||||
xhr.setRequestHeader("cache-control", "no-cache");
|
|
||||||
xhr.send(data);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
97
src/FileUploader.ts
Normal file
97
src/FileUploader.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import { isLogInRequired, needsRetry } from "./utils";
|
||||||
|
import { CsrfToken } from "./types/CsrfToken";
|
||||||
|
import { UploadFile } from "./types/UploadFile";
|
||||||
|
|
||||||
|
const requestRetryLimit = 5;
|
||||||
|
|
||||||
|
export class FileUploader {
|
||||||
|
constructor(
|
||||||
|
private appLoc: string,
|
||||||
|
private serverUrl: string,
|
||||||
|
private jobsPath: string,
|
||||||
|
private csrfToken: CsrfToken | null = null
|
||||||
|
) {}
|
||||||
|
private retryCount = 0;
|
||||||
|
|
||||||
|
public uploadFile(sasJob: string, files: UploadFile[], params: any) {
|
||||||
|
if (files?.length < 1) throw new Error("Atleast one file must be provided");
|
||||||
|
|
||||||
|
let paramsString = "";
|
||||||
|
|
||||||
|
for (let param in params) {
|
||||||
|
if (params.hasOwnProperty(param)) {
|
||||||
|
paramsString += `&${param}=${params[param]}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const program = this.appLoc
|
||||||
|
? this.appLoc.replace(/\/?$/, "/") + sasJob.replace(/^\//, "")
|
||||||
|
: sasJob;
|
||||||
|
const uploadUrl = `${this.serverUrl}${this.jobsPath}/?${
|
||||||
|
"_program=" + program
|
||||||
|
}${paramsString}`;
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
"cache-control": "no-cache",
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
for (let file of files) {
|
||||||
|
formData.append("file", file.file, file.fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.csrfToken) formData.append("_csrf", this.csrfToken.value);
|
||||||
|
|
||||||
|
fetch(uploadUrl, {
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
referrerPolicy: "same-origin",
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
.then(async (response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 403) {
|
||||||
|
const tokenHeader = response.headers.get("X-CSRF-HEADER");
|
||||||
|
|
||||||
|
if (tokenHeader) {
|
||||||
|
const token = response.headers.get(tokenHeader);
|
||||||
|
this.csrfToken = {
|
||||||
|
headerName: tokenHeader,
|
||||||
|
value: token || "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.text();
|
||||||
|
})
|
||||||
|
.then((responseText) => {
|
||||||
|
if (isLogInRequired(responseText))
|
||||||
|
reject("You must be logged in to upload a fle");
|
||||||
|
|
||||||
|
if (needsRetry(responseText)) {
|
||||||
|
if (this.retryCount < requestRetryLimit) {
|
||||||
|
this.retryCount++;
|
||||||
|
this.uploadFile(sasJob, files, params).then(
|
||||||
|
(res: any) => resolve(res),
|
||||||
|
(err: any) => reject(err)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.retryCount = 0;
|
||||||
|
reject(responseText);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.retryCount = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
resolve(JSON.parse(responseText));
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,10 @@ import {
|
|||||||
} from "./utils";
|
} from "./utils";
|
||||||
import * as NodeFormData from "form-data";
|
import * as NodeFormData from "form-data";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import { Job, Session, Context, Folder } from "./types";
|
import { Job, Session, Context, Folder, CsrfToken } from "./types";
|
||||||
|
import { JobDefinition } from "./types/JobDefinition";
|
||||||
|
import { formatDataForRequest } from "./utils/formatDataForRequest";
|
||||||
|
import { SessionManager } from "./SessionManager";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A client for interfacing with the SAS Viya REST API
|
* A client for interfacing with the SAS Viya REST API
|
||||||
@@ -16,14 +19,21 @@ export class SASViyaApiClient {
|
|||||||
constructor(
|
constructor(
|
||||||
private serverUrl: string,
|
private serverUrl: string,
|
||||||
private rootFolderName: string,
|
private rootFolderName: string,
|
||||||
|
private contextName: string,
|
||||||
|
private setCsrfToken: (csrfToken: CsrfToken) => void,
|
||||||
private rootFolderMap = new Map<string, Job[]>()
|
private rootFolderMap = new Map<string, Job[]>()
|
||||||
) {
|
) {
|
||||||
if (!rootFolderName) {
|
if (!rootFolderName) {
|
||||||
throw new Error("Root folder must be provided.");
|
throw new Error("Root folder must be provided.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private csrfToken: { headerName: string; value: string } | null = null;
|
private csrfToken: CsrfToken | null = null;
|
||||||
private rootFolder: Folder | null = null;
|
private rootFolder: Folder | null = null;
|
||||||
|
private sessionManager = new SessionManager(
|
||||||
|
this.serverUrl,
|
||||||
|
this.contextName,
|
||||||
|
this.setCsrfToken
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a map containing the directory structure in the currently set root folder.
|
* Returns a map containing the directory structure in the currently set root folder.
|
||||||
@@ -32,7 +42,7 @@ export class SASViyaApiClient {
|
|||||||
if (this.rootFolderMap.size) {
|
if (this.rootFolderMap.size) {
|
||||||
return this.rootFolderMap;
|
return this.rootFolderMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.populateRootFolderMap();
|
this.populateRootFolderMap();
|
||||||
return this.rootFolderMap;
|
return this.rootFolderMap;
|
||||||
}
|
}
|
||||||
@@ -68,7 +78,7 @@ export class SASViyaApiClient {
|
|||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
headers.Authorization = `Bearer ${accessToken}`;
|
headers.Authorization = `Bearer ${accessToken}`;
|
||||||
}
|
}
|
||||||
const contexts = await this.request<{ items: Context[] }>(
|
const { result: contexts } = await this.request<{ items: Context[] }>(
|
||||||
`${this.serverUrl}/compute/contexts`,
|
`${this.serverUrl}/compute/contexts`,
|
||||||
{ headers }
|
{ headers }
|
||||||
);
|
);
|
||||||
@@ -93,7 +103,8 @@ export class SASViyaApiClient {
|
|||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
headers.Authorization = `Bearer ${accessToken}`;
|
headers.Authorization = `Bearer ${accessToken}`;
|
||||||
}
|
}
|
||||||
const contexts = await this.request<{ items: Context[] }>(
|
|
||||||
|
const { result: contexts } = await this.request<{ items: Context[] }>(
|
||||||
`${this.serverUrl}/compute/contexts`,
|
`${this.serverUrl}/compute/contexts`,
|
||||||
{ headers }
|
{ headers }
|
||||||
);
|
);
|
||||||
@@ -106,9 +117,7 @@ export class SASViyaApiClient {
|
|||||||
`test-${context.name}`,
|
`test-${context.name}`,
|
||||||
linesOfCode,
|
linesOfCode,
|
||||||
context.name,
|
context.name,
|
||||||
accessToken,
|
accessToken
|
||||||
undefined,
|
|
||||||
true
|
|
||||||
).catch(() => null);
|
).catch(() => null);
|
||||||
});
|
});
|
||||||
const results = await Promise.all(promises);
|
const results = await Promise.all(promises);
|
||||||
@@ -153,7 +162,7 @@ export class SASViyaApiClient {
|
|||||||
headers.Authorization = `Bearer ${accessToken}`;
|
headers.Authorization = `Bearer ${accessToken}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const contexts = await this.request<{ items: Context[] }>(
|
const { result: contexts } = await this.request<{ items: Context[] }>(
|
||||||
`${this.serverUrl}/compute/contexts`,
|
`${this.serverUrl}/compute/contexts`,
|
||||||
{ headers }
|
{ headers }
|
||||||
);
|
);
|
||||||
@@ -172,7 +181,7 @@ export class SASViyaApiClient {
|
|||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const createdSession = this.request<Session>(
|
const { result: createdSession } = await this.request<Session>(
|
||||||
`${this.serverUrl}/compute/contexts/${executionContext.id}/sessions`,
|
`${this.serverUrl}/compute/contexts/${executionContext.id}/sessions`,
|
||||||
createSessionRequest
|
createSessionRequest
|
||||||
);
|
);
|
||||||
@@ -190,98 +199,153 @@ export class SASViyaApiClient {
|
|||||||
* @param silent - optional flag to turn of logging.
|
* @param silent - optional flag to turn of logging.
|
||||||
*/
|
*/
|
||||||
public async executeScript(
|
public async executeScript(
|
||||||
fileName: string,
|
jobName: string,
|
||||||
linesOfCode: string[],
|
linesOfCode: string[],
|
||||||
contextName: string,
|
contextName: string,
|
||||||
accessToken?: string,
|
accessToken?: string,
|
||||||
sessionId = "",
|
silent = false,
|
||||||
silent = false
|
data = null,
|
||||||
|
debug = false
|
||||||
) {
|
) {
|
||||||
|
silent = !debug;
|
||||||
|
|
||||||
const headers: any = {
|
const headers: any = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
};
|
};
|
||||||
|
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
headers.Authorization = `Bearer ${accessToken}`;
|
headers.Authorization = `Bearer ${accessToken}`;
|
||||||
}
|
}
|
||||||
if (this.csrfToken) {
|
|
||||||
headers[this.csrfToken.headerName] = this.csrfToken.value;
|
let executionSessionId: string;
|
||||||
|
const session = await this.sessionManager.getSession(accessToken);
|
||||||
|
executionSessionId = session!.id;
|
||||||
|
|
||||||
|
const jobArguments: { [key: string]: any } = {
|
||||||
|
_contextName: contextName,
|
||||||
|
_OMITJSONLISTING: true,
|
||||||
|
_OMITJSONLOG: true,
|
||||||
|
_OMITSESSIONRESULTS: true,
|
||||||
|
_OMITTEXTLISTING: true,
|
||||||
|
_OMITTEXTLOG: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (debug) {
|
||||||
|
jobArguments["_OMITTEXTLOG"] = false;
|
||||||
|
jobArguments["_OMITSESSIONRESULTS"] = false;
|
||||||
|
jobArguments["_DEBUG"] = 131;
|
||||||
}
|
}
|
||||||
const contexts = await this.request<{ items: Context[] }>(
|
|
||||||
`${this.serverUrl}/compute/contexts`,
|
const fileName = `exec-${
|
||||||
|
jobName.includes("/") ? jobName.split("/")[1] : jobName
|
||||||
|
}`;
|
||||||
|
|
||||||
|
let jobVariables: any = {
|
||||||
|
SYS_JES_JOB_URI: "",
|
||||||
|
_program: this.rootFolderName + "/" + jobName,
|
||||||
|
};
|
||||||
|
|
||||||
|
let files: any[] = [];
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
if (JSON.stringify(data).includes(";")) {
|
||||||
|
files = await this.uploadTables(data, accessToken);
|
||||||
|
jobVariables["_webin_file_count"] = files.length;
|
||||||
|
files.forEach((fileInfo, index) => {
|
||||||
|
jobVariables[
|
||||||
|
`_webin_fileuri${index + 1}`
|
||||||
|
] = `/files/files/${fileInfo.file.id}`;
|
||||||
|
jobVariables[`_webin_name${index + 1}`] = fileInfo.tableName;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
jobVariables = { ...jobVariables, ...formatDataForRequest(data) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute job in session
|
||||||
|
const postJobRequest = {
|
||||||
|
method: "POST",
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: fileName,
|
||||||
|
description: "Powered by SASjs",
|
||||||
|
code: linesOfCode,
|
||||||
|
variables: jobVariables,
|
||||||
|
arguments: jobArguments,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const { result: postedJob, etag } = await this.request<Job>(
|
||||||
|
`${this.serverUrl}/compute/sessions/${executionSessionId}/jobs`,
|
||||||
|
postJobRequest
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!silent) {
|
||||||
|
console.log(`Job has been submitted for ${fileName}`);
|
||||||
|
console.log(
|
||||||
|
`You can monitor the job progress at ${this.serverUrl}${
|
||||||
|
postedJob.links.find((l: any) => l.rel === "state")!.href
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const jobStatus = await this.pollJobState(
|
||||||
|
postedJob,
|
||||||
|
etag,
|
||||||
|
accessToken,
|
||||||
|
silent
|
||||||
|
);
|
||||||
|
|
||||||
|
const { result: currentJob } = await this.request<Job>(
|
||||||
|
`${this.serverUrl}/compute/sessions/${executionSessionId}/jobs/${postedJob.id}`,
|
||||||
{ headers }
|
{ headers }
|
||||||
);
|
);
|
||||||
const executionContext =
|
|
||||||
contexts.items && contexts.items.length
|
|
||||||
? contexts.items.find((c: any) => c.name === contextName)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (executionContext) {
|
let jobResult, log;
|
||||||
// Request new session in context or use the ID passed in
|
|
||||||
let executionSessionId: string;
|
|
||||||
if (sessionId) {
|
|
||||||
executionSessionId = sessionId;
|
|
||||||
} else {
|
|
||||||
const createSessionRequest = {
|
|
||||||
method: "POST",
|
|
||||||
headers,
|
|
||||||
};
|
|
||||||
const createdSession = await this.request<Session>(
|
|
||||||
`${this.serverUrl}/compute/contexts/${executionContext.id}/sessions`,
|
|
||||||
createSessionRequest
|
|
||||||
);
|
|
||||||
|
|
||||||
executionSessionId = createdSession.id;
|
if (jobStatus === "failed" || jobStatus === "error") {
|
||||||
}
|
return Promise.reject(currentJob.error);
|
||||||
// Execute job in session
|
|
||||||
const postJobRequest = {
|
|
||||||
method: "POST",
|
|
||||||
headers,
|
|
||||||
body: JSON.stringify({
|
|
||||||
name: fileName,
|
|
||||||
description: "Powered by SASjs",
|
|
||||||
code: linesOfCode,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
const postedJob = await this.request<Job>(
|
|
||||||
`${this.serverUrl}/compute/sessions/${executionSessionId}/jobs`,
|
|
||||||
postJobRequest
|
|
||||||
);
|
|
||||||
if (!silent) {
|
|
||||||
console.log(`Job has been submitted for ${fileName}`);
|
|
||||||
console.log(
|
|
||||||
`You can monitor the job progress at ${this.serverUrl}${
|
|
||||||
postedJob.links.find((l: any) => l.rel === "state")!.href
|
|
||||||
}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const jobStatus = await this.pollJobState(postedJob, accessToken, silent);
|
|
||||||
const logLink = postedJob.links.find((l: any) => l.rel === "log");
|
|
||||||
if (logLink) {
|
|
||||||
const log = await this.request(
|
|
||||||
`${this.serverUrl}${logLink.href}?limit=100000`,
|
|
||||||
{
|
|
||||||
headers,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return { jobStatus, log };
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.error(
|
|
||||||
`Unable to find execution context ${contextName}.\nPlease check the contextName in the tgtDeployVars and try again.`
|
|
||||||
);
|
|
||||||
console.error("Response from server: ", JSON.stringify(contexts));
|
|
||||||
}
|
}
|
||||||
|
const resultLink = `/compute/sessions/${executionSessionId}/filerefs/_webout/content`;
|
||||||
|
const logLink = currentJob.links.find((l) => l.rel === "log");
|
||||||
|
|
||||||
|
if (resultLink) {
|
||||||
|
jobResult = await this.request<any>(
|
||||||
|
`${this.serverUrl}${resultLink}`,
|
||||||
|
{ headers },
|
||||||
|
"text"
|
||||||
|
).catch((e) => ({
|
||||||
|
result: JSON.stringify(e),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (true && 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"));
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.sessionManager.clearSession(executionSessionId, accessToken);
|
||||||
|
|
||||||
|
return { result: jobResult?.result, log };
|
||||||
|
// } else {
|
||||||
|
// console.error(
|
||||||
|
// `Unable to find execution context ${contextName}.\nPlease check the contextName in the tgtDeployVars and try again.`
|
||||||
|
// );
|
||||||
|
// console.error("Response from server: ", JSON.stringify(this.contexts));
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a folder in the specified location. Either parentFolderPath or
|
* Creates a folder in the specified location. Either parentFolderPath or
|
||||||
* parentFolderUri must be provided.
|
* parentFolderUri must be provided.
|
||||||
* @param folderName - the name of the new folder.
|
* @param folderName - the name of the new folder.
|
||||||
* @param parentFolderPath - the full path to the parent folder. If not
|
* @param parentFolderPath - the full path to the parent folder. If not
|
||||||
* provided, the parentFolderUri must be provided.
|
* provided, the parentFolderUri must be provided.
|
||||||
* @param parentFolderUri - the URI (eg /folders/folders/UUID) of the parent
|
* @param parentFolderUri - the URI (eg /folders/folders/UUID) of the parent
|
||||||
* folder. If not provided, the parentFolderPath must be provided.
|
* folder. If not provided, the parentFolderPath must be provided.
|
||||||
*/
|
*/
|
||||||
public async createFolder(
|
public async createFolder(
|
||||||
@@ -296,17 +360,27 @@ 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) {
|
||||||
console.log(`Parent folder is not present: ${parentFolderPath}`);
|
console.log(`Parent folder is not present: ${parentFolderPath}`);
|
||||||
|
|
||||||
const newParentFolderPath = parentFolderPath.substring(0, parentFolderPath.lastIndexOf("/"));
|
const newParentFolderPath = parentFolderPath.substring(
|
||||||
|
0,
|
||||||
|
parentFolderPath.lastIndexOf("/")
|
||||||
|
);
|
||||||
const newFolderName = `${parentFolderPath.split("/").pop()}`;
|
const newFolderName = `${parentFolderPath.split("/").pop()}`;
|
||||||
if (newParentFolderPath === ""){
|
if (newParentFolderPath === "") {
|
||||||
throw new Error("Root Folder should have been present on server");
|
throw new Error("Root Folder should have been present on server");
|
||||||
}
|
}
|
||||||
console.log(`Creating Parent Folder:\n${newFolderName} in ${newParentFolderPath}`)
|
console.log(
|
||||||
const parentFolder = await this.createFolder(newFolderName, newParentFolderPath, undefined, accessToken)
|
`Creating Parent Folder:\n${newFolderName} in ${newParentFolderPath}`
|
||||||
console.log(`Parent Folder "${newFolderName}" successfully created.`)
|
);
|
||||||
|
const parentFolder = await this.createFolder(
|
||||||
|
newFolderName,
|
||||||
|
newParentFolderPath,
|
||||||
|
undefined,
|
||||||
|
accessToken
|
||||||
|
);
|
||||||
|
console.log(`Parent Folder "${newFolderName}" successfully created.`);
|
||||||
parentFolderUri = `/folders/folders/${parentFolder.id}`;
|
parentFolderUri = `/folders/folders/${parentFolder.id}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -324,7 +398,7 @@ export class SASViyaApiClient {
|
|||||||
createFolderRequest.headers.Authorization = `Bearer ${accessToken}`;
|
createFolderRequest.headers.Authorization = `Bearer ${accessToken}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const createFolderResponse = await this.request<Folder>(
|
const { result: createFolderResponse } = await this.request<Folder>(
|
||||||
`${this.serverUrl}/folders/folders?parentFolderUri=${parentFolderUri}`,
|
`${this.serverUrl}/folders/folders?parentFolderUri=${parentFolderUri}`,
|
||||||
createFolderRequest
|
createFolderRequest
|
||||||
);
|
);
|
||||||
@@ -350,7 +424,9 @@ export class SASViyaApiClient {
|
|||||||
accessToken?: string
|
accessToken?: string
|
||||||
) {
|
) {
|
||||||
if (!parentFolderPath && !parentFolderUri) {
|
if (!parentFolderPath && !parentFolderUri) {
|
||||||
throw new Error('Either parentFolderPath or parentFolderUri must be provided');
|
throw new Error(
|
||||||
|
"Either parentFolderPath or parentFolderUri must be provided"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!parentFolderUri && parentFolderPath) {
|
if (!parentFolderUri && parentFolderPath) {
|
||||||
@@ -365,12 +441,12 @@ export class SASViyaApiClient {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
name: jobName,
|
name: jobName,
|
||||||
parameters:[
|
parameters: [
|
||||||
{
|
{
|
||||||
"name":"_addjesbeginendmacros",
|
name: "_addjesbeginendmacros",
|
||||||
"type":"CHARACTER",
|
type: "CHARACTER",
|
||||||
"defaultValue":"false"
|
defaultValue: "false",
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
type: "Compute",
|
type: "Compute",
|
||||||
code,
|
code,
|
||||||
@@ -544,6 +620,74 @@ export class SASViyaApiClient {
|
|||||||
return deleteResponse;
|
return deleteResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a job via the SAS Viya Compute API
|
||||||
|
* @param sasJob - the relative path to the job.
|
||||||
|
* @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 data - any data to be passed in as input to the job.
|
||||||
|
* @param accessToken - an optional access token for an authorized user.
|
||||||
|
*/
|
||||||
|
public async executeComputeJob(
|
||||||
|
sasJob: string,
|
||||||
|
contextName: string,
|
||||||
|
debug: boolean,
|
||||||
|
data?: any,
|
||||||
|
accessToken?: string
|
||||||
|
) {
|
||||||
|
if (!this.rootFolder) {
|
||||||
|
await this.populateRootFolder(accessToken);
|
||||||
|
}
|
||||||
|
if (!this.rootFolder) {
|
||||||
|
console.error("Root folder was not found");
|
||||||
|
throw new Error("Root folder was not found");
|
||||||
|
}
|
||||||
|
if (!this.rootFolderMap.size) {
|
||||||
|
await this.populateRootFolderMap(accessToken);
|
||||||
|
}
|
||||||
|
if (!this.rootFolderMap.size) {
|
||||||
|
console.error(
|
||||||
|
`The job ${sasJob} was not found in ${this.rootFolderName}`
|
||||||
|
);
|
||||||
|
throw new Error(
|
||||||
|
`The job ${sasJob} was not found in ${this.rootFolderName}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers: any = { "Content-Type": "application/json" };
|
||||||
|
if (!!accessToken) {
|
||||||
|
headers.Authorization = `Bearer ${accessToken}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const folderName = sasJob.split("/")[0];
|
||||||
|
const jobName = sasJob.split("/")[1];
|
||||||
|
const jobFolder = this.rootFolderMap.get(folderName);
|
||||||
|
const jobToExecute = jobFolder?.find((item) => item.name === jobName);
|
||||||
|
const jobDefinitionLink = jobToExecute?.links.find(
|
||||||
|
(l) => l.rel === "getResource"
|
||||||
|
);
|
||||||
|
if (!jobDefinitionLink) {
|
||||||
|
console.error("Job definition URI was not found.");
|
||||||
|
throw new Error("Job definition URI was not found.");
|
||||||
|
}
|
||||||
|
const { result: jobDefinition } = await this.request<JobDefinition>(
|
||||||
|
`${this.serverUrl}${jobDefinitionLink.href}`,
|
||||||
|
headers
|
||||||
|
);
|
||||||
|
const linesToExecute = jobDefinition.code
|
||||||
|
.replace(/\r\n/g, "\n")
|
||||||
|
.split("\n");
|
||||||
|
return await this.executeScript(
|
||||||
|
sasJob,
|
||||||
|
linesToExecute,
|
||||||
|
contextName,
|
||||||
|
accessToken,
|
||||||
|
true,
|
||||||
|
data,
|
||||||
|
debug
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 path to the job.
|
||||||
@@ -574,11 +718,12 @@ export class SASViyaApiClient {
|
|||||||
`The job ${sasJob} was not found in ${this.rootFolderName}`
|
`The job ${sasJob} was not found in ${this.rootFolderName}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let files: any[] = [];
|
let files: any[] = [];
|
||||||
if (data && Object.keys(data).length) {
|
if (data && Object.keys(data).length) {
|
||||||
files = await this.uploadTables(data, accessToken);
|
files = await this.uploadTables(data, accessToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
const jobName = path.basename(sasJob);
|
const jobName = path.basename(sasJob);
|
||||||
const jobFolder = sasJob.replace(`/${jobName}`, "");
|
const jobFolder = sasJob.replace(`/${jobName}`, "");
|
||||||
const allJobsInFolder = this.rootFolderMap.get(jobFolder.replace("/", ""));
|
const allJobsInFolder = this.rootFolderMap.get(jobFolder.replace("/", ""));
|
||||||
@@ -595,7 +740,7 @@ export class SASViyaApiClient {
|
|||||||
headers.Authorization = `Bearer ${accessToken}`;
|
headers.Authorization = `Bearer ${accessToken}`;
|
||||||
}
|
}
|
||||||
requestInfo.headers = headers;
|
requestInfo.headers = headers;
|
||||||
const jobDefinition = await this.request<Job>(
|
const { result: jobDefinition } = await this.request<Job>(
|
||||||
`${this.serverUrl}${jobDefinitionLink}`,
|
`${this.serverUrl}${jobDefinitionLink}`,
|
||||||
requestInfo
|
requestInfo
|
||||||
);
|
);
|
||||||
@@ -612,15 +757,15 @@ export class SASViyaApiClient {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (debug) {
|
if (debug) {
|
||||||
jobArguments["_omittextlog"] = "false";
|
jobArguments["_OMITTEXTLOG"] = "false";
|
||||||
jobArguments["_omitsessionresults"] = "false";
|
jobArguments["_OMITSESSIONRESULTS"] = "false";
|
||||||
jobArguments["_debug"] = 131;
|
jobArguments["_DEBUG"] = 131;
|
||||||
}
|
}
|
||||||
|
|
||||||
files.forEach((fileInfo, index) => {
|
files.forEach((fileInfo, index) => {
|
||||||
jobArguments[
|
jobArguments[
|
||||||
`_webin_fileuri${index + 1}`
|
`_webin_fileuri${index + 1}`
|
||||||
] = `/files/files/${fileInfo.id}`;
|
] = `/files/files/${fileInfo.file.id}`;
|
||||||
jobArguments[`_webin_name${index + 1}`] = fileInfo.tableName;
|
jobArguments[`_webin_name${index + 1}`] = fileInfo.tableName;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -634,25 +779,45 @@ export class SASViyaApiClient {
|
|||||||
arguments: jobArguments,
|
arguments: jobArguments,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
const postedJob = await this.request<Job>(
|
const { result: postedJob, etag } = await this.request<Job>(
|
||||||
`${this.serverUrl}/jobExecution/jobs?_action=wait`,
|
`${this.serverUrl}/jobExecution/jobs?_action=wait`,
|
||||||
postJobRequest
|
postJobRequest
|
||||||
);
|
);
|
||||||
const jobStatus = await this.pollJobState(postedJob, accessToken, true);
|
const jobStatus = await this.pollJobState(
|
||||||
const currentJob = await this.request<Job>(
|
postedJob,
|
||||||
|
etag,
|
||||||
|
accessToken,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
const { result: currentJob } = await this.request<Job>(
|
||||||
`${this.serverUrl}/jobExecution/jobs/${postedJob.id}`,
|
`${this.serverUrl}/jobExecution/jobs/${postedJob.id}`,
|
||||||
{ headers }
|
{ headers }
|
||||||
);
|
);
|
||||||
const resultLink = currentJob.results["_webout.json"];
|
|
||||||
if (resultLink) {
|
|
||||||
const result = await this.request<any>(
|
|
||||||
`${this.serverUrl}${resultLink}/content`,
|
|
||||||
{ headers }
|
|
||||||
);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
return postedJob;
|
let jobResult, log;
|
||||||
|
if (jobStatus === "failed") {
|
||||||
|
return Promise.reject(currentJob.error);
|
||||||
|
}
|
||||||
|
const resultLink = currentJob.results["_webout.json"];
|
||||||
|
const logLink = currentJob.links.find((l) => l.rel === "log");
|
||||||
|
if (resultLink) {
|
||||||
|
jobResult = await this.request<any>(
|
||||||
|
`${this.serverUrl}${resultLink}/content`,
|
||||||
|
{ headers },
|
||||||
|
"text"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (debug && logLink) {
|
||||||
|
log = await this.request<any>(
|
||||||
|
`${this.serverUrl}${logLink.href}/content`,
|
||||||
|
{
|
||||||
|
headers,
|
||||||
|
}
|
||||||
|
).then((res: any) =>
|
||||||
|
res.result.items.map((i: any) => i.line).join("\n")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return { result: jobResult?.result, log };
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`The job ${sasJob} was not found at the location ${this.rootFolderName}`
|
`The job ${sasJob} was not found at the location ${this.rootFolderName}`
|
||||||
@@ -669,14 +834,14 @@ export class SASViyaApiClient {
|
|||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
requestInfo.headers = { Authorization: `Bearer ${accessToken}` };
|
requestInfo.headers = { Authorization: `Bearer ${accessToken}` };
|
||||||
}
|
}
|
||||||
const folder = await this.request<Folder>(
|
const { result: folder } = await this.request<Folder>(
|
||||||
`${this.serverUrl}${url}`,
|
`${this.serverUrl}${url}`,
|
||||||
requestInfo
|
requestInfo
|
||||||
);
|
);
|
||||||
if (!folder){
|
if (!folder) {
|
||||||
throw new Error("Cannot populate RootFolderMap unless rootFolder exists");
|
throw new Error("Cannot populate RootFolderMap unless rootFolder exists");
|
||||||
}
|
}
|
||||||
const 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`,
|
||||||
requestInfo
|
requestInfo
|
||||||
);
|
);
|
||||||
@@ -691,7 +856,7 @@ export class SASViyaApiClient {
|
|||||||
this.rootFolderName +
|
this.rootFolderName +
|
||||||
"/" +
|
"/" +
|
||||||
member.name;
|
member.name;
|
||||||
const memberDetail = await this.request<Folder>(
|
const { result: memberDetail } = await this.request<Folder>(
|
||||||
`${this.serverUrl}${subFolderUrl}`,
|
`${this.serverUrl}${subFolderUrl}`,
|
||||||
requestInfo
|
requestInfo
|
||||||
);
|
);
|
||||||
@@ -700,7 +865,7 @@ export class SASViyaApiClient {
|
|||||||
(l: any) => l.rel === "members"
|
(l: any) => l.rel === "members"
|
||||||
);
|
);
|
||||||
|
|
||||||
const memberContents = await this.request<{ items: any[] }>(
|
const { result: memberContents } = await this.request<{ items: any[] }>(
|
||||||
`${this.serverUrl}${membersLink!.href}`,
|
`${this.serverUrl}${membersLink!.href}`,
|
||||||
requestInfo
|
requestInfo
|
||||||
);
|
);
|
||||||
@@ -721,29 +886,37 @@ export class SASViyaApiClient {
|
|||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
requestInfo.headers = { Authorization: `Bearer ${accessToken}` };
|
requestInfo.headers = { Authorization: `Bearer ${accessToken}` };
|
||||||
}
|
}
|
||||||
|
let error;
|
||||||
const rootFolder = await this.request<Folder>(
|
const rootFolder = await this.request<Folder>(
|
||||||
`${this.serverUrl}${url}`,
|
`${this.serverUrl}${url}`,
|
||||||
requestInfo
|
requestInfo
|
||||||
).catch(() => null);
|
);
|
||||||
|
|
||||||
this.rootFolder = rootFolder;
|
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,
|
||||||
accessToken?: string,
|
accessToken?: string,
|
||||||
silent = false
|
silent = false
|
||||||
) {
|
) {
|
||||||
|
const MAX_POLL_COUNT = 1000;
|
||||||
|
const POLL_INTERVAL = 100;
|
||||||
let postedJobState = "";
|
let postedJobState = "";
|
||||||
let pollCount = 0;
|
let pollCount = 0;
|
||||||
const headers: any = {
|
const headers: any = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
"If-None-Match": etag,
|
||||||
};
|
};
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
headers.Authorization = `Bearer ${accessToken}`;
|
headers.Authorization = `Bearer ${accessToken}`;
|
||||||
}
|
}
|
||||||
const stateLink = postedJob.links.find((l: any) => l.rel === "state");
|
const stateLink = postedJob.links.find((l: any) => l.rel === "state");
|
||||||
return new Promise((resolve, _) => {
|
return new Promise(async (resolve, _) => {
|
||||||
const interval = setInterval(async () => {
|
const interval = setInterval(async () => {
|
||||||
if (
|
if (
|
||||||
postedJobState === "running" ||
|
postedJobState === "running" ||
|
||||||
@@ -754,8 +927,8 @@ export class SASViyaApiClient {
|
|||||||
if (!silent) {
|
if (!silent) {
|
||||||
console.log("Polling job status... \n");
|
console.log("Polling job status... \n");
|
||||||
}
|
}
|
||||||
const jobState = await this.request<string>(
|
const { result: jobState } = await this.request<string>(
|
||||||
`${this.serverUrl}${stateLink.href}?wait=30`,
|
`${this.serverUrl}${stateLink.href}?_action=wait&wait=30`,
|
||||||
{
|
{
|
||||||
headers,
|
headers,
|
||||||
},
|
},
|
||||||
@@ -767,7 +940,7 @@ export class SASViyaApiClient {
|
|||||||
console.log(`Current state: ${postedJobState}\n`);
|
console.log(`Current state: ${postedJobState}\n`);
|
||||||
}
|
}
|
||||||
pollCount++;
|
pollCount++;
|
||||||
if (pollCount >= 100) {
|
if (pollCount >= MAX_POLL_COUNT) {
|
||||||
resolve(postedJobState);
|
resolve(postedJobState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -775,7 +948,50 @@ export class SASViyaApiClient {
|
|||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
resolve(postedJobState);
|
resolve(postedJobState);
|
||||||
}
|
}
|
||||||
}, 100);
|
}, POLL_INTERVAL);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -802,33 +1018,40 @@ export class SASViyaApiClient {
|
|||||||
headers,
|
headers,
|
||||||
};
|
};
|
||||||
|
|
||||||
const file = await this.request<any>(
|
const uploadResponse = await this.request<any>(
|
||||||
`${this.serverUrl}/files/files#rawUpload`,
|
`${this.serverUrl}/files/files#rawUpload`,
|
||||||
createFileRequest
|
createFileRequest
|
||||||
);
|
);
|
||||||
|
|
||||||
uploadedFiles.push({ tableName, file });
|
uploadedFiles.push({ tableName, file: uploadResponse.result });
|
||||||
}
|
}
|
||||||
return uploadedFiles;
|
return uploadedFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getFolderUri(folderPath: string, accessToken?: string) {
|
private async getFolderUri(folderPath: string, accessToken?: string) {
|
||||||
const url = "/folders/folders/@item?path=" + folderPath;
|
const url = "/folders/folders/@item?path=" + folderPath;
|
||||||
const requestInfo: any = {
|
const requestInfo: any = {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
};
|
};
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
requestInfo.headers = { Authorization: `Bearer ${accessToken}` };
|
requestInfo.headers = { Authorization: `Bearer ${accessToken}` };
|
||||||
}
|
}
|
||||||
const folder = await this.request<Folder>(
|
const { result: folder } = await this.request<Folder>(
|
||||||
`${this.serverUrl}${url}`,
|
`${this.serverUrl}${url}`,
|
||||||
requestInfo
|
requestInfo
|
||||||
);
|
).catch((err) => {
|
||||||
if (!folder)
|
return { result: null };
|
||||||
return undefined;
|
});
|
||||||
return `/folders/folders/${folder.id}`;
|
|
||||||
|
if (!folder) return undefined;
|
||||||
|
return `/folders/folders/${folder.id}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setCsrfTokenLocal = (csrfToken: CsrfToken) => {
|
||||||
|
this.csrfToken = csrfToken;
|
||||||
|
this.setCsrfToken(csrfToken);
|
||||||
|
};
|
||||||
|
|
||||||
private async request<T>(
|
private async request<T>(
|
||||||
url: string,
|
url: string,
|
||||||
options: RequestInit,
|
options: RequestInit,
|
||||||
@@ -843,7 +1066,7 @@ export class SASViyaApiClient {
|
|||||||
return await makeRequest<T>(
|
return await makeRequest<T>(
|
||||||
url,
|
url,
|
||||||
options,
|
options,
|
||||||
(csrfToken) => (this.csrfToken = csrfToken),
|
this.setCsrfTokenLocal,
|
||||||
contentType
|
contentType
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
571
src/SASjs.ts
571
src/SASjs.ts
@@ -20,9 +20,12 @@ import {
|
|||||||
SASjsRequest,
|
SASjsRequest,
|
||||||
SASjsWaitingRequest,
|
SASjsWaitingRequest,
|
||||||
ServerType,
|
ServerType,
|
||||||
|
CsrfToken,
|
||||||
|
UploadFile,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { SASViyaApiClient } from "./SASViyaApiClient";
|
import { SASViyaApiClient } from "./SASViyaApiClient";
|
||||||
import { SAS9ApiClient } from "./SAS9ApiClient";
|
import { SAS9ApiClient } from "./SAS9ApiClient";
|
||||||
|
import { FileUploader } from "./FileUploader";
|
||||||
|
|
||||||
const defaultConfig: SASjsConfig = {
|
const defaultConfig: SASjsConfig = {
|
||||||
serverUrl: "",
|
serverUrl: "",
|
||||||
@@ -32,6 +35,7 @@ const defaultConfig: SASjsConfig = {
|
|||||||
serverType: ServerType.SASViya,
|
serverType: ServerType.SASViya,
|
||||||
debug: true,
|
debug: true,
|
||||||
contextName: "SAS Job Execution compute context",
|
contextName: "SAS Job Execution compute context",
|
||||||
|
useComputeApi: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const requestRetryLimit = 5;
|
const requestRetryLimit = 5;
|
||||||
@@ -41,18 +45,21 @@ const requestRetryLimit = 5;
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export default class SASjs {
|
export default class SASjs {
|
||||||
private sasjsConfig = new SASjsConfig();
|
private sasjsConfig: SASjsConfig = new SASjsConfig();
|
||||||
private jobsPath: string = "";
|
private jobsPath: string = "";
|
||||||
private logoutUrl: string = "";
|
private logoutUrl: string = "";
|
||||||
private loginUrl: string = "";
|
private loginUrl: string = "";
|
||||||
private _csrf: string | null = null;
|
private csrfTokenApi: CsrfToken | null = null;
|
||||||
private _csrfHeader: string | null = null;
|
private csrfTokenWeb: CsrfToken | null = null;
|
||||||
private retryCount: number = 0;
|
private retryCountWeb: number = 0;
|
||||||
|
private retryCountComputeApi: number = 0;
|
||||||
|
private retryCountJeseApi: number = 0;
|
||||||
private sasjsRequests: SASjsRequest[] = [];
|
private sasjsRequests: SASjsRequest[] = [];
|
||||||
private sasjsWaitingRequests: SASjsWaitingRequest[] = [];
|
private sasjsWaitingRequests: SASjsWaitingRequest[] = [];
|
||||||
private userName: string = "";
|
private userName: string = "";
|
||||||
private sasViyaApiClient: SASViyaApiClient | null = null;
|
private sasViyaApiClient: SASViyaApiClient | null = null;
|
||||||
private sas9ApiClient: SAS9ApiClient | null = null;
|
private sas9ApiClient: SAS9ApiClient | null = null;
|
||||||
|
private fileUploader: FileUploader | null = null;
|
||||||
|
|
||||||
constructor(config?: any) {
|
constructor(config?: any) {
|
||||||
this.sasjsConfig = {
|
this.sasjsConfig = {
|
||||||
@@ -115,7 +122,6 @@ export default class SASjs {
|
|||||||
linesOfCode,
|
linesOfCode,
|
||||||
contextName,
|
contextName,
|
||||||
accessToken,
|
accessToken,
|
||||||
sessionId,
|
|
||||||
silent
|
silent
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -234,11 +240,19 @@ export default class SASjs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the _csrf token of the current session.
|
* Returns the _csrf token of the current session for the API approach
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
public getCsrf() {
|
public getCsrfApi() {
|
||||||
return this._csrf;
|
return this.csrfTokenApi?.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the _csrf token of the current session for the WEB approach.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public getCsrfWeb() {
|
||||||
|
return this.csrfTokenWeb?.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -362,6 +376,26 @@ export default class SASjs {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads a file to the given service
|
||||||
|
* @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 file - Array of files to be uploaded, including File object and file name.
|
||||||
|
* @param params - Request URL paramaters
|
||||||
|
*/
|
||||||
|
public uploadFile(sasJob: string, files: UploadFile[], params: any) {
|
||||||
|
const fileUploader =
|
||||||
|
this.fileUploader ||
|
||||||
|
new FileUploader(
|
||||||
|
this.sasjsConfig.appLoc,
|
||||||
|
this.sasjsConfig.serverUrl,
|
||||||
|
this.jobsPath,
|
||||||
|
this.csrfTokenWeb
|
||||||
|
);
|
||||||
|
return fileUploader.uploadFile(sasJob, files, params);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Makes a request to the SAS Service specified in `SASjob`. The response
|
* Makes a request to the SAS Service specified in `SASjob`. The response
|
||||||
* object will always contain table names in lowercase, and column names in
|
* object will always contain table names in lowercase, and column names in
|
||||||
@@ -373,8 +407,9 @@ export default class SASjs {
|
|||||||
* Process.) Is prepended at runtime with the value of `appLoc`.
|
* Process.) Is prepended at runtime with the value of `appLoc`.
|
||||||
* @param data - A JSON object containing one or more tables to be sent to
|
* @param data - A JSON object containing one or more tables to be sent to
|
||||||
* SAS. Can be `null` if no inputs required.
|
* SAS. Can be `null` if no inputs required.
|
||||||
* @param params - Provide any changes to the config here, for instance to
|
* @param config - Provide any changes to the config here, for instance to
|
||||||
* enable / disable `debug`.
|
* 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 - provide a function here to be called if the
|
||||||
* user is not logged in (eg to display a login form). The request will be
|
* user is not logged in (eg to display a login form). The request will be
|
||||||
* resubmitted after logon.
|
* resubmitted after logon.
|
||||||
@@ -382,7 +417,118 @@ export default class SASjs {
|
|||||||
public async request(
|
public async request(
|
||||||
sasJob: string,
|
sasJob: string,
|
||||||
data: any,
|
data: any,
|
||||||
params?: any,
|
config: any = {},
|
||||||
|
loginRequiredCallback?: any,
|
||||||
|
accessToken?: string
|
||||||
|
) {
|
||||||
|
let requestResponse;
|
||||||
|
|
||||||
|
config = {
|
||||||
|
...this.sasjsConfig,
|
||||||
|
...config,
|
||||||
|
};
|
||||||
|
|
||||||
|
sasJob = sasJob.startsWith("/") ? sasJob.replace("/", "") : sasJob;
|
||||||
|
|
||||||
|
if (config.serverType === ServerType.SASViya && config.contextName) {
|
||||||
|
if (config.useComputeApi) {
|
||||||
|
requestResponse = await this.executeJobViaComputeApi(
|
||||||
|
sasJob,
|
||||||
|
data,
|
||||||
|
config,
|
||||||
|
loginRequiredCallback,
|
||||||
|
accessToken
|
||||||
|
);
|
||||||
|
|
||||||
|
this.retryCountComputeApi = 0;
|
||||||
|
} else {
|
||||||
|
requestResponse = await this.executeJobViaJesApi(
|
||||||
|
sasJob,
|
||||||
|
data,
|
||||||
|
config,
|
||||||
|
loginRequiredCallback,
|
||||||
|
accessToken
|
||||||
|
);
|
||||||
|
|
||||||
|
this.retryCountJeseApi = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
requestResponse = await this.executeJobViaWeb(
|
||||||
|
sasJob,
|
||||||
|
data,
|
||||||
|
config,
|
||||||
|
loginRequiredCallback
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the folders and services in the provided JSON on the given location
|
||||||
|
* (appLoc) on the given server (serverUrl).
|
||||||
|
* @param serviceJson - the JSON specifying the folders and services to be created.
|
||||||
|
* @param appLoc - the base folder in which to create the new folders and
|
||||||
|
* services. If not provided, is taken from SASjsConfig.
|
||||||
|
* @param serverUrl - the server on which to deploy the folders and services.
|
||||||
|
* If not provided, is taken from SASjsConfig.
|
||||||
|
* @param accessToken - an optional access token to be passed in when
|
||||||
|
* using this function from the command line.
|
||||||
|
*/
|
||||||
|
public async deployServicePack(
|
||||||
|
serviceJson: any,
|
||||||
|
appLoc?: string,
|
||||||
|
serverUrl?: string,
|
||||||
|
accessToken?: string
|
||||||
|
) {
|
||||||
|
if (this.sasjsConfig.serverType !== ServerType.SASViya) {
|
||||||
|
throw new Error("This operation is only supported on SAS Viya servers.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let sasApiClient: any = null;
|
||||||
|
if (serverUrl || appLoc) {
|
||||||
|
if (!serverUrl) {
|
||||||
|
serverUrl = this.sasjsConfig.serverUrl;
|
||||||
|
}
|
||||||
|
if (!appLoc) {
|
||||||
|
appLoc = this.sasjsConfig.appLoc;
|
||||||
|
}
|
||||||
|
if (this.sasjsConfig.serverType === ServerType.SASViya) {
|
||||||
|
sasApiClient = new SASViyaApiClient(
|
||||||
|
serverUrl,
|
||||||
|
appLoc,
|
||||||
|
this.sasjsConfig.contextName,
|
||||||
|
this.setCsrfTokenApi
|
||||||
|
);
|
||||||
|
} else if (this.sasjsConfig.serverType === ServerType.SAS9) {
|
||||||
|
sasApiClient = new SAS9ApiClient(serverUrl);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let sasClientConfig: any = null;
|
||||||
|
if (this.sasjsConfig.serverType === ServerType.SASViya) {
|
||||||
|
sasClientConfig = this.sasViyaApiClient!.getConfig();
|
||||||
|
} else if (this.sasjsConfig.serverType === ServerType.SAS9) {
|
||||||
|
sasClientConfig = this.sas9ApiClient!.getConfig();
|
||||||
|
}
|
||||||
|
serverUrl = sasClientConfig.serverUrl;
|
||||||
|
appLoc = sasClientConfig.rootFolderName as string;
|
||||||
|
}
|
||||||
|
const members =
|
||||||
|
serviceJson.members[0].name === "services"
|
||||||
|
? serviceJson.members[0].members
|
||||||
|
: serviceJson.members;
|
||||||
|
await this.createFoldersAndServices(
|
||||||
|
appLoc,
|
||||||
|
members,
|
||||||
|
accessToken,
|
||||||
|
sasApiClient
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async executeJobViaComputeApi(
|
||||||
|
sasJob: string,
|
||||||
|
data: any,
|
||||||
|
config: any,
|
||||||
loginRequiredCallback?: any,
|
loginRequiredCallback?: any,
|
||||||
accessToken?: string
|
accessToken?: string
|
||||||
) {
|
) {
|
||||||
@@ -394,55 +540,164 @@ export default class SASjs {
|
|||||||
},
|
},
|
||||||
SASjob: sasJob,
|
SASjob: sasJob,
|
||||||
data,
|
data,
|
||||||
params,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// if (
|
sasjsWaitingRequest.requestPromise.promise = new Promise(
|
||||||
// this.sasjsConfig.serverType === ServerType.SASViya &&
|
async (resolve, reject) => {
|
||||||
// this.sasjsConfig.contextName
|
this.sasViyaApiClient
|
||||||
// ) {
|
?.executeComputeJob(
|
||||||
// sasjsWaitingRequest.requestPromise.promise = new Promise(
|
sasJob,
|
||||||
// async (resolve, reject) => {
|
config.contextName,
|
||||||
// const session = await this.checkSession();
|
config.debug,
|
||||||
|
data,
|
||||||
|
accessToken
|
||||||
|
)
|
||||||
|
.then((response) => {
|
||||||
|
if (!config.debug) {
|
||||||
|
this.appendSasjsRequest(null, sasJob, null);
|
||||||
|
} else {
|
||||||
|
this.appendSasjsRequest(response, sasJob, null);
|
||||||
|
}
|
||||||
|
|
||||||
// if (!session.isLoggedIn) {
|
resolve(JSON.parse(response!.result));
|
||||||
// if (loginRequiredCallback) loginRequiredCallback(true);
|
})
|
||||||
// logInRequired = true;
|
.catch(async (e) => {
|
||||||
// sasjsWaitingRequest.requestPromise.resolve = resolve;
|
if (needsRetry(JSON.stringify(e))) {
|
||||||
// sasjsWaitingRequest.requestPromise.reject = reject;
|
if (this.retryCountComputeApi < requestRetryLimit) {
|
||||||
// this.sasjsWaitingRequests.push(sasjsWaitingRequest);
|
let retryResponse = await this.executeJobViaComputeApi(
|
||||||
// } else {
|
sasJob,
|
||||||
// resolve(
|
data,
|
||||||
// await this.sasViyaApiClient?.executeJob(
|
config,
|
||||||
// sasJob,
|
loginRequiredCallback,
|
||||||
// this.sasjsConfig.contextName,
|
accessToken
|
||||||
// this.sasjsConfig.debug,
|
);
|
||||||
// data,
|
|
||||||
// accessToken
|
this.retryCountComputeApi++;
|
||||||
// )
|
|
||||||
// );
|
resolve(retryResponse);
|
||||||
// }
|
} else {
|
||||||
// }
|
this.retryCountComputeApi = 0;
|
||||||
// );
|
reject({ MESSAGE: "Compute API retry requests limit reached" });
|
||||||
// return sasjsWaitingRequest.requestPromise.promise;
|
}
|
||||||
// } else {
|
}
|
||||||
const program = this.sasjsConfig.appLoc
|
|
||||||
? this.sasjsConfig.appLoc.replace(/\/?$/, "/") + sasJob.replace(/^\//, "")
|
if (e && e.status === 401) {
|
||||||
|
if (loginRequiredCallback) loginRequiredCallback(true);
|
||||||
|
sasjsWaitingRequest.requestPromise.resolve = resolve;
|
||||||
|
sasjsWaitingRequest.requestPromise.reject = reject;
|
||||||
|
sasjsWaitingRequest.config = config;
|
||||||
|
this.sasjsWaitingRequests.push(sasjsWaitingRequest);
|
||||||
|
} else {
|
||||||
|
reject({ MESSAGE: e || "Job execution failed" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return sasjsWaitingRequest.requestPromise.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async executeJobViaJesApi(
|
||||||
|
sasJob: string,
|
||||||
|
data: any,
|
||||||
|
config: any,
|
||||||
|
loginRequiredCallback?: any,
|
||||||
|
accessToken?: string
|
||||||
|
) {
|
||||||
|
const sasjsWaitingRequest: SASjsWaitingRequest = {
|
||||||
|
requestPromise: {
|
||||||
|
promise: null,
|
||||||
|
resolve: null,
|
||||||
|
reject: null,
|
||||||
|
},
|
||||||
|
SASjob: sasJob,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
|
||||||
|
sasjsWaitingRequest.requestPromise.promise = new Promise(
|
||||||
|
async (resolve, reject) => {
|
||||||
|
const session = await this.checkSession();
|
||||||
|
|
||||||
|
if (!session.isLoggedIn) {
|
||||||
|
if (loginRequiredCallback) loginRequiredCallback(true);
|
||||||
|
sasjsWaitingRequest.requestPromise.resolve = resolve;
|
||||||
|
sasjsWaitingRequest.requestPromise.reject = reject;
|
||||||
|
sasjsWaitingRequest.config = config;
|
||||||
|
this.sasjsWaitingRequests.push(sasjsWaitingRequest);
|
||||||
|
} else {
|
||||||
|
resolve(
|
||||||
|
await this.sasViyaApiClient
|
||||||
|
?.executeJob(
|
||||||
|
sasJob,
|
||||||
|
config.contextName,
|
||||||
|
config.debug,
|
||||||
|
data,
|
||||||
|
accessToken
|
||||||
|
)
|
||||||
|
.then((response) => {
|
||||||
|
if (!config.debug) {
|
||||||
|
this.appendSasjsRequest(null, sasJob, null);
|
||||||
|
} else {
|
||||||
|
this.appendSasjsRequest(response, sasJob, null);
|
||||||
|
}
|
||||||
|
return JSON.parse(response!.result);
|
||||||
|
})
|
||||||
|
.catch(async (e) => {
|
||||||
|
if (needsRetry(JSON.stringify(e))) {
|
||||||
|
if (this.retryCountJeseApi < requestRetryLimit) {
|
||||||
|
let retryResponse = await this.executeJobViaJesApi(
|
||||||
|
sasJob,
|
||||||
|
data,
|
||||||
|
config,
|
||||||
|
loginRequiredCallback,
|
||||||
|
accessToken
|
||||||
|
);
|
||||||
|
|
||||||
|
this.retryCountJeseApi++;
|
||||||
|
|
||||||
|
resolve(retryResponse);
|
||||||
|
} else {
|
||||||
|
this.retryCountJeseApi = 0;
|
||||||
|
reject({ MESSAGE: "Jes API retry requests limit reached" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reject({ MESSAGE: (e && e.message) || "Job execution failed" });
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return sasjsWaitingRequest.requestPromise.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async executeJobViaWeb(
|
||||||
|
sasJob: string,
|
||||||
|
data: any,
|
||||||
|
config: any,
|
||||||
|
loginRequiredCallback?: any
|
||||||
|
) {
|
||||||
|
const sasjsWaitingRequest: SASjsWaitingRequest = {
|
||||||
|
requestPromise: {
|
||||||
|
promise: null,
|
||||||
|
resolve: null,
|
||||||
|
reject: null,
|
||||||
|
},
|
||||||
|
SASjob: sasJob,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
const program = config.appLoc
|
||||||
|
? config.appLoc.replace(/\/?$/, "/") + sasJob.replace(/^\//, "")
|
||||||
: sasJob;
|
: sasJob;
|
||||||
const jobUri =
|
const jobUri =
|
||||||
this.sasjsConfig.serverType === "SASVIYA"
|
config.serverType === "SASVIYA" ? await this.getJobUri(sasJob) : "";
|
||||||
? await this.getJobUri(sasJob)
|
const apiUrl = `${config.serverUrl}${this.jobsPath}/?${
|
||||||
: "";
|
|
||||||
const apiUrl = `${this.sasjsConfig.serverUrl}${this.jobsPath}/?${
|
|
||||||
jobUri.length > 0
|
jobUri.length > 0
|
||||||
? "__program=" + program + "&_job=" + jobUri
|
? "__program=" + program + "&_job=" + jobUri
|
||||||
: "_program=" + program
|
: "_program=" + program
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
const inputParams = params ? params : {};
|
|
||||||
const requestParams = {
|
const requestParams = {
|
||||||
...inputParams,
|
...this.getRequestParamsWeb(),
|
||||||
...this.getRequestParams(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
@@ -451,74 +706,68 @@ export default class SASjs {
|
|||||||
let errorMsg = "";
|
let errorMsg = "";
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
console.log("Input data", data);
|
|
||||||
const stringifiedData = JSON.stringify(data);
|
const stringifiedData = JSON.stringify(data);
|
||||||
if (
|
if (
|
||||||
this.sasjsConfig.serverType === ServerType.SAS9 ||
|
config.serverType === ServerType.SAS9 ||
|
||||||
stringifiedData.length > 500000 ||
|
stringifiedData.length > 500000 ||
|
||||||
stringifiedData.includes(";")
|
stringifiedData.includes(";")
|
||||||
) {
|
) {
|
||||||
// file upload approach
|
// file upload approach
|
||||||
for (const tableName in data) {
|
for (const tableName in data) {
|
||||||
console.log("TableName: ", tableName);
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const name = tableName;
|
const name = tableName;
|
||||||
const csv = convertToCSV(data[tableName]);
|
const csv = convertToCSV(data[tableName]);
|
||||||
console.log("Converted CSV", csv);
|
|
||||||
if (csv === "ERROR: LARGE STRING LENGTH") {
|
if (csv === "ERROR: LARGE STRING LENGTH") {
|
||||||
console.log("String too long");
|
|
||||||
isError = true;
|
isError = true;
|
||||||
errorMsg =
|
errorMsg =
|
||||||
"The max length of a string value in SASjs is 32765 characters.";
|
"The max length of a string value in SASjs is 32765 characters.";
|
||||||
}
|
}
|
||||||
|
|
||||||
const file = new Blob([csv], { type: "application/csv" });
|
const file = new Blob([csv], {
|
||||||
console.log("File", file);
|
type: "application/csv",
|
||||||
|
});
|
||||||
|
|
||||||
formData.append(name, file, `${name}.csv`);
|
formData.append(name, file, `${name}.csv`);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// param based approach
|
||||||
|
const sasjsTables = [];
|
||||||
|
let tableCounter = 0;
|
||||||
|
for (const tableName in data) {
|
||||||
|
if (isError) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tableCounter++;
|
||||||
|
sasjsTables.push(tableName);
|
||||||
|
const csv = convertToCSV(data[tableName]);
|
||||||
|
if (csv === "ERROR: LARGE STRING LENGTH") {
|
||||||
|
isError = true;
|
||||||
|
errorMsg =
|
||||||
|
"The max length of a string value in SASjs is 32765 characters.";
|
||||||
|
}
|
||||||
|
// if csv has length more then 16k, send in chunks
|
||||||
|
if (csv.length > 16000) {
|
||||||
|
const csvChunks = splitChunks(csv);
|
||||||
|
// append chunks to form data with same key
|
||||||
|
csvChunks.map((chunk) => {
|
||||||
|
formData.append(`sasjs${tableCounter}data`, chunk);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
requestParams[`sasjs${tableCounter}data`] = csv;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
requestParams["sasjs_tables"] = sasjsTables.join(" ");
|
||||||
}
|
}
|
||||||
// param based approach
|
|
||||||
const sasjsTables = [];
|
|
||||||
let tableCounter = 0;
|
|
||||||
for (const tableName in data) {
|
|
||||||
if (isError) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
tableCounter++;
|
|
||||||
sasjsTables.push(tableName);
|
|
||||||
const csv = convertToCSV(data[tableName]);
|
|
||||||
if (csv === "ERROR: LARGE STRING LENGTH") {
|
|
||||||
isError = true;
|
|
||||||
errorMsg =
|
|
||||||
"The max length of a string value in SASjs is 32765 characters.";
|
|
||||||
}
|
|
||||||
// if csv has length more then 16k, send in chunks
|
|
||||||
if (csv.length > 16000) {
|
|
||||||
const csvChunks = splitChunks(csv);
|
|
||||||
// append chunks to form data with same key
|
|
||||||
csvChunks.map((chunk) => {
|
|
||||||
formData.append(`sasjs${tableCounter}data`, chunk);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
requestParams[`sasjs${tableCounter}data`] = csv;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
requestParams["sasjs_tables"] = sasjsTables.join(" ");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Request params", requestParams);
|
|
||||||
|
|
||||||
for (const key in requestParams) {
|
for (const key in requestParams) {
|
||||||
if (requestParams.hasOwnProperty(key)) {
|
if (requestParams.hasOwnProperty(key)) {
|
||||||
formData.append(key, requestParams[key]);
|
formData.append(key, requestParams[key]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Form data", formData);
|
|
||||||
|
|
||||||
let isRedirected = false;
|
let isRedirected = false;
|
||||||
|
|
||||||
sasjsWaitingRequest.requestPromise.promise = new Promise(
|
sasjsWaitingRequest.requestPromise.promise = new Promise(
|
||||||
@@ -526,11 +775,9 @@ export default class SASjs {
|
|||||||
if (isError) {
|
if (isError) {
|
||||||
reject({ MESSAGE: errorMsg });
|
reject({ MESSAGE: errorMsg });
|
||||||
}
|
}
|
||||||
const headers: any = {
|
const headers: any = {};
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
if (this.csrfTokenWeb) {
|
||||||
};
|
headers[this.csrfTokenWeb.headerName] = this.csrfTokenWeb.value;
|
||||||
if (this._csrfHeader && this._csrf) {
|
|
||||||
headers[this._csrfHeader] = this._csrf;
|
|
||||||
}
|
}
|
||||||
fetch(apiUrl, {
|
fetch(apiUrl, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -545,16 +792,15 @@ export default class SASjs {
|
|||||||
|
|
||||||
if (tokenHeader) {
|
if (tokenHeader) {
|
||||||
const token = response.headers.get(tokenHeader);
|
const token = response.headers.get(tokenHeader);
|
||||||
this._csrfHeader = tokenHeader;
|
this.csrfTokenWeb = {
|
||||||
this._csrf = token;
|
headerName: tokenHeader,
|
||||||
|
value: token || "",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (response.redirected && config.serverType === ServerType.SAS9) {
|
||||||
response.redirected &&
|
|
||||||
this.sasjsConfig.serverType === ServerType.SAS9
|
|
||||||
) {
|
|
||||||
isRedirected = true;
|
isRedirected = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -565,30 +811,28 @@ export default class SASjs {
|
|||||||
(needsRetry(responseText) || isRedirected) &&
|
(needsRetry(responseText) || isRedirected) &&
|
||||||
!isLogInRequired(responseText)
|
!isLogInRequired(responseText)
|
||||||
) {
|
) {
|
||||||
if (this.retryCount < requestRetryLimit) {
|
if (this.retryCountWeb < requestRetryLimit) {
|
||||||
this.retryCount++;
|
this.retryCountWeb++;
|
||||||
this.request(sasJob, data, params).then(
|
this.request(sasJob, data).then(
|
||||||
(res: any) => resolve(res),
|
(res: any) => resolve(res),
|
||||||
(err: any) => reject(err)
|
(err: any) => reject(err)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.retryCount = 0;
|
this.retryCountWeb = 0;
|
||||||
reject(responseText);
|
reject(responseText);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.retryCount = 0;
|
this.retryCountWeb = 0;
|
||||||
this.parseLogFromResponse(responseText, program);
|
this.parseLogFromResponse(responseText, program);
|
||||||
|
|
||||||
if (isLogInRequired(responseText)) {
|
if (isLogInRequired(responseText)) {
|
||||||
if (loginRequiredCallback) loginRequiredCallback(true);
|
if (loginRequiredCallback) loginRequiredCallback(true);
|
||||||
sasjsWaitingRequest.requestPromise.resolve = resolve;
|
sasjsWaitingRequest.requestPromise.resolve = resolve;
|
||||||
sasjsWaitingRequest.requestPromise.reject = reject;
|
sasjsWaitingRequest.requestPromise.reject = reject;
|
||||||
|
sasjsWaitingRequest.config = config;
|
||||||
this.sasjsWaitingRequests.push(sasjsWaitingRequest);
|
this.sasjsWaitingRequests.push(sasjsWaitingRequest);
|
||||||
} else {
|
} else {
|
||||||
if (
|
if (config.serverType === ServerType.SAS9 && config.debug) {
|
||||||
this.sasjsConfig.serverType === ServerType.SAS9 &&
|
|
||||||
this.sasjsConfig.debug
|
|
||||||
) {
|
|
||||||
this.updateUsername(responseText);
|
this.updateUsername(responseText);
|
||||||
const jsonResponseText = this.parseSAS9Response(responseText);
|
const jsonResponseText = this.parseSAS9Response(responseText);
|
||||||
|
|
||||||
@@ -600,8 +844,8 @@ export default class SASjs {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
this.sasjsConfig.serverType === ServerType.SASViya &&
|
config.serverType === ServerType.SASViya &&
|
||||||
this.sasjsConfig.debug
|
config.debug
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
this.parseSASVIYADebugResponse(responseText).then(
|
this.parseSASVIYADebugResponse(responseText).then(
|
||||||
@@ -639,72 +883,15 @@ export default class SASjs {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return sasjsWaitingRequest.requestPromise.promise;
|
return sasjsWaitingRequest.requestPromise.promise;
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private setCsrfTokenApi = (csrfToken: CsrfToken) => {
|
||||||
* Creates the folders and services in the provided JSON on the given location
|
this.csrfTokenApi = csrfToken;
|
||||||
* (appLoc) on the given server (serverUrl).
|
};
|
||||||
* @param serviceJson - the JSON specifying the folders and services to be created.
|
|
||||||
* @param appLoc - the base folder in which to create the new folders and
|
|
||||||
* services. If not provided, is taken from SASjsConfig.
|
|
||||||
* @param serverUrl - the server on which to deploy the folders and services.
|
|
||||||
* If not provided, is taken from SASjsConfig.
|
|
||||||
* @param accessToken - an optional access token to be passed in when
|
|
||||||
* using this function from the command line.
|
|
||||||
*/
|
|
||||||
public async deployServicePack(
|
|
||||||
serviceJson: any,
|
|
||||||
appLoc?: string,
|
|
||||||
serverUrl?: string,
|
|
||||||
accessToken?: string
|
|
||||||
) {
|
|
||||||
if (this.sasjsConfig.serverType !== ServerType.SASViya) {
|
|
||||||
throw new Error("This operation is only supported on SAS Viya servers.");
|
|
||||||
}
|
|
||||||
|
|
||||||
let sasApiClient: any = null;
|
|
||||||
if (serverUrl || appLoc) {
|
|
||||||
if (!serverUrl) {
|
|
||||||
serverUrl = this.sasjsConfig.serverUrl;
|
|
||||||
}
|
|
||||||
if (!appLoc) {
|
|
||||||
appLoc = this.sasjsConfig.appLoc;
|
|
||||||
}
|
|
||||||
if (this.sasjsConfig.serverType === ServerType.SASViya) {
|
|
||||||
sasApiClient = new SASViyaApiClient(serverUrl, appLoc);
|
|
||||||
} else if (this.sasjsConfig.serverType === ServerType.SAS9) {
|
|
||||||
sasApiClient = new SAS9ApiClient(serverUrl);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let sasClientConfig: any = null;
|
|
||||||
if (this.sasjsConfig.serverType === ServerType.SASViya) {
|
|
||||||
sasClientConfig = this.sasViyaApiClient!.getConfig();
|
|
||||||
} else if (this.sasjsConfig.serverType === ServerType.SAS9) {
|
|
||||||
sasClientConfig = this.sas9ApiClient!.getConfig();
|
|
||||||
}
|
|
||||||
serverUrl = sasClientConfig.serverUrl;
|
|
||||||
appLoc = sasClientConfig.rootFolderName as string;
|
|
||||||
}
|
|
||||||
const members =
|
|
||||||
serviceJson.members[0].name === "services"
|
|
||||||
? serviceJson.members[0].members
|
|
||||||
: serviceJson.members;
|
|
||||||
await this.createFoldersAndServices(
|
|
||||||
appLoc,
|
|
||||||
members,
|
|
||||||
accessToken,
|
|
||||||
sasApiClient
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async resendWaitingRequests() {
|
private async resendWaitingRequests() {
|
||||||
for (const sasjsWaitingRequest of this.sasjsWaitingRequests) {
|
for (const sasjsWaitingRequest of this.sasjsWaitingRequests) {
|
||||||
this.request(
|
this.request(sasjsWaitingRequest.SASjob, sasjsWaitingRequest.data).then(
|
||||||
sasjsWaitingRequest.SASjob,
|
|
||||||
sasjsWaitingRequest.data,
|
|
||||||
sasjsWaitingRequest.params
|
|
||||||
).then(
|
|
||||||
(res: any) => {
|
(res: any) => {
|
||||||
sasjsWaitingRequest.requestPromise.resolve(res);
|
sasjsWaitingRequest.requestPromise.resolve(res);
|
||||||
},
|
},
|
||||||
@@ -717,12 +904,12 @@ export default class SASjs {
|
|||||||
this.sasjsWaitingRequests = [];
|
this.sasjsWaitingRequests = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
private getRequestParams(): any {
|
private getRequestParamsWeb(): any {
|
||||||
const requestParams: any = {};
|
const requestParams: any = {};
|
||||||
|
|
||||||
// if (this._csrf) {
|
if (this.csrfTokenWeb) {
|
||||||
// requestParams["_csrf"] = this._csrf;
|
requestParams["_csrf"] = this.csrfTokenWeb.value;
|
||||||
// }
|
}
|
||||||
|
|
||||||
if (this.sasjsConfig.debug) {
|
if (this.sasjsConfig.debug) {
|
||||||
requestParams["_omittextlog"] = "false";
|
requestParams["_omittextlog"] = "false";
|
||||||
@@ -859,14 +1046,20 @@ export default class SASjs {
|
|||||||
let generatedCode = "";
|
let generatedCode = "";
|
||||||
let sasWork = null;
|
let sasWork = null;
|
||||||
|
|
||||||
if (response) {
|
if (response && response.result && response.log) {
|
||||||
sourceCode = parseSourceCode(response);
|
sourceCode = parseSourceCode(response.log);
|
||||||
generatedCode = parseGeneratedCode(response);
|
generatedCode = parseGeneratedCode(response.log);
|
||||||
sasWork = await this.parseSasWork(response);
|
sasWork = JSON.parse(response.result).WORK;
|
||||||
|
} else {
|
||||||
|
if (response) {
|
||||||
|
sourceCode = parseSourceCode(response);
|
||||||
|
generatedCode = parseGeneratedCode(response);
|
||||||
|
sasWork = await this.parseSasWork(response);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.sasjsRequests.push({
|
this.sasjsRequests.push({
|
||||||
logFile: response,
|
logFile: (response && response.log) || response,
|
||||||
serviceLink: program,
|
serviceLink: program,
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
sourceCode,
|
sourceCode,
|
||||||
@@ -887,7 +1080,7 @@ export default class SASjs {
|
|||||||
try {
|
try {
|
||||||
jsonResponse = JSON.parse(this.parseSAS9Response(response));
|
jsonResponse = JSON.parse(this.parseSAS9Response(response));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await this.parseSASVIYADebugResponse(response).then(
|
await this.parseSASVIYADebugResponse(response).then(
|
||||||
@@ -895,11 +1088,11 @@ export default class SASjs {
|
|||||||
try {
|
try {
|
||||||
jsonResponse = JSON.parse(resText);
|
jsonResponse = JSON.parse(resText);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(err: any) => {
|
(err: any) => {
|
||||||
console.log(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -916,6 +1109,10 @@ export default class SASjs {
|
|||||||
return sortedRequests;
|
return sortedRequests;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public clearSasRequests() {
|
||||||
|
this.sasjsRequests = [];
|
||||||
|
}
|
||||||
|
|
||||||
private setupConfiguration() {
|
private setupConfiguration() {
|
||||||
if (
|
if (
|
||||||
this.sasjsConfig.serverUrl === undefined ||
|
this.sasjsConfig.serverUrl === undefined ||
|
||||||
@@ -951,7 +1148,9 @@ export default class SASjs {
|
|||||||
else
|
else
|
||||||
this.sasViyaApiClient = new SASViyaApiClient(
|
this.sasViyaApiClient = new SASViyaApiClient(
|
||||||
this.sasjsConfig.serverUrl,
|
this.sasjsConfig.serverUrl,
|
||||||
this.sasjsConfig.appLoc
|
this.sasjsConfig.appLoc,
|
||||||
|
this.sasjsConfig.contextName,
|
||||||
|
this.setCsrfTokenApi
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (this.sasjsConfig.serverType === ServerType.SAS9) {
|
if (this.sasjsConfig.serverType === ServerType.SAS9) {
|
||||||
@@ -959,6 +1158,12 @@ export default class SASjs {
|
|||||||
this.sas9ApiClient!.setConfig(this.sasjsConfig.serverUrl);
|
this.sas9ApiClient!.setConfig(this.sasjsConfig.serverUrl);
|
||||||
else this.sas9ApiClient = new SAS9ApiClient(this.sasjsConfig.serverUrl);
|
else this.sas9ApiClient = new SAS9ApiClient(this.sasjsConfig.serverUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.fileUploader = new FileUploader(
|
||||||
|
this.sasjsConfig.appLoc,
|
||||||
|
this.sasjsConfig.serverUrl,
|
||||||
|
this.jobsPath
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private setLoginUrl = (matches: RegExpExecArray) => {
|
private setLoginUrl = (matches: RegExpExecArray) => {
|
||||||
|
|||||||
160
src/SessionManager.ts
Normal file
160
src/SessionManager.ts
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
import { Session, Context, CsrfToken } from "./types";
|
||||||
|
import { asyncForEach, makeRequest } from "./utils";
|
||||||
|
|
||||||
|
const MAX_SESSION_COUNT = 1;
|
||||||
|
|
||||||
|
export class SessionManager {
|
||||||
|
constructor(
|
||||||
|
private serverUrl: string,
|
||||||
|
private contextName: string,
|
||||||
|
private setCsrfToken: (csrfToken: CsrfToken) => void
|
||||||
|
) {}
|
||||||
|
private sessions: Session[] = [];
|
||||||
|
private currentContext: Context | null = null;
|
||||||
|
private csrfToken: CsrfToken | null = null;
|
||||||
|
|
||||||
|
async getSession(accessToken?: string) {
|
||||||
|
await this.createSessions(accessToken);
|
||||||
|
this.createAndWaitForSession(accessToken);
|
||||||
|
const session = this.sessions.pop();
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearSession(id: string, accessToken?: string) {
|
||||||
|
const deleteSessionRequest = {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: this.getHeaders(accessToken),
|
||||||
|
};
|
||||||
|
return await this.request<Session>(
|
||||||
|
`${this.serverUrl}/compute/sessions/${id}`,
|
||||||
|
deleteSessionRequest
|
||||||
|
).then(() => {
|
||||||
|
this.sessions = this.sessions.filter((s) => s.id !== id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createSessions(accessToken?: string) {
|
||||||
|
if (!this.sessions.length) {
|
||||||
|
if (!this.currentContext) {
|
||||||
|
await this.setCurrentContext(accessToken);
|
||||||
|
}
|
||||||
|
await asyncForEach(new Array(MAX_SESSION_COUNT), async () => {
|
||||||
|
const createdSession = await this.createAndWaitForSession(accessToken);
|
||||||
|
this.sessions.push(createdSession);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createAndWaitForSession(accessToken?: string) {
|
||||||
|
const createSessionRequest = {
|
||||||
|
method: "POST",
|
||||||
|
headers: this.getHeaders(accessToken),
|
||||||
|
};
|
||||||
|
const { result: createdSession, etag } = await this.request<Session>(
|
||||||
|
`${this.serverUrl}/compute/contexts/${this.currentContext!.id}/sessions`,
|
||||||
|
createSessionRequest
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.waitForSession(createdSession, etag);
|
||||||
|
this.sessions.push(createdSession);
|
||||||
|
return createdSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async setCurrentContext(accessToken?: string) {
|
||||||
|
if (!this.currentContext) {
|
||||||
|
const { result: contexts } = await this.request<{
|
||||||
|
items: Context[];
|
||||||
|
}>(`${this.serverUrl}/compute/contexts`, {
|
||||||
|
headers: this.getHeaders(accessToken),
|
||||||
|
});
|
||||||
|
|
||||||
|
const contextsList =
|
||||||
|
contexts && contexts.items && contexts.items.length
|
||||||
|
? contexts.items
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const currentContext = contextsList.find(
|
||||||
|
(c: any) => c.name === this.contextName
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!currentContext) {
|
||||||
|
throw new Error(
|
||||||
|
`The context ${this.contextName} was not found on the server ${this.serverUrl}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentContext = currentContext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getHeaders(accessToken?: string) {
|
||||||
|
const headers: any = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
};
|
||||||
|
if (accessToken) {
|
||||||
|
headers.Authorization = `Bearer ${accessToken}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async waitForSession(
|
||||||
|
session: Session,
|
||||||
|
etag: string | null,
|
||||||
|
accessToken?: string,
|
||||||
|
silent = false
|
||||||
|
) {
|
||||||
|
let sessionState = session.state;
|
||||||
|
const headers: any = {
|
||||||
|
...this.getHeaders(accessToken),
|
||||||
|
"If-None-Match": etag,
|
||||||
|
};
|
||||||
|
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`);
|
||||||
|
}
|
||||||
|
resolve(sessionState);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resolve(sessionState);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async request<T>(
|
||||||
|
url: string,
|
||||||
|
options: RequestInit,
|
||||||
|
contentType: "text" | "json" = "json"
|
||||||
|
) {
|
||||||
|
if (this.csrfToken) {
|
||||||
|
options.headers = {
|
||||||
|
...options.headers,
|
||||||
|
[this.csrfToken.headerName]: this.csrfToken.value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return await makeRequest<T>(
|
||||||
|
url,
|
||||||
|
options,
|
||||||
|
(token) => {
|
||||||
|
this.csrfToken = token;
|
||||||
|
this.setCsrfToken(token);
|
||||||
|
},
|
||||||
|
contentType
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,4 +8,5 @@ export interface Job {
|
|||||||
createdBy: string;
|
createdBy: string;
|
||||||
links: Link[];
|
links: Link[];
|
||||||
results: JobResult;
|
results: JobResult;
|
||||||
|
error?: any;
|
||||||
}
|
}
|
||||||
|
|||||||
3
src/types/JobDefinition.ts
Normal file
3
src/types/JobDefinition.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export interface JobDefinition {
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
@@ -27,4 +27,5 @@ export class SASjsConfig {
|
|||||||
*/
|
*/
|
||||||
debug: boolean = true;
|
debug: boolean = true;
|
||||||
contextName: string = "";
|
contextName: string = "";
|
||||||
|
useComputeApi = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,5 +10,5 @@ export interface SASjsWaitingRequest {
|
|||||||
};
|
};
|
||||||
SASjob: string;
|
SASjob: string;
|
||||||
data: any;
|
data: any;
|
||||||
params?: any;
|
config?: any;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import { Link } from "./Link";
|
||||||
|
|
||||||
export interface Session {
|
export interface Session {
|
||||||
id: string;
|
id: string;
|
||||||
|
state: string;
|
||||||
|
links: Link[];
|
||||||
}
|
}
|
||||||
|
|||||||
9
src/types/UploadFile.ts
Normal file
9
src/types/UploadFile.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* Represents a object that is passed to file uploader
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export interface UploadFile {
|
||||||
|
file: File;
|
||||||
|
fileName: string;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -8,3 +8,4 @@ export * from "./SASjsRequest";
|
|||||||
export * from "./SASjsWaitingRequest";
|
export * from "./SASjsWaitingRequest";
|
||||||
export * from "./ServerType";
|
export * from "./ServerType";
|
||||||
export * from "./Session";
|
export * from "./Session";
|
||||||
|
export * from "./UploadFile";
|
||||||
|
|||||||
33
src/utils/formatDataForRequest.ts
Normal file
33
src/utils/formatDataForRequest.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { convertToCSV } from "./convertToCsv";
|
||||||
|
import { splitChunks } from "./splitChunks";
|
||||||
|
|
||||||
|
export const formatDataForRequest = (data: any) => {
|
||||||
|
const sasjsTables = [];
|
||||||
|
let tableCounter = 0;
|
||||||
|
const result: any = {};
|
||||||
|
|
||||||
|
for (const tableName in data) {
|
||||||
|
tableCounter++;
|
||||||
|
sasjsTables.push(tableName);
|
||||||
|
const csv = convertToCSV(data[tableName]);
|
||||||
|
if (csv === "ERROR: LARGE STRING LENGTH") {
|
||||||
|
throw new Error(
|
||||||
|
"The max length of a string value in SASjs is 32765 characters."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// if csv has length more then 16k, send in chunks
|
||||||
|
if (csv.length > 16000) {
|
||||||
|
const csvChunks = splitChunks(csv);
|
||||||
|
// append chunks to form data with same key
|
||||||
|
result[`sasjs${tableCounter}data0`] = csvChunks.length;
|
||||||
|
csvChunks.forEach((chunk, index) => {
|
||||||
|
result[`sasjs${tableCounter}data${index + 1}`] = chunk;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
result[`sasjs${tableCounter}data`] = csv;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result["sasjs_tables"] = sasjsTables.join(" ");
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
@@ -1,16 +1,26 @@
|
|||||||
import { CsrfToken } from "../types";
|
import { CsrfToken } from "../types";
|
||||||
|
import { needsRetry } from "./needsRetry";
|
||||||
|
|
||||||
|
let retryCount: number = 0;
|
||||||
|
let retryLimit: number = 5;
|
||||||
|
|
||||||
export async function makeRequest<T>(
|
export async function makeRequest<T>(
|
||||||
url: string,
|
url: string,
|
||||||
request: RequestInit,
|
request: RequestInit,
|
||||||
callback: (value: CsrfToken) => any,
|
callback: (value: CsrfToken) => any,
|
||||||
contentType: "text" | "json" = "json"
|
contentType: "text" | "json" = "json"
|
||||||
): Promise<T> {
|
): Promise<{ result: T; etag: string | null }> {
|
||||||
|
let retryRequest: any = null;
|
||||||
|
|
||||||
const responseTransform =
|
const responseTransform =
|
||||||
contentType === "json"
|
contentType === "json"
|
||||||
? (res: Response) => res.json()
|
? (res: Response) => res.json()
|
||||||
: (res: Response) => res.text();
|
: (res: Response) => res.text();
|
||||||
const result = await fetch(url, request).then((response) => {
|
let etag = null;
|
||||||
|
const result = await fetch(url, request).then(async (response) => {
|
||||||
|
if (response.redirected && response.url.includes("SASLogon/login")) {
|
||||||
|
return Promise.reject({ status: 401 });
|
||||||
|
}
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
if (response.status === 403) {
|
if (response.status === 403) {
|
||||||
const tokenHeader = response.headers.get("X-CSRF-HEADER");
|
const tokenHeader = response.headers.get("X-CSRF-HEADER");
|
||||||
@@ -22,16 +32,76 @@ export async function makeRequest<T>(
|
|||||||
value: token || "",
|
value: token || "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const retryRequest = {
|
retryRequest = {
|
||||||
...request,
|
...request,
|
||||||
headers: { ...request.headers, [tokenHeader]: token },
|
headers: { ...request.headers, [tokenHeader]: token },
|
||||||
};
|
};
|
||||||
return fetch(url, retryRequest).then(responseTransform);
|
return fetch(url, retryRequest).then((res) => {
|
||||||
|
etag = res.headers.get("ETag");
|
||||||
|
return responseTransform(res);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
const body = await response.text();
|
||||||
|
|
||||||
|
if (needsRetry(body)) {
|
||||||
|
if (retryCount < retryLimit) {
|
||||||
|
retryCount++;
|
||||||
|
let retryResponse = await makeRequest(
|
||||||
|
url,
|
||||||
|
retryRequest || request,
|
||||||
|
callback,
|
||||||
|
contentType
|
||||||
|
);
|
||||||
|
retryCount = 0;
|
||||||
|
|
||||||
|
etag = retryResponse.etag;
|
||||||
|
return retryResponse.result;
|
||||||
|
} else {
|
||||||
|
retryCount = 0;
|
||||||
|
|
||||||
|
throw new Error("Request retry limit exceeded");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject({ status: response.status, body });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return responseTransform(response);
|
if (response.status === 204) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
const responseTransformed = await responseTransform(response);
|
||||||
|
let responseText = "";
|
||||||
|
|
||||||
|
if (typeof responseTransformed === "string") {
|
||||||
|
responseText = responseTransformed;
|
||||||
|
} else {
|
||||||
|
responseText = JSON.stringify(responseTransformed);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needsRetry(responseText)) {
|
||||||
|
if (retryCount < retryLimit) {
|
||||||
|
retryCount++;
|
||||||
|
const retryResponse = await makeRequest(
|
||||||
|
url,
|
||||||
|
retryRequest || request,
|
||||||
|
callback,
|
||||||
|
contentType
|
||||||
|
);
|
||||||
|
retryCount = 0;
|
||||||
|
|
||||||
|
etag = retryResponse.etag;
|
||||||
|
return retryResponse.result;
|
||||||
|
} else {
|
||||||
|
retryCount = 0;
|
||||||
|
|
||||||
|
throw new Error("Request retry limit exceeded");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
etag = response.headers.get("ETag");
|
||||||
|
return responseTransformed;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return result;
|
return { result, etag };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
export const needsRetry = (responseText: string): boolean => {
|
export const needsRetry = (responseText: string): boolean => {
|
||||||
return (
|
return (
|
||||||
(responseText.includes('"errorCode":403') &&
|
!!responseText &&
|
||||||
|
((responseText.includes('"errorCode":403') &&
|
||||||
responseText.includes("_csrf") &&
|
responseText.includes("_csrf") &&
|
||||||
responseText.includes("X-CSRF-TOKEN")) ||
|
responseText.includes("X-CSRF-TOKEN")) ||
|
||||||
(responseText.includes('"status":403') &&
|
(responseText.includes('"status":403') &&
|
||||||
responseText.includes('"error":"Forbidden"')) ||
|
responseText.includes('"error":"Forbidden"')) ||
|
||||||
(responseText.includes('"status":449') &&
|
(responseText.includes('"status":449') &&
|
||||||
responseText.includes("Authentication success, retry original request"))
|
responseText.includes(
|
||||||
|
"Authentication success, retry original request"
|
||||||
|
)))
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
"links": [
|
"links": [
|
||||||
{
|
{
|
||||||
"label": "SASjs on Github",
|
"label": "SASjs on Github",
|
||||||
"url": "https://github.com/macropeople/sasjs"
|
"url": "https://github.com/sasjs/adapter"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "SASjs.io",
|
"label": "SASjs.io",
|
||||||
@@ -32,11 +32,11 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "SASjs CLI",
|
"label": "SASjs CLI",
|
||||||
"url": "https://github.com/macropeople/sasjs-cli"
|
"url": "https://github.com/sasjs/cli"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "React Seed App",
|
"label": "React Seed App",
|
||||||
"url": "https://github.com/macropeople/react-seed-app"
|
"url": "https://github.com/sasjs/react-seed-app"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user