mirror of
https://github.com/sasjs/adapter.git
synced 2026-01-04 19:20:05 +00:00
Compare commits
120 Commits
suggestion
...
v1.3.12
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
619833db29 | ||
|
|
a587d9f6de | ||
|
|
83fb89f779 | ||
|
|
6b98bbce7c | ||
|
|
3c2487e423 | ||
|
|
0d52af5375 | ||
|
|
d0da343efc | ||
|
|
54f401a319 | ||
|
|
5efcb11b7d | ||
|
|
929d7b993b | ||
|
|
688221c042 | ||
|
|
57d0b30f47 | ||
| 0d5af2487d | |||
| 1ea163fd03 | |||
|
|
f27444bc52 | ||
|
|
de426c9a92 | ||
|
|
a006ead205 | ||
|
|
422c2a1fd5 | ||
|
|
0c6409e402 | ||
|
|
68b864cf75 | ||
|
|
75a11cdff4 | ||
|
|
4e2b6d32cc | ||
|
|
cd9757b383 | ||
|
|
fb727788d0 | ||
|
|
35eb6c4935 | ||
|
|
ea0f338b90 | ||
|
|
b6a17b39b9 | ||
|
|
9ed64e5a2c | ||
|
|
0479a5d651 | ||
|
|
005f10bb47 | ||
|
|
98c9cb78ff | ||
|
|
8192f69f67 | ||
|
|
c28a8ebf15 | ||
|
|
a409d8cdb6 | ||
|
|
618a20eaba | ||
|
|
c9b1273c31 | ||
|
|
59674744be | ||
|
|
870cc0055b | ||
|
|
0ffa62fab4 | ||
|
|
b4c7868fb6 | ||
|
|
2266578013 | ||
|
|
f2ebe1a5b0 | ||
|
|
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 |
8
.github/workflows/build.yml
vendored
8
.github/workflows/build.yml
vendored
@@ -21,7 +21,11 @@ jobs:
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm ci
|
||||
- run: npm run package:lib
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
- name: Check code style
|
||||
run: npm run lint
|
||||
- name: Build Package
|
||||
run: npm run package:lib
|
||||
env:
|
||||
CI: true
|
||||
|
||||
4
.github/workflows/npmpublish.yml
vendored
4
.github/workflows/npmpublish.yml
vendored
@@ -6,7 +6,7 @@ name: SASjs Build and Publish
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -16,6 +16,8 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
- name: Check code style
|
||||
run: npm run lint
|
||||
- name: Build Project
|
||||
run: npm run build
|
||||
- name: Semantic Release
|
||||
|
||||
6
.prettierrc
Normal file
6
.prettierrc
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"trailingComma": "none",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": false
|
||||
}
|
||||
@@ -16,7 +16,7 @@ Tests are run using cypress. Before running tests, you need to define the follow
|
||||
|
||||
```
|
||||
|
||||
filename mc url "https://raw.githubusercontent.com/macropeople/macrocore/main/mc_all.sas?_=1";
|
||||
filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
|
||||
%inc mc;
|
||||
filename ft15f001 temp;
|
||||
parmcards4;
|
||||
@@ -40,18 +40,13 @@ parmcards4;
|
||||
# Viya
|
||||
|
||||
```
|
||||
filename mc url "https://raw.githubusercontent.com/macropeople/macrocore/main/mc_all.sas";
|
||||
filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
|
||||
%inc mc;
|
||||
|
||||
filename ft15f001 temp;
|
||||
parmcards4;
|
||||
%webout(FETCH)
|
||||
%webout(OPEN)
|
||||
%global sasjs_tables;
|
||||
%let sasjs_tables=&sasjs_tables;
|
||||
%put &=sasjs_tables;
|
||||
%let sasjs_tables=&sasjs_tables;
|
||||
%macro x();
|
||||
%global sasjs_tables;
|
||||
%do i=1 %to %sysfunc(countw(&sasjs_tables));
|
||||
%let table=%scan(&sasjs_tables,&i);
|
||||
%webout(OBJ,&table)
|
||||
@@ -60,13 +55,11 @@ parmcards4;
|
||||
%x()
|
||||
%webout(CLOSE)
|
||||
;;;;
|
||||
%mv_createwebservice(path=/Public/app/common,name=sendObj)
|
||||
%mp_createwebservice(path=/Public/app/common,name=sendObj)
|
||||
filename ft15f001 temp;
|
||||
parmcards4;
|
||||
%webout(FETCH)
|
||||
%webout(OPEN)
|
||||
%global sasjs_tables;
|
||||
%let sasjs_tables=&sasjs_tables;
|
||||
%put &=sasjs_tables;
|
||||
%macro x();
|
||||
%do i=1 %to %sysfunc(countw(&sasjs_tables));
|
||||
%let table=%scan(&sasjs_tables,&i);
|
||||
@@ -76,7 +69,15 @@ parmcards4;
|
||||
%x()
|
||||
%webout(CLOSE)
|
||||
;;;;
|
||||
%mv_createwebservice(path=/Public/app/common,name=sendArr)
|
||||
%mp_createwebservice(path=/Public/app/common,name=sendArr)
|
||||
filename ft15f001 temp;
|
||||
parmcards4;
|
||||
If you can keep your head when all about you
|
||||
Are losing theirs and blaming it on you,
|
||||
If you can trust yourself when all men doubt you,
|
||||
But make allowance for their doubting too;
|
||||
;;;;
|
||||
%mp_createwebservice(path=/Public/app/common,name=makeErr)
|
||||
```
|
||||
|
||||
The above services will return anything you send. To run the tests simply launch `npm run cypress`.
|
||||
|
||||
@@ -10,13 +10,13 @@ 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.
|
||||
|
||||
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)
|
||||
|
||||
## 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/main/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:
|
||||
|
||||
@@ -43,6 +43,6 @@ You now have a simple web app with a backend service!
|
||||
|
||||
# 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.
|
||||
|
||||
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
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
342
docs/index.html
342
docs/index.html
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
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-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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
213
example.html
213
example.html
@@ -1,114 +1,109 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<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'>
|
||||
<script src='https://cdn.jsdelivr.net/combine/npm/chart.js@2.9.3,npm/jquery@3.5.1,npm/@sasjs/adapter@1'></script>
|
||||
<script>
|
||||
const sasJs = new SASjs.default({
|
||||
appLoc: '/Products/demo/readme',
|
||||
serverType:'SAS9',
|
||||
debug: 'false'
|
||||
})
|
||||
|
||||
const initSasJs = () => {
|
||||
$('#loading-spinner').show()
|
||||
|
||||
// instantiate sasJs with options such as backend app location
|
||||
// login (it's also possible to set an auto login when making requests)
|
||||
sasJs.logIn($('#username')[0].value, $('#password')[0].value)
|
||||
.then((response) => {
|
||||
if (response.isLoggedIn === true) {
|
||||
$('#loading-spinner').hide()
|
||||
$('.login').hide()
|
||||
$('#getDataBtn').show()
|
||||
$('#cars').show()
|
||||
}
|
||||
})
|
||||
<head>
|
||||
<script src="https://cdn.jsdelivr.net/combine/npm/chart.js@2.9.3,npm/jquery@3.5.1,npm/@sasjs/adapter@1"></script>
|
||||
<script>
|
||||
var sasJs = new SASjs.default({
|
||||
appLoc: "/Public/app/readme"
|
||||
,serverType:"SAS9"
|
||||
,debug: false
|
||||
});
|
||||
function initSasJs() {
|
||||
$('#loading-spinner').show()
|
||||
// instantiate sasjs with options such as backend app location
|
||||
// login (it's also possible to set an autologin when making requests)
|
||||
sasJs.logIn(
|
||||
$('#username')[0].value
|
||||
,$('#password')[0].value
|
||||
).then((response) => {
|
||||
if (response.isLoggedIn === true) {
|
||||
$('#loading-spinner').hide()
|
||||
$('.login').hide()
|
||||
$('#getdata').show()
|
||||
$('#cars').show()
|
||||
}
|
||||
})
|
||||
}
|
||||
function getData(){
|
||||
$('#loading-spinner').show()
|
||||
$('#myChart').remove();
|
||||
$('#chart-container').append('<canvas id="myChart" style="display: none;"></canvas>')
|
||||
// make a request to a SAS service
|
||||
var type = $("#cars")[0].options[$("#cars")[0].selectedIndex].value;
|
||||
// request data from an endpoint under your appLoc
|
||||
sasJs.request("/common/getdata", {
|
||||
// send data as an array of objects - each object is one row
|
||||
fromjs: [{ type: type }]
|
||||
}).then((response) => {
|
||||
$('#myChart').show();
|
||||
var labels = []
|
||||
var data = []
|
||||
response.areas.map((d) => {
|
||||
labels.push(d.MAKE);
|
||||
data.push(d.AVPRICE);
|
||||
})
|
||||
$('#loading-spinner').hide()
|
||||
initGraph(labels, data, type);
|
||||
})
|
||||
}
|
||||
function initGraph(labels, data, type){
|
||||
var myCanvas = document.getElementById("myChart");
|
||||
var ctx = myCanvas.getContext("2d");
|
||||
var myChart = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: "Average Invoice Price in USD for " + type + " Cars by Manufacturer",
|
||||
data: data,
|
||||
backgroundColor: "rgba(255,99,132,0.2)",
|
||||
borderColor: "rgba(255,99,132,1)",
|
||||
borderWidth: 1,
|
||||
hoverBackgroundColor: "rgba(255,99,132,0.4)",
|
||||
hoverBorderColor: "rgba(255,99,132,1)",
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
scales: {yAxes: [{ticks: {beginAtZero: true}}]}
|
||||
}
|
||||
|
||||
// make a request to a SAS service
|
||||
const getData = () => {
|
||||
$('#loading-spinner').show()
|
||||
$('#myChart').remove()
|
||||
$('#chart-container').append("<canvas id='myChart' style='display: none'></canvas>")
|
||||
|
||||
const type = $('#cars')[0].options[$('#cars')[0].selectedIndex].value
|
||||
|
||||
// request data from an endpoint under your appLoc
|
||||
// send data as an array of objects - each object is one row
|
||||
sasJs.request('/common/getdata', {fromjs: [{ type: type }]})
|
||||
.then((response) => {
|
||||
$('#myChart').show()
|
||||
$('#loading-spinner').hide()
|
||||
|
||||
const labels = response.areas.map(area => area.MAKE)
|
||||
const data = response.areas.map(area => area.AVPRICE)
|
||||
|
||||
initGraph(labels, data, type)
|
||||
})
|
||||
}
|
||||
|
||||
const initGraph = (labels, data, type) => {
|
||||
const myCanvas = document.getElementById('myChart')
|
||||
const ctx = myCanvas.getContext('2d')
|
||||
|
||||
const myChart = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: `Average Invoice Price in USD for ${type} Cars by Manufacturer`,
|
||||
data: data,
|
||||
backgroundColor: 'rgba(255,99,132,0.2)',
|
||||
borderColor: 'rgba(255,99,132,1)',
|
||||
borderWidth: 1,
|
||||
hoverBackgroundColor: 'rgba(255,99,132,0.4)',
|
||||
hoverBorderColor: 'rgba(255,99,132,1)',
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
scales: {yAxes: [{ticks: {beginAtZero: true}}]}
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class='container-fluid' style='text-align: center; margin-top: 10px'>
|
||||
<div class='row'>
|
||||
<div class='col-lg-5 col-md-7 col-sm-10 mx-auto mx-auto'>
|
||||
<h1>Demo Seed App for <span class='code'>SASjs</span></h1>
|
||||
<div class='login' id='login-form'>
|
||||
<div class='form-group'>
|
||||
<input class='form-control' type='text' id='username' placeholder='Enter username' />
|
||||
</div>
|
||||
<div class='form-group'>
|
||||
<input class='form-control' type='password' id='password' placeholder='Enter password' />
|
||||
</div>
|
||||
<button id='login' onclick='initSasJs()' class='login btn btn-primary' style='margin-bottom: 5px'>Log In</button>
|
||||
</div>
|
||||
<select name='cars' id='cars' style='margin-bottom: 5px; display: none' class='form-control'>
|
||||
<option value='Hybrid'>Hybrid</option>
|
||||
<option value='SUV'>SUV</option>
|
||||
<option value='Sedan'>Sedan</option>
|
||||
<option value='Sports'>Sports</option>
|
||||
<option value='Truck'>Truck</option>
|
||||
<option value='Wagon'>Wagon</option>
|
||||
</select>
|
||||
<button id='getDataBtn' onclick='getData()' style='margin-bottom: 5px; display: none' class='btn btn-success'>Get Data</button>
|
||||
<br>
|
||||
<br>
|
||||
<div id='loading-spinner' class='spinner-border text-primary' role='status' style='display: none'>
|
||||
<span class='sr-only'>Loading...</span>
|
||||
</div>
|
||||
<br>
|
||||
</div>
|
||||
});
|
||||
}
|
||||
</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>
|
||||
<body>
|
||||
<div class="container-fluid" style="text-align: center; margin-top: 10px;">
|
||||
<div class="row">
|
||||
<div class="col-lg-5 col-md-7 col-sm-10 mx-auto mx-auto">
|
||||
<h1>Demo Seed App for <span class="code">SASjs</span></h1>
|
||||
<div class="login" id="login-form">
|
||||
<div class="form-group">
|
||||
<input class="form-control" type="text" id="username" placeholder="Enter username" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input class="form-control" type="password" id="password" placeholder="Enter password" />
|
||||
</div>
|
||||
<button id="login" onclick="initSasJs()" class="login btn btn-primary" style="margin-bottom: 5px;">Log In</button>
|
||||
</div>
|
||||
<select name="cars" id="cars" style="margin-bottom: 5px; display: none;" class="form-control">
|
||||
<option value="Hybrid">Hybrid</option>
|
||||
<option value="SUV">SUV</option>
|
||||
<option value="Sedan">Sedan</option>
|
||||
<option value="Sports">Sports</option>
|
||||
<option value="Truck">Truck</option>
|
||||
<option value="Wagon">Wagon</option>
|
||||
</select>
|
||||
<button id="getdata" onclick="getData()" style="margin-bottom: 5px; display: none;" class="btn btn-success">Get Data</button><br><br>
|
||||
<div id="loading-spinner" class="spinner-border text-primary" role="status" style="display: none;">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div><br>
|
||||
</div>
|
||||
<div id='chart-container' style='height: 65vh; width: 100%; position: relative; margin: auto'>
|
||||
<canvas id='myChart' style='display: none'></canvas>
|
||||
</div>
|
||||
</body>
|
||||
</head>
|
||||
</div>
|
||||
</div>
|
||||
<div id="chart-container" style="height: 65vh; width: 100%; position: relative; margin: auto;">
|
||||
<canvas id="myChart" style="display: none;"></canvas>
|
||||
</div>
|
||||
</body>
|
||||
</head>
|
||||
|
||||
1716
package-lock.json
generated
1716
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
18
package.json
18
package.json
@@ -5,8 +5,8 @@
|
||||
"build": "rimraf build && webpack",
|
||||
"package:lib": "npm run build && cp ./package.json build && cd build && npm version \"5.0.0\" && npm pack",
|
||||
"publish:lib": "npm run build && cd build && npm publish",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"src/**/*.js\"",
|
||||
"lint": "tslint -p tsconfig.json",
|
||||
"lint:fix": "npx prettier --write 'src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}'",
|
||||
"lint": "npx prettier --check 'src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}'",
|
||||
"test": "jest",
|
||||
"prepublishOnly": "cp -r ./build/* . && rm -rf ./build",
|
||||
"postpublish": "git clean -fd",
|
||||
@@ -22,9 +22,6 @@
|
||||
{
|
||||
"pkgRoot": "/build"
|
||||
}
|
||||
],
|
||||
"branches": [
|
||||
"main"
|
||||
]
|
||||
},
|
||||
"keywords": [
|
||||
@@ -40,23 +37,22 @@
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/isomorphic-fetch": "0.0.35",
|
||||
"@types/jest": "^26.0.4",
|
||||
"@types/jest": "^26.0.10",
|
||||
"cp": "^0.2.0",
|
||||
"jest": "^25.5.4",
|
||||
"path": "^0.12.7",
|
||||
"prettier": "^2.0.5",
|
||||
"rimraf": "^3.0.2",
|
||||
"semantic-release": "^17.1.1",
|
||||
"ts-jest": "^25.5.1",
|
||||
"ts-loader": "^7.0.5",
|
||||
"tslint": "^6.1.2",
|
||||
"ts-loader": "^8.0.3",
|
||||
"tslint": "^6.1.3",
|
||||
"tslint-config-prettier": "^1.18.0",
|
||||
"typedoc": "^0.17.8",
|
||||
"typedoc-neo-theme": "^1.0.9",
|
||||
"typedoc-plugin-external-module-name": "^4.0.3",
|
||||
"typescript": "^3.9.6",
|
||||
"typescript": "^3.9.7",
|
||||
"uglifyjs-webpack-plugin": "^2.2.0",
|
||||
"webpack": "^4.43.0",
|
||||
"webpack": "^4.44.1",
|
||||
"webpack-cli": "^3.3.12"
|
||||
},
|
||||
"main": "index.js",
|
||||
|
||||
6
sasjs-tests/.prettierrc
Normal file
6
sasjs-tests/.prettierrc
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"trailingComma": "none",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": false
|
||||
}
|
||||
252
sasjs-tests/package-lock.json
generated
252
sasjs-tests/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sasjs-tests",
|
||||
"version": "0.1.0",
|
||||
"name": "@sasjs/tests",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -1356,11 +1356,81 @@
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz",
|
||||
"integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw=="
|
||||
},
|
||||
"@sasjs/adapter": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/adapter/-/adapter-1.3.6.tgz",
|
||||
"integrity": "sha512-d2B+cTII+vabKCU8mJy90mEz3tCWw2pEp4qIBGsDamJiTS0Rx69dgXGHuRUm8KtjLDHHrzwXATsqviU3dnU0QQ==",
|
||||
"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.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/test-framework/-/test-framework-1.4.0.tgz",
|
||||
"integrity": "sha512-Pd8PUH5B5RO6q4w3OQXX7aWicvA/CJMXA/FCf2xp332ZTKBb/5uV+HphAOFKpCh58y+ykYYVSV0ZaDO/4t1h3A==",
|
||||
"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": {
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.3.tgz",
|
||||
"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": {
|
||||
"version": "4.2.0",
|
||||
"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-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": {
|
||||
"version": "5.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.8.tgz",
|
||||
@@ -3754,6 +3832,11 @@
|
||||
"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": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
||||
@@ -4095,6 +4178,15 @@
|
||||
"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": {
|
||||
"version": "6.0.5",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
|
||||
@@ -4912,11 +5004,21 @@
|
||||
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
|
||||
},
|
||||
"encoding": {
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz",
|
||||
"integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=",
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
|
||||
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
|
||||
"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": {
|
||||
@@ -5609,6 +5711,11 @@
|
||||
"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": {
|
||||
"version": "0.1.2",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "5.1.1",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "4.10.1",
|
||||
"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": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.2.tgz",
|
||||
@@ -8123,6 +8245,11 @@
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
|
||||
@@ -8887,6 +9014,11 @@
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
|
||||
@@ -9880,6 +10012,11 @@
|
||||
"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": {
|
||||
"version": "1.0.26",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"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": {
|
||||
"version": "5.2.0",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "2.2.5",
|
||||
"resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.5.tgz",
|
||||
@@ -12086,6 +12225,47 @@
|
||||
"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.2.0",
|
||||
"resolved": "https://registry.npmjs.org/semantic-ui-react/-/semantic-ui-react-1.2.0.tgz",
|
||||
"integrity": "sha512-9tNL94nEy16RdupTQNiURyemWUIxtTpQgFimCbOOHRBOe1ApsFz3FWFsrGjv9zFtE7dQMslLYov9BQOelTCVwA==",
|
||||
"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.20",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
|
||||
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "6.3.0",
|
||||
"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": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
|
||||
@@ -13443,6 +13628,11 @@
|
||||
"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": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
||||
@@ -13744,6 +13934,14 @@
|
||||
"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": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.2.tgz",
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{
|
||||
"name": "sasjs-tests",
|
||||
"version": "0.1.0",
|
||||
"name": "@sasjs/tests",
|
||||
"version": "1.0.0",
|
||||
"homepage": ".",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@sasjs/adapter": "^1.3.6",
|
||||
"@sasjs/test-framework": "^1.4.0",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.5.0",
|
||||
"@testing-library/user-event": "^7.2.1",
|
||||
@@ -16,7 +18,6 @@
|
||||
"react-dom": "^16.13.1",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "3.4.1",
|
||||
"sasjs": "file:../build/sasjs-5.0.0.tgz",
|
||||
"typescript": "^3.9.6"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -24,7 +25,7 @@
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"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": {
|
||||
"extends": "react-app"
|
||||
@@ -44,4 +45,4 @@
|
||||
"devDependencies": {
|
||||
"node-sass": "^4.14.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"appLoc": "/Public/app",
|
||||
"serverType": "SASVIYA",
|
||||
"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,10 +1,9 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import App from './App';
|
||||
import React from "react";
|
||||
import { render } from "@testing-library/react";
|
||||
import App from "./App";
|
||||
|
||||
test('renders learn react link', () => {
|
||||
test("renders learn react link", () => {
|
||||
const { getByText } = render(<App />);
|
||||
const linkElement = getByText(/learn react/i);
|
||||
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -1,54 +1,30 @@
|
||||
import React, { ReactElement, useState, useContext, useEffect } from "react";
|
||||
import "./App.scss";
|
||||
import TestSuiteRunner from "./TestSuiteRunner";
|
||||
import { AppContext } from "./context/AppContext";
|
||||
import { TestSuiteRunner, TestSuite, AppContext } from "@sasjs/test-framework";
|
||||
import { basicTests } from "./testSuites/Basic";
|
||||
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 [appLoc, setAppLoc] = useState("");
|
||||
const [debug, setDebug] = useState(false);
|
||||
const { adapter } = useContext(AppContext);
|
||||
const { adapter, config } = useContext(AppContext);
|
||||
const [testSuites, setTestSuites] = useState<TestSuite[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (adapter) adapter.setDebugState(debug);
|
||||
}, [debug, adapter]);
|
||||
|
||||
useEffect(() => {
|
||||
if (appLoc && adapter) {
|
||||
adapter.setSASjsConfig({ ...adapter.getSasjsConfig(), appLoc });
|
||||
if (adapter) {
|
||||
setTestSuites([
|
||||
basicTests(adapter, config.userName, config.password),
|
||||
sendArrTests(adapter),
|
||||
sendObjTests(adapter),
|
||||
specialCaseTests(adapter),
|
||||
sasjsRequestTests(adapter)
|
||||
]);
|
||||
}
|
||||
}, [appLoc, adapter]);
|
||||
|
||||
useEffect(() => {
|
||||
setAppLoc(adapter.getSasjsConfig().appLoc);
|
||||
}, [adapter]);
|
||||
}, [adapter, config]);
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<div className="controls">
|
||||
<div className="row">
|
||||
<label>Debug</label>
|
||||
<div className="debug-toggle">
|
||||
<label className="switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
onChange={(e) => setDebug(e.target.checked)} // FIXME: rename 'e' => 'event'
|
||||
/>
|
||||
<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)} // FIXME: rename 'e' => 'event'
|
||||
placeholder="AppLoc"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{adapter && <TestSuiteRunner adapter={adapter} />}
|
||||
{adapter && testSuites && <TestSuiteRunner testSuites={testSuites} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { ReactElement, useState, useCallback, useContext } from "react";
|
||||
import "./Login.scss";
|
||||
import { AppContext } from "./context/AppContext";
|
||||
import { AppContext } from "@sasjs/test-framework";
|
||||
import { Redirect } from "react-router-dom";
|
||||
|
||||
const Login = (): ReactElement<{}> => {
|
||||
@@ -9,14 +9,11 @@ const Login = (): ReactElement<{}> => {
|
||||
const appContext = useContext(AppContext);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(e) => { // FIXME: rename 'e' => 'event'
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
|
||||
appContext.adapter.logIn(username, password)
|
||||
.then(() => {
|
||||
appContext.setIsLoggedIn(true);
|
||||
});
|
||||
// FIXME: catch block
|
||||
appContext.adapter.logIn(username, password).then((res) => {
|
||||
appContext.setIsLoggedIn(res.isLoggedIn);
|
||||
});
|
||||
},
|
||||
[username, password, appContext]
|
||||
);
|
||||
@@ -41,7 +38,7 @@ const Login = (): ReactElement<{}> => {
|
||||
type="password"
|
||||
value={password}
|
||||
required
|
||||
onChange={(e) => setPassword(e.target.value)} // FIXME: rename 'e' => 'event'
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" className="submit-button">
|
||||
@@ -54,4 +51,4 @@ const Login = (): ReactElement<{}> => {
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
export default Login;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { ReactElement, useContext, FunctionComponent } from "react";
|
||||
import { Redirect, Route } from "react-router-dom";
|
||||
import { AppContext } from "./context/AppContext";
|
||||
import { AppContext } from "@sasjs/test-framework";
|
||||
|
||||
interface PrivateRouteProps {
|
||||
component: FunctionComponent;
|
||||
@@ -8,14 +8,16 @@ interface PrivateRouteProps {
|
||||
path: string;
|
||||
}
|
||||
|
||||
const PrivateRoute = (props: PrivateRouteProps): ReactElement<PrivateRouteProps> => {
|
||||
const PrivateRoute = (
|
||||
props: PrivateRouteProps
|
||||
): ReactElement<PrivateRouteProps> => {
|
||||
const { component, path, exact } = props;
|
||||
const appContext = useContext(AppContext);
|
||||
|
||||
return appContext.isLoggedIn ?
|
||||
return appContext.isLoggedIn ? (
|
||||
<Route component={component} path={path} exact={exact} />
|
||||
:
|
||||
) : (
|
||||
<Redirect to="/login" />
|
||||
);
|
||||
};
|
||||
|
||||
export default PrivateRoute;
|
||||
export default PrivateRoute;
|
||||
|
||||
@@ -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,125 +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"; // FIXME: declared but never used
|
||||
import "./TestSuiteRunner.scss";
|
||||
import SASjs from "sasjs";
|
||||
import { AppContext } from "./context/AppContext";
|
||||
import { sendArrTests, sendObjTests } from "./testSuites/RequestData"; // FIXME: declared but never used
|
||||
import { specialCaseTests } from "./testSuites/SpecialCases";
|
||||
import { sasjsRequestTests } from "./testSuites/SasjsRequests"; // FIXME: declared but never used
|
||||
|
||||
interface TestSuiteRunnerProps {
|
||||
adapter: SASjs;
|
||||
}
|
||||
|
||||
const TestSuiteRunner = (props: TestSuiteRunnerProps): ReactElement<TestSuiteRunnerProps> => {
|
||||
const { adapter } = props;
|
||||
const { config } = useContext(AppContext); // FIXME: declared but never used
|
||||
const [testSuites, setTestSuites] = useState<TestSuite[]>([]);
|
||||
const [runTests, setRunTests] = useState(false);
|
||||
const [completedTestSuites, setCompletedTestSuites] = useState< // FIXME: create interface
|
||||
{
|
||||
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"} // TODO: 'submit-button' class should be assigned by default
|
||||
onClick={() => setRunTests(true)}
|
||||
disabled={runTests}
|
||||
>
|
||||
{runTests ? (
|
||||
<>
|
||||
{
|
||||
// FIXME: fragment is not needed in this case
|
||||
}
|
||||
<div className="loading-spinner"></div>Running tests...
|
||||
</>
|
||||
) : (
|
||||
"Run tests!"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
{completedTestSuites.map((completedTestSuite, index) => { // TODO: refactor
|
||||
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,87 +0,0 @@
|
||||
import React, { ReactElement, useEffect, useState } from "react";
|
||||
import TestCard from "./TestCard";
|
||||
import { start } from "repl"; // FIXME: declared but never used
|
||||
|
||||
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" && ( // FIXME: use switch statement
|
||||
<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,101 +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< // TODO: create an interface
|
||||
{
|
||||
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((test, index) => {
|
||||
const { test, result, error } = test;
|
||||
const { title, description } = test;
|
||||
|
||||
return (
|
||||
<TestCard
|
||||
key={index}
|
||||
title={title}
|
||||
description={description}
|
||||
status={result === true ? "passed" : "failed"}
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<></> // FIXME: use {null} instead
|
||||
);
|
||||
};
|
||||
|
||||
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,43 +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); // TODO: refactor variable names
|
||||
|
||||
return (
|
||||
<div className="test-suite">
|
||||
<div className={`test-suite-name ${overallStatus ? "passed" : "failed"}`}>
|
||||
{name}
|
||||
</div>
|
||||
{tests.map((test, index) => {
|
||||
const { test, result, error, executionTime } = test;
|
||||
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,52 +0,0 @@
|
||||
import React, { createContext, useState, useEffect, ReactNode } from "react";
|
||||
import SASjs from "sasjs";
|
||||
|
||||
export const AppContext = createContext<{ // TODO: create an interface
|
||||
config: any; // TODO: be more specific on type declaration
|
||||
sasJsConfig: any; // TODO: be more specific on type declaration
|
||||
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}); // TODO: be more specific on type declaration
|
||||
const [adapter, setAdapter] = useState<SASjs>((null as unknown) as SASjs);
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("config.json") // TODO: use axios instead of fetch
|
||||
.then((res) => res.json())
|
||||
.then((configJson: any) => { // TODO: be more specific on type declaration
|
||||
setConfig(configJson);
|
||||
|
||||
const sasjs = new SASjs(configJson.sasJsConfig);
|
||||
|
||||
setAdapter(sasjs);
|
||||
|
||||
sasjs.checkSession().then((response) => {
|
||||
setIsLoggedIn(response.isLoggedIn);
|
||||
}); // FIXME: add catch block
|
||||
});// FIXME: add catch block
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AppContext.Provider
|
||||
value={{
|
||||
config,
|
||||
sasJsConfig: config.sasJsConfig,
|
||||
isLoggedIn,
|
||||
setIsLoggedIn,
|
||||
adapter,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</AppContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -1,8 +1,7 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
||||
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans",
|
||||
"Droid Sans", "Helvetica Neue", sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-color: #1f2027;
|
||||
@@ -10,8 +9,7 @@ body {
|
||||
}
|
||||
|
||||
* {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
|
||||
monospace;
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||
}
|
||||
|
||||
input {
|
||||
|
||||
@@ -3,7 +3,7 @@ import ReactDOM from "react-dom";
|
||||
import { Route, HashRouter, Switch } from "react-router-dom";
|
||||
import "./index.scss";
|
||||
import * as serviceWorker from "./serviceWorker";
|
||||
import { AppProvider } from "./context/AppContext";
|
||||
import { AppProvider } from "@sasjs/test-framework";
|
||||
import PrivateRoute from "./PrivateRoute";
|
||||
import Login from "./Login";
|
||||
import App from "./App";
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
// opt-in, read https://bit.ly/CRA-PWA
|
||||
|
||||
const isLocalhost = Boolean(
|
||||
window.location.hostname === 'localhost' ||
|
||||
window.location.hostname === "localhost" ||
|
||||
// [::1] is the IPv6 localhost address.
|
||||
window.location.hostname === '[::1]' ||
|
||||
window.location.hostname === "[::1]" ||
|
||||
// 127.0.0.0/8 are considered localhost for IPv4.
|
||||
window.location.hostname.match(
|
||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
||||
@@ -21,7 +21,7 @@ const isLocalhost = Boolean(
|
||||
);
|
||||
|
||||
export function register(config) {
|
||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||
if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
|
||||
// The URL constructor is available in all browsers that support SW.
|
||||
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
|
||||
if (publicUrl.origin !== window.location.origin) {
|
||||
@@ -31,7 +31,7 @@ export function register(config) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
window.addEventListener("load", () => {
|
||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
||||
|
||||
if (isLocalhost) {
|
||||
@@ -42,8 +42,8 @@ export function register(config) {
|
||||
// service worker/PWA documentation.
|
||||
navigator.serviceWorker.ready.then(() => {
|
||||
console.log(
|
||||
'This web app is being served cache-first by a service ' +
|
||||
'worker. To learn more, visit https://bit.ly/CRA-PWA'
|
||||
"This web app is being served cache-first by a service " +
|
||||
"worker. To learn more, visit https://bit.ly/CRA-PWA"
|
||||
);
|
||||
});
|
||||
} else {
|
||||
@@ -57,21 +57,21 @@ export function register(config) {
|
||||
function registerValidSW(swUrl, config) {
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then(registration => {
|
||||
.then((registration) => {
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing;
|
||||
if (installingWorker == null) {
|
||||
return;
|
||||
}
|
||||
installingWorker.onstatechange = () => {
|
||||
if (installingWorker.state === 'installed') {
|
||||
if (installingWorker.state === "installed") {
|
||||
if (navigator.serviceWorker.controller) {
|
||||
// At this point, the updated precached content has been fetched,
|
||||
// but the previous service worker will still serve the older
|
||||
// content until all client tabs are closed.
|
||||
console.log(
|
||||
'New content is available and will be used when all ' +
|
||||
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
|
||||
"New content is available and will be used when all " +
|
||||
"tabs for this page are closed. See https://bit.ly/CRA-PWA."
|
||||
);
|
||||
|
||||
// Execute callback
|
||||
@@ -82,7 +82,7 @@ function registerValidSW(swUrl, config) {
|
||||
// At this point, everything has been precached.
|
||||
// It's the perfect time to display a
|
||||
// "Content is cached for offline use." message.
|
||||
console.log('Content is cached for offline use.');
|
||||
console.log("Content is cached for offline use.");
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onSuccess) {
|
||||
@@ -93,25 +93,25 @@ function registerValidSW(swUrl, config) {
|
||||
};
|
||||
};
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error during service worker registration:', error);
|
||||
.catch((error) => {
|
||||
console.error("Error during service worker registration:", error);
|
||||
});
|
||||
}
|
||||
|
||||
function checkValidServiceWorker(swUrl, config) {
|
||||
// Check if the service worker can be found. If it can't reload the page.
|
||||
fetch(swUrl, {
|
||||
headers: { 'Service-Worker': 'script' },
|
||||
headers: { "Service-Worker": "script" }
|
||||
})
|
||||
.then(response => {
|
||||
.then((response) => {
|
||||
// Ensure service worker exists, and that we really are getting a JS file.
|
||||
const contentType = response.headers.get('content-type');
|
||||
const contentType = response.headers.get("content-type");
|
||||
if (
|
||||
response.status === 404 ||
|
||||
(contentType != null && contentType.indexOf('javascript') === -1)
|
||||
(contentType != null && contentType.indexOf("javascript") === -1)
|
||||
) {
|
||||
// No service worker found. Probably a different app. Reload the page.
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
navigator.serviceWorker.ready.then((registration) => {
|
||||
registration.unregister().then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
@@ -123,18 +123,18 @@ function checkValidServiceWorker(swUrl, config) {
|
||||
})
|
||||
.catch(() => {
|
||||
console.log(
|
||||
'No internet connection found. App is running in offline mode.'
|
||||
"No internet connection found. App is running in offline mode."
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function unregister() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
if ("serviceWorker" in navigator) {
|
||||
navigator.serviceWorker.ready
|
||||
.then(registration => {
|
||||
.then((registration) => {
|
||||
registration.unregister();
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
console.error(error.message);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import "@testing-library/jest-dom/extend-expect";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import SASjs, { ServerType, SASjsConfig } from "sasjs";
|
||||
import { TestSuite } from "../types";
|
||||
import SASjs, { ServerType, SASjsConfig } from "@sasjs/adapter";
|
||||
import { TestSuite } from "@sasjs/test-framework";
|
||||
|
||||
const defaultConfig: SASjsConfig = {
|
||||
serverUrl: window.location.origin,
|
||||
@@ -9,6 +9,7 @@ const defaultConfig: SASjsConfig = {
|
||||
serverType: ServerType.SASViya,
|
||||
debug: true,
|
||||
contextName: "SAS Job Execution compute context",
|
||||
useComputeApi: false
|
||||
};
|
||||
|
||||
const customConfig = {
|
||||
@@ -17,7 +18,7 @@ const customConfig = {
|
||||
pathSASViya: "viya",
|
||||
appLoc: "/Public/seedapp",
|
||||
serverType: ServerType.SAS9,
|
||||
debug: false,
|
||||
debug: false
|
||||
};
|
||||
|
||||
export const basicTests = (
|
||||
@@ -33,16 +34,18 @@ export const basicTests = (
|
||||
test: async () => {
|
||||
return adapter.logIn(userName, password);
|
||||
},
|
||||
assertion: (response: any) => // FIXME: be more specific on type declaration
|
||||
response && response.isLoggedIn && response.userName === userName,
|
||||
assertion: (response: any) =>
|
||||
response && response.isLoggedIn && response.userName === userName
|
||||
},
|
||||
{
|
||||
title: "Default config",
|
||||
description: "Should instantiate with default config when none is provided",
|
||||
test: async () => Promise.resolve(new SASjs()),
|
||||
description:
|
||||
"Should instantiate with default config when none is provided",
|
||||
test: async () => {
|
||||
return Promise.resolve(new SASjs());
|
||||
},
|
||||
assertion: (sasjsInstance: SASjs) => {
|
||||
const sasjsConfig = sasjsInstance.getSasjsConfig();
|
||||
|
||||
return (
|
||||
sasjsConfig.serverUrl === defaultConfig.serverUrl &&
|
||||
sasjsConfig.pathSAS9 === defaultConfig.pathSAS9 &&
|
||||
@@ -51,7 +54,7 @@ export const basicTests = (
|
||||
sasjsConfig.serverType === defaultConfig.serverType &&
|
||||
sasjsConfig.debug === defaultConfig.debug
|
||||
);
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Custom config",
|
||||
@@ -69,7 +72,7 @@ export const basicTests = (
|
||||
sasjsConfig.serverType === customConfig.serverType &&
|
||||
sasjsConfig.debug === customConfig.debug
|
||||
);
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Config overrides",
|
||||
@@ -89,7 +92,7 @@ export const basicTests = (
|
||||
sasjsConfig.serverType === defaultConfig.serverType &&
|
||||
sasjsConfig.debug === false
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
@@ -1,44 +1,45 @@
|
||||
import SASjs from "sasjs";
|
||||
import { TestSuite } from "../types";
|
||||
import SASjs from "@sasjs/adapter";
|
||||
import { TestSuite } from "@sasjs/test-framework";
|
||||
|
||||
const stringData: any = { table1: [{ col1: "first col value" }] }; // TODO: be more specific on type declaration
|
||||
const numericData: any = { table1: [{ col1: 3.14159265 }] }; // TODO: be more specific on type declaration
|
||||
const multiColumnData: any = { // TODO: be more specific on type declaration
|
||||
table1: [{ col1: 42, col2: 1.618, col3: "x", col4: "x" }],
|
||||
const stringData: any = { table1: [{ col1: "first col value" }] };
|
||||
const numericData: any = { table1: [{ col1: 3.14159265 }] };
|
||||
const multiColumnData: any = {
|
||||
table1: [{ col1: 42, col2: 1.618, col3: "x", col4: "x" }]
|
||||
};
|
||||
const multipleRowsWithNulls: any = { // TODO: be more specific on type declaration
|
||||
const multipleRowsWithNulls: any = {
|
||||
table1: [
|
||||
{ col1: 42, col2: null, col3: "x", col4: "" },
|
||||
{ col1: 42, col2: null, col3: "x", col4: "" },
|
||||
{ col1: 42, col2: null, col3: "x", col4: "" },
|
||||
{ col1: 42, col2: 1.62, col3: "x", col4: "x" },
|
||||
{ col1: 42, col2: 1.62, col3: "x", col4: "x" },
|
||||
],
|
||||
{ col1: 42, col2: 1.62, col3: "x", col4: "x" }
|
||||
]
|
||||
};
|
||||
const multipleColumnsWithNulls: any = { // TODO: be more specific on type declaration
|
||||
const multipleColumnsWithNulls: any = {
|
||||
table1: [
|
||||
{ col1: 42, col2: null, col3: "x", col4: null },
|
||||
{ col1: 42, col2: null, col3: "x", col4: null },
|
||||
{ col1: 42, col2: null, col3: "x", col4: null },
|
||||
{ col1: 42, col2: null, col3: "x", col4: "" },
|
||||
{ col1: 42, col2: null, col3: "x", col4: "" },
|
||||
],
|
||||
{ col1: 42, col2: null, col3: "x", col4: "" }
|
||||
]
|
||||
};
|
||||
|
||||
const getLongStringData = (length = 32764) => { // FIXME: add type declaration
|
||||
const getLongStringData = (length = 32764) => {
|
||||
let x = "X";
|
||||
|
||||
for (let i = 1; i <= length; i++) x += 'X'
|
||||
|
||||
const data: any = { table1: [{ col1: x }] }; // TODO: be more specific on type declaration
|
||||
|
||||
for (let i = 1; i <= length; i++) {
|
||||
x = x + "X";
|
||||
}
|
||||
const data: any = { table1: [{ col1: x }] };
|
||||
return data;
|
||||
};
|
||||
|
||||
const getLargeObjectData = () => {
|
||||
const data = { table1: [{ big: "data" }] };
|
||||
|
||||
for (let i = 1; i < 10000; i++) data.table1.push(data.table1[0])
|
||||
for (let i = 1; i < 10000; i++) {
|
||||
data.table1.push(data.table1[0]);
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
@@ -49,8 +50,12 @@ export const sendArrTests = (adapter: SASjs): TestSuite => ({
|
||||
{
|
||||
title: "Single string value",
|
||||
description: "Should send an array with a single string value",
|
||||
test: () => adapter.request("common/sendArr", stringData),
|
||||
assertion: (res: any) => res.table1[0][0] === stringData.table1[0].col1 // TODO: be more specific on type declaration
|
||||
test: () => {
|
||||
return adapter.request("common/sendArr", stringData);
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
return res.table1[0][0] === stringData.table1[0].col1;
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Long string value",
|
||||
@@ -59,18 +64,22 @@ export const sendArrTests = (adapter: SASjs): TestSuite => ({
|
||||
test: () => {
|
||||
return adapter.request("common/sendArr", getLongStringData());
|
||||
},
|
||||
assertion: (res: any) => res.table1[0][0] === getLongStringData().table1[0].col1 // TODO: be more specific on type declaration
|
||||
assertion: (res: any) => {
|
||||
const longStringData = getLongStringData();
|
||||
return res.table1[0][0] === longStringData.table1[0].col1;
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Overly long string value",
|
||||
description:
|
||||
"Should error out with long string values over 32765 characters",
|
||||
test: () => adapter
|
||||
.request("common/sendArr", getLongStringData(32767))
|
||||
.catch((e) => e), // TODO: rename
|
||||
assertion: (error: any) => { // TODO: be more specific on type declaration
|
||||
return !!error && !!error.MESSAGE; // FIXME: refactor
|
||||
test: () => {
|
||||
const data = getLongStringData(32767);
|
||||
return adapter.request("common/sendArr", data).catch((e) => e);
|
||||
},
|
||||
assertion: (error: any) => {
|
||||
return !!error && !!error.MESSAGE;
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Single numeric value",
|
||||
@@ -78,9 +87,9 @@ export const sendArrTests = (adapter: SASjs): TestSuite => ({
|
||||
test: () => {
|
||||
return adapter.request("common/sendArr", numericData);
|
||||
},
|
||||
assertion: (res: any) => { // TODO: be more specific on type declaration
|
||||
assertion: (res: any) => {
|
||||
return res.table1[0][0] === numericData.table1[0].col1;
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Multiple columns",
|
||||
@@ -88,14 +97,14 @@ export const sendArrTests = (adapter: SASjs): TestSuite => ({
|
||||
test: () => {
|
||||
return adapter.request("common/sendArr", multiColumnData);
|
||||
},
|
||||
assertion: (res: any) => { // TODO: be more specific on type declaration
|
||||
assertion: (res: any) => {
|
||||
return (
|
||||
res.table1[0][0] === multiColumnData.table1[0].col1 &&
|
||||
res.table1[0][1] === multiColumnData.table1[0].col2 &&
|
||||
res.table1[0][2] === multiColumnData.table1[0].col3 &&
|
||||
res.table1[0][3] === multiColumnData.table1[0].col4
|
||||
);
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Multiple rows with nulls",
|
||||
@@ -103,10 +112,9 @@ export const sendArrTests = (adapter: SASjs): TestSuite => ({
|
||||
test: () => {
|
||||
return adapter.request("common/sendArr", multipleRowsWithNulls);
|
||||
},
|
||||
assertion: (res: any) => { // TODO: be more specific on type declaration
|
||||
assertion: (res: any) => {
|
||||
let result = true;
|
||||
multipleRowsWithNulls.table1.forEach((_: any, index: number) => { // TODO: be more specific on type declaration
|
||||
// FIXME: use loop
|
||||
multipleRowsWithNulls.table1.forEach((_: any, index: number) => {
|
||||
result =
|
||||
result &&
|
||||
res.table1[index][0] === multipleRowsWithNulls.table1[index].col1;
|
||||
@@ -120,9 +128,8 @@ export const sendArrTests = (adapter: SASjs): TestSuite => ({
|
||||
result &&
|
||||
res.table1[index][3] === multipleRowsWithNulls.table1[index].col4;
|
||||
});
|
||||
|
||||
return result;
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Multiple columns with nulls",
|
||||
@@ -130,9 +137,9 @@ export const sendArrTests = (adapter: SASjs): TestSuite => ({
|
||||
test: () => {
|
||||
return adapter.request("common/sendArr", multipleColumnsWithNulls);
|
||||
},
|
||||
assertion: (res: any) => { // TODO: be more specific on type declaration
|
||||
assertion: (res: any) => {
|
||||
let result = true;
|
||||
multipleColumnsWithNulls.table1.forEach((_: any, index: number) => { // TODO: be more specific on type declaration
|
||||
multipleColumnsWithNulls.table1.forEach((_: any, index: number) => {
|
||||
result =
|
||||
result &&
|
||||
res.table1[index][0] ===
|
||||
@@ -151,9 +158,9 @@ export const sendArrTests = (adapter: SASjs): TestSuite => ({
|
||||
(multipleColumnsWithNulls.table1[index].col4 || "");
|
||||
});
|
||||
return result;
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
||||
@@ -163,12 +170,12 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
||||
title: "Invalid column name",
|
||||
description: "Should throw an error",
|
||||
test: async () => {
|
||||
const invalidData: any = { // TODO: be more specific on type declaration
|
||||
"1 invalid table": [{ col1: 42 }],
|
||||
const invalidData: any = {
|
||||
"1 invalid table": [{ col1: 42 }]
|
||||
};
|
||||
return adapter.request("common/sendObj", invalidData).catch((e) => e);
|
||||
},
|
||||
assertion: (error: any) => !!error && !!error.MESSAGE, // TODO: be more specific on type declaration
|
||||
assertion: (error: any) => !!error && !!error.MESSAGE
|
||||
},
|
||||
{
|
||||
title: "Single string value",
|
||||
@@ -176,9 +183,9 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
||||
test: () => {
|
||||
return adapter.request("common/sendObj", stringData);
|
||||
},
|
||||
assertion: (res: any) => { // TODO: be more specific on type declaration
|
||||
assertion: (res: any) => {
|
||||
return res.table1[0].COL1 === stringData.table1[0].col1;
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Long string value",
|
||||
@@ -187,10 +194,10 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
||||
test: () => {
|
||||
return adapter.request("common/sendObj", getLongStringData());
|
||||
},
|
||||
assertion: (res: any) => { // TODO: be more specific on type declaration
|
||||
assertion: (res: any) => {
|
||||
const longStringData = getLongStringData();
|
||||
return res.table1[0].COL1 === longStringData.table1[0].col1;
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Overly long string value",
|
||||
@@ -201,9 +208,9 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
||||
.request("common/sendObj", getLongStringData(32767))
|
||||
.catch((e) => e);
|
||||
},
|
||||
assertion: (error: any) => { // TODO: be more specific on type declaration
|
||||
assertion: (error: any) => {
|
||||
return !!error && !!error.MESSAGE;
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Single numeric value",
|
||||
@@ -211,9 +218,9 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
||||
test: () => {
|
||||
return adapter.request("common/sendObj", numericData);
|
||||
},
|
||||
assertion: (res: any) => { // TODO: be more specific on type declaration
|
||||
assertion: (res: any) => {
|
||||
return res.table1[0].COL1 === numericData.table1[0].col1;
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
@@ -222,10 +229,10 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
||||
test: () => {
|
||||
return adapter.request("common/sendObj", getLargeObjectData());
|
||||
},
|
||||
assertion: (res: any) => { // TODO: be more specific on type declaration
|
||||
assertion: (res: any) => {
|
||||
const data = getLargeObjectData();
|
||||
return res.table1[9000].BIG === data.table1[9000].big;
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Multiple columns",
|
||||
@@ -233,14 +240,14 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
||||
test: () => {
|
||||
return adapter.request("common/sendObj", multiColumnData);
|
||||
},
|
||||
assertion: (res: any) => { // TODO: be more specific on type declaration
|
||||
assertion: (res: any) => {
|
||||
return (
|
||||
res.table1[0].COL1 === multiColumnData.table1[0].col1 &&
|
||||
res.table1[0].COL2 === multiColumnData.table1[0].col2 &&
|
||||
res.table1[0].COL3 === multiColumnData.table1[0].col3 &&
|
||||
res.table1[0].COL4 === multiColumnData.table1[0].col4
|
||||
);
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Multiple rows with nulls",
|
||||
@@ -248,9 +255,9 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
||||
test: () => {
|
||||
return adapter.request("common/sendObj", multipleRowsWithNulls);
|
||||
},
|
||||
assertion: (res: any) => { // TODO: be more specific on type declaration
|
||||
assertion: (res: any) => {
|
||||
let result = true;
|
||||
multipleRowsWithNulls.table1.forEach((_: any, index: number) => { // TODO: be more specific on type declaration
|
||||
multipleRowsWithNulls.table1.forEach((_: any, index: number) => {
|
||||
result =
|
||||
result &&
|
||||
res.table1[index].COL1 === multipleRowsWithNulls.table1[index].col1;
|
||||
@@ -265,7 +272,7 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
||||
res.table1[index].COL4 === multipleRowsWithNulls.table1[index].col4;
|
||||
});
|
||||
return result;
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Multiple columns with nulls",
|
||||
@@ -273,9 +280,9 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
||||
test: () => {
|
||||
return adapter.request("common/sendObj", multipleColumnsWithNulls);
|
||||
},
|
||||
assertion: (res: any) => { // TODO: be more specific on type declaration
|
||||
assertion: (res: any) => {
|
||||
let result = true;
|
||||
multipleColumnsWithNulls.table1.forEach((_: any, index: number) => { // TODO: be more specific on type declaration
|
||||
multipleColumnsWithNulls.table1.forEach((_: any, index: number) => {
|
||||
result =
|
||||
result &&
|
||||
res.table1[index].COL1 ===
|
||||
@@ -294,7 +301,7 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
||||
(multipleColumnsWithNulls.table1[index].col4 || "");
|
||||
});
|
||||
return result;
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import SASjs from "sasjs";
|
||||
import { TestSuite } from "../types";
|
||||
import SASjs from "@sasjs/adapter";
|
||||
import { TestSuite } from "@sasjs/test-framework";
|
||||
|
||||
const data: any = { table1: [{ col1: "first col value" }] }; // TODO: be more specific on type declaration
|
||||
const data: any = { table1: [{ col1: "first col value" }] };
|
||||
|
||||
export const sasjsRequestTests = (adapter: SASjs): TestSuite => ({
|
||||
name: "SASjs Requests",
|
||||
@@ -9,12 +9,42 @@ export const sasjsRequestTests = (adapter: SASjs): TestSuite => ({
|
||||
{
|
||||
title: "WORK tables",
|
||||
description: "Should get WORK tables after request",
|
||||
test: async () => adapter.request("common/sendArr", data),
|
||||
assertion: (res: any) => {
|
||||
const requests = adapter.getSasRequests();
|
||||
|
||||
return adapter.getSasjsConfig().debug ? requests[0].SASWORK !== null : requests[0].SASWORK === null
|
||||
test: async () => {
|
||||
return adapter.request("common/sendArr", data);
|
||||
},
|
||||
assertion: () => {
|
||||
const requests = adapter.getSasRequests();
|
||||
if (adapter.getSasjsConfig().debug) {
|
||||
return requests[0].SASWORK !== null;
|
||||
} else {
|
||||
return requests[0].SASWORK === null;
|
||||
}
|
||||
}
|
||||
},
|
||||
],
|
||||
{
|
||||
title: "Make error and capture log",
|
||||
description: "Should make an error and capture log",
|
||||
test: async () => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
adapter
|
||||
.request("common/makeErr", data)
|
||||
.then((res) => {
|
||||
//no action here, this request must throw error
|
||||
})
|
||||
.catch((err) => {
|
||||
let sasRequests = adapter.getSasRequests();
|
||||
let makeErrRequest =
|
||||
sasRequests.find((req) =>
|
||||
req.serviceLink.includes("makeErr")
|
||||
) || null;
|
||||
|
||||
resolve(!!makeErrRequest);
|
||||
});
|
||||
});
|
||||
},
|
||||
assertion: (response) => {
|
||||
return response;
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import SASjs from "sasjs";
|
||||
import { TestSuite } from "../types";
|
||||
import SASjs from "@sasjs/adapter";
|
||||
import { TestSuite } from "@sasjs/test-framework";
|
||||
|
||||
const specialCharData: any = { // TODO: be more specific on type definition
|
||||
const specialCharData: any = {
|
||||
table1: [
|
||||
{
|
||||
tab: "\t",
|
||||
@@ -9,16 +9,16 @@ const specialCharData: any = { // TODO: be more specific on type definition
|
||||
cr: "\r",
|
||||
semicolon: ";semi",
|
||||
percent: "%",
|
||||
singleQuote: "'", // TODO: use ``
|
||||
doubleQuote: '"', // TODO: use ``
|
||||
singleQuote: "'",
|
||||
doubleQuote: '"',
|
||||
crlf: "\r\n",
|
||||
euro: "€euro",
|
||||
banghash: "!#banghash",
|
||||
},
|
||||
],
|
||||
banghash: "!#banghash"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const moreSpecialCharData: any = { // TODO: be more specific on type definition
|
||||
const moreSpecialCharData: any = {
|
||||
table1: [
|
||||
{
|
||||
speech0: '"speech',
|
||||
@@ -31,53 +31,51 @@ const moreSpecialCharData: any = { // TODO: be more specific on type definition
|
||||
sigma: "Σsigma",
|
||||
at: "@at",
|
||||
serbian: "Српски",
|
||||
dollar: "$",
|
||||
},
|
||||
],
|
||||
dollar: "$"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const getWideData = () => { // FIXME: declared but never used
|
||||
const cols: any = {}; // TODO: be more specific on type definition
|
||||
for (let i = 1; i <= 10000; i++) { // Why 10000?
|
||||
const getWideData = () => {
|
||||
const cols: any = {};
|
||||
for (let i = 1; i <= 10000; i++) {
|
||||
cols["col" + i] = "test" + i;
|
||||
}
|
||||
|
||||
const data: any = { // TODO: be more specific on type definition
|
||||
table1: [cols],
|
||||
const data: any = {
|
||||
table1: [cols]
|
||||
};
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const getTables = () => { // FIXME: declared but never used
|
||||
const tables: any = {}; // TODO: be more specific on type definition
|
||||
const getTables = () => {
|
||||
const tables: any = {};
|
||||
|
||||
for (let i = 1; i <= 100; i++) { // why 100
|
||||
for (let i = 1; i <= 100; i++) {
|
||||
tables["table" + i] = [{ col1: "x", col2: "x", col3: "x", col4: "x" }];
|
||||
}
|
||||
|
||||
return tables;
|
||||
};
|
||||
|
||||
const getLargeDataset = () => {
|
||||
const rows: any = []; // TODO: be more specific on type definition
|
||||
const colData: string = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; // FIXME: no need to explicitly mention data type
|
||||
|
||||
const rows: any = [];
|
||||
const colData: string =
|
||||
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
|
||||
for (let i = 1; i <= 10000; i++) {
|
||||
rows.push({ col1: colData, col2: colData, col3: colData, col4: colData });
|
||||
}
|
||||
|
||||
const data: any = { // TODO: be more specific on type definition
|
||||
table1: rows,
|
||||
const data: any = {
|
||||
table1: rows
|
||||
};
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
// FIXME: declared but never used
|
||||
const errorAndCsrfData: any = { // TODO: be more specific on type definition
|
||||
const errorAndCsrfData: any = {
|
||||
error: [{ col1: "q", col2: "w", col3: "e", col4: "r" }],
|
||||
_csrf: [{ col1: "q", col2: "w", col3: "e", col4: "r" }],
|
||||
_csrf: [{ col1: "q", col2: "w", col3: "e", col4: "r" }]
|
||||
};
|
||||
|
||||
export const specialCaseTests = (adapter: SASjs): TestSuite => ({
|
||||
@@ -86,8 +84,10 @@ export const specialCaseTests = (adapter: SASjs): TestSuite => ({
|
||||
{
|
||||
title: "Common special characters",
|
||||
description: "Should handle common special characters",
|
||||
test: () => adapter.request("common/sendArr", specialCharData),
|
||||
assertion: (res: any) => { // TODO: be more specific on type definition
|
||||
test: () => {
|
||||
return adapter.request("common/sendArr", specialCharData);
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
return (
|
||||
res.table1[0][0] === specialCharData.table1[0].tab &&
|
||||
res.table1[0][1] === specialCharData.table1[0].lf &&
|
||||
@@ -100,138 +100,151 @@ export const specialCaseTests = (adapter: SASjs): TestSuite => ({
|
||||
res.table1[0][8] === specialCharData.table1[0].euro &&
|
||||
res.table1[0][9] === specialCharData.table1[0].banghash
|
||||
);
|
||||
},
|
||||
}
|
||||
},
|
||||
// TODO: delete commented out code
|
||||
|
||||
// {
|
||||
// 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",
|
||||
test: () => adapter.request("common/sendArr", getLargeDataset()),
|
||||
assertion: (res: any) => { // TODO: be more specific on type definition
|
||||
test: () => {
|
||||
return adapter.request("common/sendObj", getLargeDataset());
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
const data = getLargeDataset();
|
||||
let result = true; // TODO: rename
|
||||
|
||||
let result = true;
|
||||
for (let i = 0; i <= 10; i++) {
|
||||
result = result && res.table1[i][0] === data.table1[i][0];
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
// {
|
||||
// title: "Error and _csrf tables with sendArr",
|
||||
// description: "Should handle error and _csrf tables",
|
||||
// test: () => {
|
||||
// return adapter.request("common/sendArr", errorAndCsrfData);
|
||||
// },
|
||||
// assertion: (res: any) => {
|
||||
// return (
|
||||
// res.error[0][0] === errorAndCsrfData.error[0].col1 &&
|
||||
// res.error[0][1] === errorAndCsrfData.error[0].col2 &&
|
||||
// res.error[0][2] === errorAndCsrfData.error[0].col3 &&
|
||||
// res.error[0][3] === errorAndCsrfData.error[0].col4 &&
|
||||
// res._csrf[0][0] === errorAndCsrfData._csrf[0].col1 &&
|
||||
// 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 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
|
||||
// );
|
||||
// },
|
||||
// },
|
||||
],
|
||||
});
|
||||
{
|
||||
title: "Large dataset with sendArr",
|
||||
description: "Should handle 5mb of data",
|
||||
test: () => {
|
||||
return adapter.request("common/sendArr", getLargeDataset());
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
const data = getLargeDataset();
|
||||
let result = true;
|
||||
for (let i = 0; i <= 10; i++) {
|
||||
result =
|
||||
result && res.table1[i][0] === Object.values(data.table1[i])[0];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Error and _csrf tables with sendArr",
|
||||
description: "Should handle error and _csrf tables",
|
||||
test: () => {
|
||||
return adapter.request("common/sendArr", errorAndCsrfData);
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
return (
|
||||
res.error[0][0] === errorAndCsrfData.error[0].col1 &&
|
||||
res.error[0][1] === errorAndCsrfData.error[0].col2 &&
|
||||
res.error[0][2] === errorAndCsrfData.error[0].col3 &&
|
||||
res.error[0][3] === errorAndCsrfData.error[0].col4 &&
|
||||
res._csrf[0][0] === errorAndCsrfData._csrf[0].col1 &&
|
||||
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 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>;
|
||||
}
|
||||
@@ -3,21 +3,20 @@ export const assert = (
|
||||
message = "Assertion failed"
|
||||
) => {
|
||||
let result;
|
||||
|
||||
try {
|
||||
if (typeof expression === "boolean") result = expression;
|
||||
else result = expression();
|
||||
if (typeof expression === "boolean") {
|
||||
result = expression;
|
||||
} else {
|
||||
result = expression();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(message);
|
||||
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
if (!!result) {
|
||||
return;
|
||||
} else {
|
||||
console.error(message);
|
||||
|
||||
throw new Error(message);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,27 +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 () { // TODO: use ES6
|
||||
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);
|
||||
});
|
||||
};
|
||||
100
src/FileUploader.ts
Normal file
100
src/FileUploader.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
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 setCsrfTokenWeb: any,
|
||||
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 || ""
|
||||
};
|
||||
|
||||
this.setCsrfTokenWeb(this.csrfToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -3,49 +3,49 @@
|
||||
*
|
||||
*/
|
||||
export class SAS9ApiClient {
|
||||
constructor(private serverUrl: string) {}
|
||||
constructor(private serverUrl: string) {}
|
||||
|
||||
/**
|
||||
* @returns an object containing the server URL
|
||||
*/
|
||||
public getConfig() {
|
||||
return {
|
||||
serverUrl: this.serverUrl,
|
||||
}
|
||||
}
|
||||
/**
|
||||
* returns on object containing the server URL
|
||||
*/
|
||||
public getConfig() {
|
||||
return {
|
||||
serverUrl: this.serverUrl
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates serverUrl which is not null
|
||||
* @param serverUrl - the URL of the server.
|
||||
*/
|
||||
public setConfig(serverUrl: string) {
|
||||
if (serverUrl) this.serverUrl = serverUrl
|
||||
}
|
||||
/**
|
||||
* Updates serverurl which is not null
|
||||
* @param serverUrl - the URL of the server.
|
||||
*/
|
||||
public setConfig(serverUrl: string) {
|
||||
if (serverUrl) this.serverUrl = serverUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes code on a SAS9 server.
|
||||
* @param linesOfCode - an array of lines of code to execute
|
||||
* @param serverName - the server to execute the code on
|
||||
* @param repositoryName - the repository to execute the code on
|
||||
*/
|
||||
public async executeScript(
|
||||
linesOfCode: string[], // FIXME: rename
|
||||
serverName: string,
|
||||
repositoryName: string
|
||||
) {
|
||||
const requestPayload = linesOfCode.join('\n')
|
||||
const executeScriptRequest = {
|
||||
method: 'PUT',
|
||||
headers: {Accept: 'application/json'},
|
||||
body: `command=${requestPayload}`,
|
||||
}
|
||||
// FIXME: use axios instead of fetch
|
||||
const executeScriptResponse = await fetch(
|
||||
`${this.serverUrl}/sas/servers/${serverName}/cmd?repositoryName=${repositoryName}`,
|
||||
executeScriptRequest
|
||||
).then((res) => res.text())
|
||||
// FIXME: no catch block
|
||||
/**
|
||||
* Executes code on a SAS9 server.
|
||||
* @param linesOfCode - an array of lines of code to execute
|
||||
* @param serverName - the server to execute the code on
|
||||
* @param repositoryName - the repository to execute the code on
|
||||
*/
|
||||
public async executeScript(
|
||||
linesOfCode: string[],
|
||||
serverName: string,
|
||||
repositoryName: string
|
||||
) {
|
||||
const requestPayload = linesOfCode.join("\n");
|
||||
const executeScriptRequest = {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
Accept: "application/json"
|
||||
},
|
||||
body: `command=${requestPayload}`
|
||||
};
|
||||
const executeScriptResponse = await fetch(
|
||||
`${this.serverUrl}/sas/servers/${serverName}/cmd?repositoryName=${repositoryName}`,
|
||||
executeScriptRequest
|
||||
).then((res) => res.text());
|
||||
|
||||
return executeScriptResponse
|
||||
}
|
||||
}
|
||||
return executeScriptResponse;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,15 @@
|
||||
/**
|
||||
* TODO: needs to be split into logical blocks:
|
||||
* - Folder
|
||||
* - Config
|
||||
* - Context
|
||||
* - Session
|
||||
* - Job
|
||||
* - Auth
|
||||
*/
|
||||
|
||||
import {
|
||||
isAuthorizeFormRequired,
|
||||
parseAndSubmitAuthorizeForm,
|
||||
convertToCSV,
|
||||
makeRequest,
|
||||
makeRequest
|
||||
} from "./utils";
|
||||
import * as NodeFormData from "form-data";
|
||||
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
|
||||
@@ -26,14 +19,21 @@ export class SASViyaApiClient {
|
||||
constructor(
|
||||
private serverUrl: string,
|
||||
private rootFolderName: string,
|
||||
private contextName: string,
|
||||
private setCsrfToken: (csrfToken: CsrfToken) => void,
|
||||
private rootFolderMap = new Map<string, Job[]>()
|
||||
) {
|
||||
if (!rootFolderName) {
|
||||
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 sessionManager = new SessionManager(
|
||||
this.serverUrl,
|
||||
this.contextName,
|
||||
this.setCsrfToken
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns a map containing the directory structure in the currently set root folder.
|
||||
@@ -53,7 +53,7 @@ export class SASViyaApiClient {
|
||||
public getConfig() {
|
||||
return {
|
||||
serverUrl: this.serverUrl,
|
||||
rootFolderName: this.rootFolderName,
|
||||
rootFolderName: this.rootFolderName
|
||||
};
|
||||
}
|
||||
|
||||
@@ -73,12 +73,12 @@ export class SASViyaApiClient {
|
||||
*/
|
||||
public async getAllContexts(accessToken?: string) {
|
||||
const headers: any = {
|
||||
"Content-Type": "application/json",
|
||||
"Content-Type": "application/json"
|
||||
};
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`;
|
||||
}
|
||||
const contexts = await this.request<{ items: Context[] }>(
|
||||
const { result: contexts } = await this.request<{ items: Context[] }>(
|
||||
`${this.serverUrl}/compute/contexts`,
|
||||
{ headers }
|
||||
);
|
||||
@@ -88,7 +88,7 @@ export class SASViyaApiClient {
|
||||
id: context.id,
|
||||
name: context.name,
|
||||
version: context.version,
|
||||
attributes: {},
|
||||
attributes: {}
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -98,12 +98,13 @@ export class SASViyaApiClient {
|
||||
*/
|
||||
public async getExecutableContexts(accessToken?: string) {
|
||||
const headers: any = {
|
||||
"Content-Type": "application/json",
|
||||
"Content-Type": "application/json"
|
||||
};
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`;
|
||||
}
|
||||
const contexts = await this.request<{ items: Context[] }>(
|
||||
|
||||
const { result: contexts } = await this.request<{ items: Context[] }>(
|
||||
`${this.serverUrl}/compute/contexts`,
|
||||
{ headers }
|
||||
);
|
||||
@@ -116,9 +117,7 @@ export class SASViyaApiClient {
|
||||
`test-${context.name}`,
|
||||
linesOfCode,
|
||||
context.name,
|
||||
accessToken,
|
||||
undefined,
|
||||
true
|
||||
accessToken
|
||||
).catch(() => null);
|
||||
});
|
||||
const results = await Promise.all(promises);
|
||||
@@ -140,8 +139,8 @@ export class SASViyaApiClient {
|
||||
name: contextsList[index].name,
|
||||
version: contextsList[index].version,
|
||||
attributes: {
|
||||
sysUserId,
|
||||
},
|
||||
sysUserId
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -156,14 +155,14 @@ export class SASViyaApiClient {
|
||||
*/
|
||||
public async createSession(contextName: string, accessToken?: string) {
|
||||
const headers: any = {
|
||||
"Content-Type": "application/json",
|
||||
"Content-Type": "application/json"
|
||||
};
|
||||
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`;
|
||||
}
|
||||
|
||||
const contexts = await this.request<{ items: Context[] }>(
|
||||
const { result: contexts } = await this.request<{ items: Context[] }>(
|
||||
`${this.serverUrl}/compute/contexts`,
|
||||
{ headers }
|
||||
);
|
||||
@@ -179,10 +178,10 @@ export class SASViyaApiClient {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"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`,
|
||||
createSessionRequest
|
||||
);
|
||||
@@ -200,48 +199,69 @@ export class SASViyaApiClient {
|
||||
* @param silent - optional flag to turn of logging.
|
||||
*/
|
||||
public async executeScript(
|
||||
fileName: string,
|
||||
jobName: string,
|
||||
linesOfCode: string[],
|
||||
contextName: string,
|
||||
accessToken?: string,
|
||||
sessionId = "",
|
||||
silent = false
|
||||
) {
|
||||
const headers: any = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`;
|
||||
}
|
||||
if (this.csrfToken) {
|
||||
headers[this.csrfToken.headerName] = this.csrfToken.value;
|
||||
}
|
||||
const contexts = await this.request<{ items: Context[] }>(
|
||||
`${this.serverUrl}/compute/contexts`,
|
||||
{ headers }
|
||||
);
|
||||
const executionContext =
|
||||
contexts.items && contexts.items.length
|
||||
? contexts.items.find((c: any) => c.name === contextName)
|
||||
: null;
|
||||
silent = false,
|
||||
data = null,
|
||||
debug = false
|
||||
): Promise<any> {
|
||||
silent = !debug;
|
||||
try {
|
||||
const headers: any = {
|
||||
"Content-Type": "application/json"
|
||||
};
|
||||
|
||||
if (executionContext) {
|
||||
// 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 (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`;
|
||||
}
|
||||
|
||||
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 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",
|
||||
@@ -250,12 +270,16 @@ export class SASViyaApiClient {
|
||||
name: fileName,
|
||||
description: "Powered by SASjs",
|
||||
code: linesOfCode,
|
||||
}),
|
||||
variables: jobVariables,
|
||||
arguments: jobArguments
|
||||
})
|
||||
};
|
||||
const postedJob = await this.request<Job>(
|
||||
|
||||
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(
|
||||
@@ -265,33 +289,75 @@ export class SASViyaApiClient {
|
||||
);
|
||||
}
|
||||
|
||||
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.`
|
||||
const jobStatus = await this.pollJobState(
|
||||
postedJob,
|
||||
etag,
|
||||
accessToken,
|
||||
silent
|
||||
);
|
||||
console.error("Response from server: ", JSON.stringify(contexts));
|
||||
|
||||
const { result: currentJob } = await this.request<Job>(
|
||||
`${this.serverUrl}/compute/sessions/${executionSessionId}/jobs/${postedJob.id}`,
|
||||
{ headers }
|
||||
);
|
||||
|
||||
let jobResult, log;
|
||||
|
||||
const logLink = currentJob.links.find((l) => l.rel === "log");
|
||||
|
||||
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")
|
||||
);
|
||||
}
|
||||
|
||||
if (jobStatus === "failed" || jobStatus === "error") {
|
||||
return Promise.reject({ error: currentJob.error, log: log });
|
||||
}
|
||||
const resultLink = `/compute/sessions/${executionSessionId}/filerefs/_webout/content`;
|
||||
|
||||
if (resultLink) {
|
||||
jobResult = await this.request<any>(
|
||||
`${this.serverUrl}${resultLink}`,
|
||||
{ headers },
|
||||
"text"
|
||||
).catch((e) => ({
|
||||
result: JSON.stringify(e)
|
||||
}));
|
||||
}
|
||||
|
||||
await this.sessionManager.clearSession(executionSessionId, accessToken);
|
||||
|
||||
return { result: jobResult?.result, log };
|
||||
} catch (e) {
|
||||
if (e && e.status === 404) {
|
||||
return this.executeScript(
|
||||
jobName,
|
||||
linesOfCode,
|
||||
contextName,
|
||||
accessToken,
|
||||
silent,
|
||||
data,
|
||||
debug
|
||||
);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a folder in the specified location. Either parentFolderPath or
|
||||
* parentFolderUri must be provided.
|
||||
* Creates a folder in the specified location. Either parentFolderPath or
|
||||
* parentFolderUri must be provided.
|
||||
* @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.
|
||||
* @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.
|
||||
*/
|
||||
public async createFolder(
|
||||
@@ -306,17 +372,27 @@ export class SASViyaApiClient {
|
||||
|
||||
if (!parentFolderUri && parentFolderPath) {
|
||||
parentFolderUri = await this.getFolderUri(parentFolderPath, accessToken);
|
||||
if (!parentFolderUri){
|
||||
if (!parentFolderUri) {
|
||||
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()}`;
|
||||
if (newParentFolderPath === ""){
|
||||
if (newParentFolderPath === "") {
|
||||
throw new Error("Root Folder should have been present on server");
|
||||
}
|
||||
console.log(`Creating Parent Folder:\n${newFolderName} in ${newParentFolderPath}`)
|
||||
const parentFolder = await this.createFolder(newFolderName, newParentFolderPath, undefined, accessToken)
|
||||
console.log(`Parent Folder "${newFolderName}" successfully created.`)
|
||||
console.log(
|
||||
`Creating Parent Folder:\n${newFolderName} in ${newParentFolderPath}`
|
||||
);
|
||||
const parentFolder = await this.createFolder(
|
||||
newFolderName,
|
||||
newParentFolderPath,
|
||||
undefined,
|
||||
accessToken
|
||||
);
|
||||
console.log(`Parent Folder "${newFolderName}" successfully created.`);
|
||||
parentFolderUri = `/folders/folders/${parentFolder.id}`;
|
||||
}
|
||||
}
|
||||
@@ -325,8 +401,8 @@ export class SASViyaApiClient {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
name: folderName,
|
||||
type: "folder",
|
||||
}),
|
||||
type: "folder"
|
||||
})
|
||||
};
|
||||
|
||||
createFolderRequest.headers = { "Content-Type": "application/json" };
|
||||
@@ -334,7 +410,7 @@ export class SASViyaApiClient {
|
||||
createFolderRequest.headers.Authorization = `Bearer ${accessToken}`;
|
||||
}
|
||||
|
||||
const createFolderResponse = await this.request<Folder>(
|
||||
const { result: createFolderResponse } = await this.request<Folder>(
|
||||
`${this.serverUrl}/folders/folders?parentFolderUri=${parentFolderUri}`,
|
||||
createFolderRequest
|
||||
);
|
||||
@@ -360,7 +436,9 @@ export class SASViyaApiClient {
|
||||
accessToken?: string
|
||||
) {
|
||||
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) {
|
||||
@@ -371,26 +449,26 @@ export class SASViyaApiClient {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/vnd.sas.job.definition+json",
|
||||
Accept: "application/vnd.sas.job.definition+json",
|
||||
Accept: "application/vnd.sas.job.definition+json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: jobName,
|
||||
parameters:[
|
||||
parameters: [
|
||||
{
|
||||
"name":"_addjesbeginendmacros",
|
||||
"type":"CHARACTER",
|
||||
"defaultValue":"false"
|
||||
name: "_addjesbeginendmacros",
|
||||
type: "CHARACTER",
|
||||
defaultValue: "false"
|
||||
}
|
||||
],
|
||||
type: "Compute",
|
||||
code,
|
||||
}),
|
||||
code
|
||||
})
|
||||
};
|
||||
|
||||
if (accessToken) {
|
||||
createJobDefinitionRequest!.headers = {
|
||||
...createJobDefinitionRequest.headers,
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
};
|
||||
}
|
||||
|
||||
@@ -409,7 +487,7 @@ export class SASViyaApiClient {
|
||||
|
||||
const authCode = await fetch(authUrl, {
|
||||
referrerPolicy: "same-origin",
|
||||
credentials: "include",
|
||||
credentials: "include"
|
||||
})
|
||||
.then((response) => response.text())
|
||||
.then(async (response) => {
|
||||
@@ -465,7 +543,7 @@ export class SASViyaApiClient {
|
||||
token = Buffer.from(clientId + ":" + clientSecret).toString("base64");
|
||||
}
|
||||
const headers = {
|
||||
Authorization: "Basic " + token,
|
||||
Authorization: "Basic " + token
|
||||
};
|
||||
|
||||
let formData;
|
||||
@@ -484,7 +562,7 @@ export class SASViyaApiClient {
|
||||
credentials: "include",
|
||||
headers,
|
||||
body: formData as any,
|
||||
referrerPolicy: "same-origin",
|
||||
referrerPolicy: "same-origin"
|
||||
}).then((res) => res.json());
|
||||
|
||||
return authResponse;
|
||||
@@ -509,7 +587,7 @@ export class SASViyaApiClient {
|
||||
token = Buffer.from(clientId + ":" + clientSecret).toString("base64");
|
||||
}
|
||||
const headers = {
|
||||
Authorization: "Basic " + token,
|
||||
Authorization: "Basic " + token
|
||||
};
|
||||
|
||||
let formData;
|
||||
@@ -528,7 +606,7 @@ export class SASViyaApiClient {
|
||||
credentials: "include",
|
||||
headers,
|
||||
body: formData as any,
|
||||
referrerPolicy: "same-origin",
|
||||
referrerPolicy: "same-origin"
|
||||
}).then((res) => res.json());
|
||||
|
||||
return authResponse;
|
||||
@@ -548,12 +626,90 @@ export class SASViyaApiClient {
|
||||
const deleteResponse = await this.request(url, {
|
||||
method: "DELETE",
|
||||
credentials: "include",
|
||||
headers,
|
||||
headers
|
||||
});
|
||||
|
||||
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);
|
||||
if (!jobToExecute) {
|
||||
throw new Error("Job was not found.");
|
||||
}
|
||||
|
||||
let code = jobToExecute?.code;
|
||||
if (!code) {
|
||||
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
|
||||
);
|
||||
|
||||
code = jobDefinition.code;
|
||||
|
||||
// Add code to existing job definition
|
||||
jobToExecute.code = code;
|
||||
}
|
||||
const linesToExecute = 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
|
||||
* @param sasJob - the relative path to the job.
|
||||
@@ -589,6 +745,7 @@ export class SASViyaApiClient {
|
||||
if (data && Object.keys(data).length) {
|
||||
files = await this.uploadTables(data, accessToken);
|
||||
}
|
||||
|
||||
const jobName = path.basename(sasJob);
|
||||
const jobFolder = sasJob.replace(`/${jobName}`, "");
|
||||
const allJobsInFolder = this.rootFolderMap.get(jobFolder.replace("/", ""));
|
||||
@@ -598,14 +755,14 @@ export class SASViyaApiClient {
|
||||
(l) => l.rel === "getResource"
|
||||
)?.href;
|
||||
const requestInfo: any = {
|
||||
method: "GET",
|
||||
method: "GET"
|
||||
};
|
||||
const headers: any = { "Content-Type": "application/json" };
|
||||
if (!!accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`;
|
||||
}
|
||||
requestInfo.headers = headers;
|
||||
const jobDefinition = await this.request<Job>(
|
||||
const { result: jobDefinition } = await this.request<Job>(
|
||||
`${this.serverUrl}${jobDefinitionLink}`,
|
||||
requestInfo
|
||||
);
|
||||
@@ -618,19 +775,19 @@ export class SASViyaApiClient {
|
||||
_OMITJSONLOG: true,
|
||||
_OMITSESSIONRESULTS: true,
|
||||
_OMITTEXTLISTING: true,
|
||||
_OMITTEXTLOG: true,
|
||||
_OMITTEXTLOG: true
|
||||
};
|
||||
|
||||
if (debug) {
|
||||
jobArguments["_omittextlog"] = "false";
|
||||
jobArguments["_omitsessionresults"] = "false";
|
||||
jobArguments["_debug"] = 131;
|
||||
jobArguments["_OMITTEXTLOG"] = "false";
|
||||
jobArguments["_OMITSESSIONRESULTS"] = "false";
|
||||
jobArguments["_DEBUG"] = 131;
|
||||
}
|
||||
|
||||
files.forEach((fileInfo, index) => {
|
||||
jobArguments[
|
||||
`_webin_fileuri${index + 1}`
|
||||
] = `/files/files/${fileInfo.id}`;
|
||||
] = `/files/files/${fileInfo.file.id}`;
|
||||
jobArguments[`_webin_name${index + 1}`] = fileInfo.tableName;
|
||||
});
|
||||
|
||||
@@ -641,28 +798,48 @@ export class SASViyaApiClient {
|
||||
name: `exec-${jobName}`,
|
||||
description: "Powered by SASjs",
|
||||
jobDefinition,
|
||||
arguments: jobArguments,
|
||||
}),
|
||||
arguments: jobArguments
|
||||
})
|
||||
};
|
||||
const postedJob = await this.request<Job>(
|
||||
const { result: postedJob, etag } = await this.request<Job>(
|
||||
`${this.serverUrl}/jobExecution/jobs?_action=wait`,
|
||||
postJobRequest
|
||||
);
|
||||
const jobStatus = await this.pollJobState(postedJob, accessToken, true);
|
||||
const currentJob = await this.request<Job>(
|
||||
const jobStatus = await this.pollJobState(
|
||||
postedJob,
|
||||
etag,
|
||||
accessToken,
|
||||
true
|
||||
);
|
||||
const { result: currentJob } = await this.request<Job>(
|
||||
`${this.serverUrl}/jobExecution/jobs/${postedJob.id}`,
|
||||
{ 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 {
|
||||
throw new Error(
|
||||
`The job ${sasJob} was not found at the location ${this.rootFolderName}`
|
||||
@@ -674,19 +851,19 @@ export class SASViyaApiClient {
|
||||
const allItems = new Map<string, Job[]>();
|
||||
const url = "/folders/folders/@item?path=" + this.rootFolderName;
|
||||
const requestInfo: any = {
|
||||
method: "GET",
|
||||
method: "GET"
|
||||
};
|
||||
if (accessToken) {
|
||||
requestInfo.headers = { Authorization: `Bearer ${accessToken}` };
|
||||
}
|
||||
const folder = await this.request<Folder>(
|
||||
const { result: folder } = await this.request<Folder>(
|
||||
`${this.serverUrl}${url}`,
|
||||
requestInfo
|
||||
);
|
||||
if (!folder){
|
||||
if (!folder) {
|
||||
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`,
|
||||
requestInfo
|
||||
);
|
||||
@@ -701,7 +878,7 @@ export class SASViyaApiClient {
|
||||
this.rootFolderName +
|
||||
"/" +
|
||||
member.name;
|
||||
const memberDetail = await this.request<Folder>(
|
||||
const { result: memberDetail } = await this.request<Folder>(
|
||||
`${this.serverUrl}${subFolderUrl}`,
|
||||
requestInfo
|
||||
);
|
||||
@@ -710,7 +887,7 @@ export class SASViyaApiClient {
|
||||
(l: any) => l.rel === "members"
|
||||
);
|
||||
|
||||
const memberContents = await this.request<{ items: any[] }>(
|
||||
const { result: memberContents } = await this.request<{ items: any[] }>(
|
||||
`${this.serverUrl}${membersLink!.href}`,
|
||||
requestInfo
|
||||
);
|
||||
@@ -726,34 +903,59 @@ export class SASViyaApiClient {
|
||||
private async populateRootFolder(accessToken?: string) {
|
||||
const url = "/folders/folders/@item?path=" + this.rootFolderName;
|
||||
const requestInfo: RequestInit = {
|
||||
method: "GET",
|
||||
method: "GET"
|
||||
};
|
||||
if (accessToken) {
|
||||
requestInfo.headers = { Authorization: `Bearer ${accessToken}` };
|
||||
}
|
||||
let error;
|
||||
const rootFolder = await this.request<Folder>(
|
||||
`${this.serverUrl}${url}`,
|
||||
requestInfo
|
||||
).catch(() => null);
|
||||
);
|
||||
|
||||
this.rootFolder = rootFolder;
|
||||
this.rootFolder = rootFolder?.result || null;
|
||||
if (error) {
|
||||
throw new Error(JSON.stringify(error));
|
||||
}
|
||||
}
|
||||
|
||||
private async pollJobState(
|
||||
postedJob: any,
|
||||
etag: string | null,
|
||||
accessToken?: string,
|
||||
silent = false
|
||||
) {
|
||||
const MAX_POLL_COUNT = 1000;
|
||||
const POLL_INTERVAL = 100;
|
||||
let postedJobState = "";
|
||||
let pollCount = 0;
|
||||
const headers: any = {
|
||||
"Content-Type": "application/json",
|
||||
"If-None-Match": etag
|
||||
};
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`;
|
||||
}
|
||||
const stateLink = postedJob.links.find((l: any) => l.rel === "state");
|
||||
return new Promise((resolve, _) => {
|
||||
if (!stateLink) {
|
||||
Promise.reject("Job state link was not found.");
|
||||
}
|
||||
|
||||
const { result: state } = await this.request<string>(
|
||||
`${this.serverUrl}${stateLink.href}?_action=wait&wait=30`,
|
||||
{
|
||||
headers
|
||||
},
|
||||
"text"
|
||||
);
|
||||
|
||||
const currentState = state.trim();
|
||||
if (currentState === "completed") {
|
||||
return Promise.resolve(currentState);
|
||||
}
|
||||
|
||||
return new Promise(async (resolve, _) => {
|
||||
const interval = setInterval(async () => {
|
||||
if (
|
||||
postedJobState === "running" ||
|
||||
@@ -764,10 +966,10 @@ export class SASViyaApiClient {
|
||||
if (!silent) {
|
||||
console.log("Polling job status... \n");
|
||||
}
|
||||
const jobState = await this.request<string>(
|
||||
`${this.serverUrl}${stateLink.href}?wait=30`,
|
||||
const { result: jobState } = await this.request<string>(
|
||||
`${this.serverUrl}${stateLink.href}?_action=wait&wait=30`,
|
||||
{
|
||||
headers,
|
||||
headers
|
||||
},
|
||||
"text"
|
||||
);
|
||||
@@ -777,7 +979,7 @@ export class SASViyaApiClient {
|
||||
console.log(`Current state: ${postedJobState}\n`);
|
||||
}
|
||||
pollCount++;
|
||||
if (pollCount >= 100) {
|
||||
if (pollCount >= MAX_POLL_COUNT) {
|
||||
resolve(postedJobState);
|
||||
}
|
||||
}
|
||||
@@ -785,14 +987,57 @@ export class SASViyaApiClient {
|
||||
clearInterval(interval);
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async uploadTables(data: any, accessToken?: string) {
|
||||
const uploadedFiles = [];
|
||||
const headers: any = {
|
||||
"Content-Type": "application/json",
|
||||
"Content-Type": "application/json"
|
||||
};
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`;
|
||||
@@ -809,36 +1054,43 @@ export class SASViyaApiClient {
|
||||
const createFileRequest = {
|
||||
method: "POST",
|
||||
body: csv,
|
||||
headers,
|
||||
headers
|
||||
};
|
||||
|
||||
const file = await this.request<any>(
|
||||
const uploadResponse = await this.request<any>(
|
||||
`${this.serverUrl}/files/files#rawUpload`,
|
||||
createFileRequest
|
||||
);
|
||||
|
||||
uploadedFiles.push({ tableName, file });
|
||||
uploadedFiles.push({ tableName, file: uploadResponse.result });
|
||||
}
|
||||
return uploadedFiles;
|
||||
}
|
||||
|
||||
private async getFolderUri(folderPath: string, accessToken?: string) {
|
||||
const url = "/folders/folders/@item?path=" + folderPath;
|
||||
const requestInfo: any = {
|
||||
method: "GET",
|
||||
};
|
||||
if (accessToken) {
|
||||
requestInfo.headers = { Authorization: `Bearer ${accessToken}` };
|
||||
}
|
||||
const folder = await this.request<Folder>(
|
||||
`${this.serverUrl}${url}`,
|
||||
requestInfo
|
||||
);
|
||||
if (!folder)
|
||||
return undefined;
|
||||
return `/folders/folders/${folder.id}`;
|
||||
const requestInfo: any = {
|
||||
method: "GET"
|
||||
};
|
||||
if (accessToken) {
|
||||
requestInfo.headers = { Authorization: `Bearer ${accessToken}` };
|
||||
}
|
||||
const { result: folder } = await this.request<Folder>(
|
||||
`${this.serverUrl}${url}`,
|
||||
requestInfo
|
||||
).catch((err) => {
|
||||
return { result: null };
|
||||
});
|
||||
|
||||
if (!folder) return undefined;
|
||||
return `/folders/folders/${folder.id}`;
|
||||
}
|
||||
|
||||
setCsrfTokenLocal = (csrfToken: CsrfToken) => {
|
||||
this.csrfToken = csrfToken;
|
||||
this.setCsrfToken(csrfToken);
|
||||
};
|
||||
|
||||
private async request<T>(
|
||||
url: string,
|
||||
options: RequestInit,
|
||||
@@ -847,13 +1099,13 @@ export class SASViyaApiClient {
|
||||
if (this.csrfToken) {
|
||||
options.headers = {
|
||||
...options.headers,
|
||||
[this.csrfToken.headerName]: this.csrfToken.value,
|
||||
[this.csrfToken.headerName]: this.csrfToken.value
|
||||
};
|
||||
}
|
||||
return await makeRequest<T>(
|
||||
url,
|
||||
options,
|
||||
(csrfToken) => (this.csrfToken = csrfToken),
|
||||
this.setCsrfTokenLocal,
|
||||
contentType
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,7 @@ import SASjs from "./index";
|
||||
|
||||
const adapter = new SASjs();
|
||||
|
||||
// FIXME: adapter doesn't have 'parseSAS9SourceCode' and 'parseGeneratedCode'
|
||||
it("should parse SAS9 source code", async done => {
|
||||
it("should parse SAS9 source code", async (done) => {
|
||||
expect(sampleResponse).toBeTruthy();
|
||||
const parsedSourceCode = (adapter as any).parseSAS9SourceCode(sampleResponse);
|
||||
expect(parsedSourceCode).toBeTruthy();
|
||||
@@ -17,7 +16,7 @@ it("should parse SAS9 source code", async done => {
|
||||
done();
|
||||
});
|
||||
|
||||
it("should parse generated code", async done => {
|
||||
it("should parse generated code", async (done) => {
|
||||
expect(sampleResponse).toBeTruthy();
|
||||
const parsedGeneratedCode = (adapter as any).parseGeneratedCode(
|
||||
sampleResponse
|
||||
|
||||
698
src/SASjs.ts
698
src/SASjs.ts
File diff suppressed because it is too large
Load Diff
170
src/SessionManager.ts
Normal file
170
src/SessionManager.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
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();
|
||||
const secondsSinceSessionCreation =
|
||||
(new Date().getTime() - new Date(session!.creationTimeStamp).getTime()) /
|
||||
1000;
|
||||
if (
|
||||
secondsSinceSessionCreation >= session!.attributes.sessionInactiveTimeout
|
||||
) {
|
||||
await this.createSessions(accessToken);
|
||||
const freshSession = this.sessions.pop();
|
||||
return freshSession;
|
||||
}
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
12
src/index.ts
12
src/index.ts
@@ -1,7 +1,5 @@
|
||||
import SASjs from './SASjs'
|
||||
|
||||
export * from './types'
|
||||
export * from './SASViyaApiClient'
|
||||
export * from './SAS9ApiClient'
|
||||
|
||||
export default SASjs
|
||||
import SASjs from "./SASjs";
|
||||
export * from "./types";
|
||||
export * from "./SASViyaApiClient";
|
||||
export * from "./SAS9ApiClient";
|
||||
export default SASjs;
|
||||
|
||||
@@ -3,4 +3,4 @@ export interface Context {
|
||||
id: string;
|
||||
createdBy: string;
|
||||
version: number;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface CsrfToken {
|
||||
headerName: string;
|
||||
value: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,4 +4,4 @@ export interface Folder {
|
||||
id: string;
|
||||
uri: string;
|
||||
links: Link[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ export interface Job {
|
||||
name: string;
|
||||
uri: string;
|
||||
createdBy: string;
|
||||
code?: string;
|
||||
links: Link[];
|
||||
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;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user