1
0
mirror of https://github.com/sasjs/adapter.git synced 2026-01-13 15:10:06 +00:00

Compare commits

..

2 Commits

Author SHA1 Message Date
9598c11f42 Added suggestions 2020-07-16 15:02:40 -04:00
334a849caa Added suggestions 2020-07-16 15:02:17 -04:00
127 changed files with 3061 additions and 17546 deletions

View File

@@ -21,11 +21,7 @@ jobs:
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
- name: Install Dependencies - run: npm ci
run: npm ci - run: npm run package:lib
- name: Check code style
run: npm run lint
- name: Build Package
run: npm run package:lib
env: env:
CI: true CI: true

View File

@@ -6,7 +6,7 @@ name: SASjs Build and Publish
on: on:
push: push:
branches: branches:
- master - main
jobs: jobs:
build: build:
@@ -16,8 +16,6 @@ jobs:
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Install Dependencies - name: Install Dependencies
run: npm ci run: npm ci
- name: Check code style
run: npm run lint
- name: Build Project - name: Build Project
run: npm run build run: npm run build
- name: Semantic Release - name: Semantic Release

View File

@@ -1,6 +0,0 @@
{
"trailingComma": "none",
"tabWidth": 2,
"semi": true,
"singleQuote": false
}

View File

@@ -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/sasjs/core/main/all.sas"; filename mc url "https://raw.githubusercontent.com/macropeople/macrocore/main/mc_all.sas?_=1";
%inc mc; %inc mc;
filename ft15f001 temp; filename ft15f001 temp;
parmcards4; parmcards4;
@@ -40,13 +40,18 @@ parmcards4;
# Viya # Viya
``` ```
filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas"; filename mc url "https://raw.githubusercontent.com/macropeople/macrocore/main/mc_all.sas";
%inc mc; %inc mc;
filename ft15f001 temp; filename ft15f001 temp;
parmcards4; parmcards4;
%webout(FETCH)
%webout(OPEN) %webout(OPEN)
%global sasjs_tables;
%let sasjs_tables=&sasjs_tables;
%put &=sasjs_tables;
%let sasjs_tables=&sasjs_tables;
%macro x(); %macro x();
%global sasjs_tables;
%do i=1 %to %sysfunc(countw(&sasjs_tables)); %do i=1 %to %sysfunc(countw(&sasjs_tables));
%let table=%scan(&sasjs_tables,&i); %let table=%scan(&sasjs_tables,&i);
%webout(OBJ,&table) %webout(OBJ,&table)
@@ -55,11 +60,13 @@ parmcards4;
%x() %x()
%webout(CLOSE) %webout(CLOSE)
;;;; ;;;;
%mp_createwebservice(path=/Public/app/common,name=sendObj) %mv_createwebservice(path=/Public/app/common,name=sendObj)
filename ft15f001 temp; filename ft15f001 temp;
parmcards4; parmcards4;
%webout(FETCH)
%webout(OPEN) %webout(OPEN)
%global sasjs_tables;
%let sasjs_tables=&sasjs_tables;
%put &=sasjs_tables;
%macro x(); %macro x();
%do i=1 %to %sysfunc(countw(&sasjs_tables)); %do i=1 %to %sysfunc(countw(&sasjs_tables));
%let table=%scan(&sasjs_tables,&i); %let table=%scan(&sasjs_tables,&i);
@@ -69,15 +76,7 @@ parmcards4;
%x() %x()
%webout(CLOSE) %webout(CLOSE)
;;;; ;;;;
%mp_createwebservice(path=/Public/app/common,name=sendArr) %mv_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`. The above services will return anything you send. To run the tests simply launch `npm run cypress`.

View File

@@ -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. 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/sasjs/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/macropeople/react-seed-app) which provides some boilerplate.
For more information on building web apps with SAS, check out [sasjs.io](https://sasjs.io) For more information on building web apps with SAS, check out [sasjs.io](https://sasjs.io)
## None of this makes sense. How do I build an app with it? ## None of this makes sense. How do I build an app with it?
Ok ok. Deploy this [example.html](https://raw.githubusercontent.com/sasjs/adapter/master/example.html) file to your web server, and update `servertype` to `SAS9` or `SASVIYA` depending on your backend. Ok ok. Deploy this [example.html](https://github.com/sasjs/adapter/blob/main/example.html) file to your web server, and update `servertype` to `SAS9` or `SASVIYA` depending on your backend.
The backend part can be deployed as follows: The backend part can be deployed as follows:
@@ -43,6 +43,6 @@ You now have a simple web app with a backend service!
# More resources # More resources
For more information specific to this adapter you can check out this [user guide](https://sasjs.io/sasjs-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/sasjs-adapter/) or the [technical](http://adapter.sasjs.io/) documentation.
For more information on building web apps in general, check out these [resources](https://sasjs.io/training/resources/) or contact the [author](https://www.linkedin.com/in/allanbowe/) directly. For more information on building web apps in general, check out these [resources](https://sasjs.io/training/resources/) or contact the [author](https://www.linkedin.com/in/allanbowe/) directly.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,109 +1,114 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<script src="https://cdn.jsdelivr.net/combine/npm/chart.js@2.9.3,npm/jquery@3.5.1,npm/@sasjs/adapter@1"></script> <meta charset='utf-8' http-equiv='X-UA-Compatible' content='IE=edge' />
<script> <link rel='stylesheet' href='https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css' integrity='sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh' crossorigin='anonymous'>
var sasJs = new SASjs.default({ <script src='https://cdn.jsdelivr.net/combine/npm/chart.js@2.9.3,npm/jquery@3.5.1,npm/@sasjs/adapter@1'></script>
appLoc: "/Public/app/readme" <script>
,serverType:"SAS9" const sasJs = new SASjs.default({
,debug: false appLoc: '/Products/demo/readme',
}); serverType:'SAS9',
function initSasJs() { debug: 'false'
$('#loading-spinner').show() })
// instantiate sasjs with options such as backend app location
// login (it's also possible to set an autologin when making requests) const initSasJs = () => {
sasJs.logIn( $('#loading-spinner').show()
$('#username')[0].value
,$('#password')[0].value // instantiate sasJs with options such as backend app location
).then((response) => { // login (it's also possible to set an auto login when making requests)
if (response.isLoggedIn === true) { sasJs.logIn($('#username')[0].value, $('#password')[0].value)
$('#loading-spinner').hide() .then((response) => {
$('.login').hide() if (response.isLoggedIn === true) {
$('#getdata').show() $('#loading-spinner').hide()
$('#cars').show() $('.login').hide()
} $('#getDataBtn').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
</script> const getData = () => {
<meta charset="utf-8" http-equiv="X-UA-Compatible" content="IE=edge" /> $('#loading-spinner').show()
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous"> $('#myChart').remove()
</head> $('#chart-container').append("<canvas id='myChart' style='display: none'></canvas>")
<body>
<div class="container-fluid" style="text-align: center; margin-top: 10px;"> const type = $('#cars')[0].options[$('#cars')[0].selectedIndex].value
<div class="row">
<div class="col-lg-5 col-md-7 col-sm-10 mx-auto mx-auto"> // request data from an endpoint under your appLoc
<h1>Demo Seed App for <span class="code">SASjs</span></h1> // send data as an array of objects - each object is one row
<div class="login" id="login-form"> sasJs.request('/common/getdata', {fromjs: [{ type: type }]})
<div class="form-group"> .then((response) => {
<input class="form-control" type="text" id="username" placeholder="Enter username" /> $('#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>
</div> </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>
</div> <div id='chart-container' style='height: 65vh; width: 100%; position: relative; margin: auto'>
</div> <canvas id='myChart' style='display: none'></canvas>
<div id="chart-container" style="height: 65vh; width: 100%; position: relative; margin: auto;"> </div>
<canvas id="myChart" style="display: none;"></canvas> </body>
</div> </head>
</body>
</head>

1714
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,8 +5,8 @@
"build": "rimraf build && webpack", "build": "rimraf build && webpack",
"package:lib": "npm run build && cp ./package.json build && cd build && npm version \"5.0.0\" && npm pack", "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", "publish:lib": "npm run build && cd build && npm publish",
"lint:fix": "npx prettier --write 'src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}'", "format": "prettier --write \"src/**/*.ts\" \"src/**/*.js\"",
"lint": "npx prettier --check 'src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}'", "lint": "tslint -p tsconfig.json",
"test": "jest", "test": "jest",
"prepublishOnly": "cp -r ./build/* . && rm -rf ./build", "prepublishOnly": "cp -r ./build/* . && rm -rf ./build",
"postpublish": "git clean -fd", "postpublish": "git clean -fd",
@@ -22,6 +22,9 @@
{ {
"pkgRoot": "/build" "pkgRoot": "/build"
} }
],
"branches": [
"main"
] ]
}, },
"keywords": [ "keywords": [
@@ -37,22 +40,23 @@
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"@types/isomorphic-fetch": "0.0.35", "@types/isomorphic-fetch": "0.0.35",
"@types/jest": "^26.0.10", "@types/jest": "^26.0.4",
"cp": "^0.2.0", "cp": "^0.2.0",
"jest": "^25.5.4", "jest": "^25.5.4",
"path": "^0.12.7", "path": "^0.12.7",
"prettier": "^2.0.5",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"semantic-release": "^17.1.1", "semantic-release": "^17.1.1",
"ts-jest": "^25.5.1", "ts-jest": "^25.5.1",
"ts-loader": "^8.0.3", "ts-loader": "^7.0.5",
"tslint": "^6.1.3", "tslint": "^6.1.2",
"tslint-config-prettier": "^1.18.0", "tslint-config-prettier": "^1.18.0",
"typedoc": "^0.17.8", "typedoc": "^0.17.8",
"typedoc-neo-theme": "^1.0.9", "typedoc-neo-theme": "^1.0.9",
"typedoc-plugin-external-module-name": "^4.0.3", "typedoc-plugin-external-module-name": "^4.0.3",
"typescript": "^3.9.7", "typescript": "^3.9.6",
"uglifyjs-webpack-plugin": "^2.2.0", "uglifyjs-webpack-plugin": "^2.2.0",
"webpack": "^4.44.1", "webpack": "^4.43.0",
"webpack-cli": "^3.3.12" "webpack-cli": "^3.3.12"
}, },
"main": "index.js", "main": "index.js",

View File

@@ -1,6 +0,0 @@
{
"trailingComma": "none",
"tabWidth": 2,
"semi": true,
"singleQuote": false
}

View File

@@ -1,6 +1,6 @@
{ {
"name": "@sasjs/tests", "name": "sasjs-tests",
"version": "1.0.0", "version": "0.1.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@@ -1356,81 +1356,11 @@
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz",
"integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==" "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw=="
}, },
"@sasjs/adapter": {
"version": "1.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": { "@sheerun/mutationobserver-shim": {
"version": "0.3.3", "version": "0.3.3",
"resolved": "https://registry.npmjs.org/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.3.tgz", "resolved": "https://registry.npmjs.org/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.3.tgz",
"integrity": "sha512-DetpxZw1fzPD5xUBrIAoplLChO2VB8DlL5Gg+I1IR9b2wPqYIca2WSUxL5g1vLeR4MsQq1NeWriXAVffV+U1Fw==" "integrity": "sha512-DetpxZw1fzPD5xUBrIAoplLChO2VB8DlL5Gg+I1IR9b2wPqYIca2WSUxL5g1vLeR4MsQq1NeWriXAVffV+U1Fw=="
}, },
"@stardust-ui/react-component-event-listener": {
"version": "0.38.0",
"resolved": "https://registry.npmjs.org/@stardust-ui/react-component-event-listener/-/react-component-event-listener-0.38.0.tgz",
"integrity": "sha512-sIP/e0dyOrrlb8K7KWumfMxj/gAifswTBC4o68Aa+C/GA73ccRp/6W1VlHvF/dlOR4KLsA+5SKnhjH36xzPsWg==",
"requires": {
"@babel/runtime": "^7.1.2",
"prop-types": "^15.7.2"
}
},
"@stardust-ui/react-component-ref": {
"version": "0.38.0",
"resolved": "https://registry.npmjs.org/@stardust-ui/react-component-ref/-/react-component-ref-0.38.0.tgz",
"integrity": "sha512-xjs6WnvJVueSIXMWw0C3oWIgAPpcD03qw43oGOjUXqFktvpNkB73JoKIhS4sCrtQxBdct75qqr4ZL6JiyPcESw==",
"requires": {
"@babel/runtime": "^7.1.2",
"prop-types": "^15.7.2",
"react-is": "^16.6.3"
}
},
"@svgr/babel-plugin-add-jsx-attribute": { "@svgr/babel-plugin-add-jsx-attribute": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-4.2.0.tgz", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-4.2.0.tgz",
@@ -1906,14 +1836,6 @@
"@types/react": "*" "@types/react": "*"
} }
}, },
"@types/react-highlight.js": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/react-highlight.js/-/react-highlight.js-1.0.0.tgz",
"integrity": "sha512-5VXEuo2O9L66y/2GDQSGFTggQkpOvDc/p2ma1KHadu7o/H720HK3Fr83epd4wtQky7B/RoCPat0SKyhlhiUo7A==",
"requires": {
"@types/react": "*"
}
},
"@types/react-router": { "@types/react-router": {
"version": "5.1.8", "version": "5.1.8",
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.8.tgz", "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.8.tgz",
@@ -3832,11 +3754,6 @@
"shallow-clone": "^0.1.2" "shallow-clone": "^0.1.2"
} }
}, },
"clsx": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz",
"integrity": "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA=="
},
"co": { "co": {
"version": "4.6.0", "version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@@ -4178,15 +4095,6 @@
"sha.js": "^2.4.8" "sha.js": "^2.4.8"
} }
}, },
"create-react-context": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/create-react-context/-/create-react-context-0.3.0.tgz",
"integrity": "sha512-dNldIoSuNSvlTJ7slIKC/ZFGKexBMBrrcc+TTe1NdmROnaASuLPvqpwj9v4XS4uXZ8+YPu0sNmShX2rXI5LNsw==",
"requires": {
"gud": "^1.0.0",
"warning": "^4.0.3"
}
},
"cross-spawn": { "cross-spawn": {
"version": "6.0.5", "version": "6.0.5",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
@@ -5004,21 +4912,11 @@
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
}, },
"encoding": { "encoding": {
"version": "0.1.13", "version": "0.1.12",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz",
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=",
"requires": { "requires": {
"iconv-lite": "^0.6.2" "iconv-lite": "~0.4.13"
},
"dependencies": {
"iconv-lite": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz",
"integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
}
}
} }
}, },
"end-of-stream": { "end-of-stream": {
@@ -5711,11 +5609,6 @@
"strip-eof": "^1.0.0" "strip-eof": "^1.0.0"
} }
}, },
"exenv": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz",
"integrity": "sha1-KueOhdmJQVhnCwPUe+wfA72Ru50="
},
"exit": { "exit": {
"version": "0.1.2", "version": "0.1.2",
"resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
@@ -6585,11 +6478,6 @@
"resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz",
"integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=" "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE="
}, },
"gud": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz",
"integrity": "sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw=="
},
"gzip-size": { "gzip-size": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz", "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz",
@@ -6734,11 +6622,6 @@
"resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz",
"integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==" "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ=="
}, },
"highlight.js": {
"version": "9.18.3",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.18.3.tgz",
"integrity": "sha512-zBZAmhSupHIl5sITeMqIJnYCDfAEc3Gdkqj65wC1lpI468MMQeeQkhcIAvk+RylAkxrCcI9xy9piHiXeQ1BdzQ=="
},
"history": { "history": {
"version": "4.10.1", "version": "4.10.1",
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
@@ -8091,11 +7974,6 @@
} }
} }
}, },
"jquery": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz",
"integrity": "sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg=="
},
"js-base64": { "js-base64": {
"version": "2.6.2", "version": "2.6.2",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.2.tgz", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.2.tgz",
@@ -8245,11 +8123,6 @@
"object.assign": "^4.1.0" "object.assign": "^4.1.0"
} }
}, },
"keyboard-key": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/keyboard-key/-/keyboard-key-1.1.0.tgz",
"integrity": "sha512-qkBzPTi3rlAKvX7k0/ub44sqOfXeLc/jcnGGmj5c7BJpU8eDrEVPyhCvNYAaoubbsLm9uGWwQJO1ytQK1a9/dQ=="
},
"killable": { "killable": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
@@ -9014,11 +8887,6 @@
"minimist": "^1.2.5" "minimist": "^1.2.5"
} }
}, },
"moment": {
"version": "2.27.0",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.27.0.tgz",
"integrity": "sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ=="
},
"move-concurrently": { "move-concurrently": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
@@ -10012,11 +9880,6 @@
"ts-pnp": "^1.1.6" "ts-pnp": "^1.1.6"
} }
}, },
"popper.js": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
"integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ=="
},
"portfinder": { "portfinder": {
"version": "1.0.26", "version": "1.0.26",
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.26.tgz", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.26.tgz",
@@ -11458,34 +11321,11 @@
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.7.tgz", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.7.tgz",
"integrity": "sha512-TAv1KJFh3RhqxNvhzxj6LeT5NWklP6rDr2a0jaTfsZ5wSZWHOGeqQyejUp3xxLfPt2UpyJEcVQB/zyPcmonNFA==" "integrity": "sha512-TAv1KJFh3RhqxNvhzxj6LeT5NWklP6rDr2a0jaTfsZ5wSZWHOGeqQyejUp3xxLfPt2UpyJEcVQB/zyPcmonNFA=="
}, },
"react-highlight.js": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/react-highlight.js/-/react-highlight.js-1.0.7.tgz",
"integrity": "sha512-OVPKnV0ZvU+V//HExwbV8M9CWy49Eo/9y9pBN2OsNWUFPN6dE4YZBLmJW/5sM2DxI5v/QQLyxOnTnSSfGCP+9Q==",
"requires": {
"highlight.js": "^9.3.0",
"prop-types": "^15.6.0"
}
},
"react-is": { "react-is": {
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
}, },
"react-popper": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.7.tgz",
"integrity": "sha512-nmqYTx7QVjCm3WUZLeuOomna138R1luC4EqkW3hxJUrAe+3eNz3oFCLYdnPwILfn0mX1Ew2c3wctrjlUMYYUww==",
"requires": {
"@babel/runtime": "^7.1.2",
"create-react-context": "^0.3.0",
"deep-equal": "^1.1.1",
"popper.js": "^1.14.4",
"prop-types": "^15.6.1",
"typed-styles": "^0.0.7",
"warning": "^4.0.2"
}
},
"react-router": { "react-router": {
"version": "5.2.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz",
@@ -12110,6 +11950,27 @@
"resolved": "https://registry.npmjs.org/sanitize.css/-/sanitize.css-10.0.0.tgz", "resolved": "https://registry.npmjs.org/sanitize.css/-/sanitize.css-10.0.0.tgz",
"integrity": "sha512-vTxrZz4dX5W86M6oVWVdOVe72ZiPs41Oi7Z6Km4W5Turyz28mrXSJhhEBZoRtzJWIv3833WKVwLSDWWkEfupMg==" "integrity": "sha512-vTxrZz4dX5W86M6oVWVdOVe72ZiPs41Oi7Z6Km4W5Turyz28mrXSJhhEBZoRtzJWIv3833WKVwLSDWWkEfupMg=="
}, },
"sasjs": {
"version": "file:../build/sasjs-5.0.0.tgz",
"integrity": "sha512-8Ez2iS8BKzu2GG1Cwf/pe5PgNvdhowFodQNCTHIxMlDYgLqmg1mcpwRjJjnXF9A73gX0NkR65olYYAesp8cMMA==",
"requires": {
"es6-promise": "^4.2.8",
"form-data": "^3.0.0",
"isomorphic-fetch": "^2.2.1"
},
"dependencies": {
"form-data": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz",
"integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
}
}
}
},
"sass-graph": { "sass-graph": {
"version": "2.2.5", "version": "2.2.5",
"resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.5.tgz", "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.5.tgz",
@@ -12225,47 +12086,6 @@
"node-forge": "0.9.0" "node-forge": "0.9.0"
} }
}, },
"semantic-ui-css": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/semantic-ui-css/-/semantic-ui-css-2.4.1.tgz",
"integrity": "sha512-Pkp0p9oWOxlH0kODx7qFpIRYpK1T4WJOO4lNnpNPOoWKCrYsfHqYSKgk5fHfQtnWnsAKy7nLJMW02bgDWWFZFg==",
"requires": {
"jquery": "x.*"
}
},
"semantic-ui-react": {
"version": "1.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": { "semver": {
"version": "6.3.0", "version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
@@ -12455,11 +12275,6 @@
} }
} }
}, },
"shallowequal": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
"integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="
},
"shebang-command": { "shebang-command": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
@@ -13628,11 +13443,6 @@
"mime-types": "~2.1.24" "mime-types": "~2.1.24"
} }
}, },
"typed-styles": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/typed-styles/-/typed-styles-0.0.7.tgz",
"integrity": "sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q=="
},
"typedarray": { "typedarray": {
"version": "0.0.6", "version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
@@ -13934,14 +13744,6 @@
"makeerror": "1.0.x" "makeerror": "1.0.x"
} }
}, },
"warning": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
"requires": {
"loose-envify": "^1.0.0"
}
},
"watchpack": { "watchpack": {
"version": "1.7.2", "version": "1.7.2",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.2.tgz", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.2.tgz",

View File

@@ -1,11 +1,9 @@
{ {
"name": "@sasjs/tests", "name": "sasjs-tests",
"version": "1.0.0", "version": "0.1.0",
"homepage": ".", "homepage": ".",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@sasjs/adapter": "^1.3.6",
"@sasjs/test-framework": "^1.4.0",
"@testing-library/jest-dom": "^4.2.4", "@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.5.0", "@testing-library/react": "^9.5.0",
"@testing-library/user-event": "^7.2.1", "@testing-library/user-event": "^7.2.1",
@@ -18,6 +16,7 @@
"react-dom": "^16.13.1", "react-dom": "^16.13.1",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-scripts": "3.4.1", "react-scripts": "3.4.1",
"sasjs": "file:../build/sasjs-5.0.0.tgz",
"typescript": "^3.9.6" "typescript": "^3.9.6"
}, },
"scripts": { "scripts": {
@@ -25,7 +24,7 @@
"build": "react-scripts build", "build": "react-scripts build",
"test": "react-scripts test", "test": "react-scripts test",
"eject": "react-scripts eject", "eject": "react-scripts eject",
"deploy": "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" "deploy": "rsync -avhe ssh ./build/* --delete kriaco@sas.analytium.co.uk:/var/www/html/kriaco/sasjs-tests"
}, },
"eslintConfig": { "eslintConfig": {
"extends": "react-app" "extends": "react-app"
@@ -45,4 +44,4 @@
"devDependencies": { "devDependencies": {
"node-sass": "^4.14.1" "node-sass": "^4.14.1"
} }
} }

View File

@@ -6,7 +6,6 @@
"appLoc": "/Public/app", "appLoc": "/Public/app",
"serverType": "SASVIYA", "serverType": "SASVIYA",
"debug": false, "debug": false,
"contextName": "SharedCompute", "contextName": null
"useComputeApi": true
} }
} }

102
sasjs-tests/src/App.scss Normal file
View File

@@ -0,0 +1,102 @@
.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%;
}
}

View File

@@ -1,9 +1,10 @@
import React from "react"; import React from 'react';
import { render } from "@testing-library/react"; import { render } from '@testing-library/react';
import App from "./App"; import App from './App';
test("renders learn react link", () => { test('renders learn react link', () => {
const { getByText } = render(<App />); const { getByText } = render(<App />);
const linkElement = getByText(/learn react/i); const linkElement = getByText(/learn react/i);
expect(linkElement).toBeInTheDocument(); expect(linkElement).toBeInTheDocument();
}); });

View File

@@ -1,30 +1,54 @@
import React, { ReactElement, useState, useContext, useEffect } from "react"; import React, { ReactElement, useState, useContext, useEffect } from "react";
import { TestSuiteRunner, TestSuite, AppContext } from "@sasjs/test-framework"; import "./App.scss";
import { basicTests } from "./testSuites/Basic"; import TestSuiteRunner from "./TestSuiteRunner";
import { sendArrTests, sendObjTests } from "./testSuites/RequestData"; import { AppContext } from "./context/AppContext";
import { specialCaseTests } from "./testSuites/SpecialCases";
import { sasjsRequestTests } from "./testSuites/SasjsRequests";
import "@sasjs/test-framework/dist/index.css";
const App = (): ReactElement<{}> => { const App = (): ReactElement<{}> => {
const { adapter, config } = useContext(AppContext); const [appLoc, setAppLoc] = useState("");
const [testSuites, setTestSuites] = useState<TestSuite[]>([]); const [debug, setDebug] = useState(false);
const { adapter } = useContext(AppContext);
useEffect(() => { useEffect(() => {
if (adapter) { if (adapter) adapter.setDebugState(debug);
setTestSuites([ }, [debug, adapter]);
basicTests(adapter, config.userName, config.password),
sendArrTests(adapter), useEffect(() => {
sendObjTests(adapter), if (appLoc && adapter) {
specialCaseTests(adapter), adapter.setSASjsConfig({ ...adapter.getSasjsConfig(), appLoc });
sasjsRequestTests(adapter)
]);
} }
}, [adapter, config]); }, [appLoc, adapter]);
useEffect(() => {
setAppLoc(adapter.getSasjsConfig().appLoc);
}, [adapter]);
return ( return (
<div className="app"> <div className="app">
{adapter && testSuites && <TestSuiteRunner testSuites={testSuites} />} <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} />}
</div> </div>
); );
}; };

View File

@@ -1,6 +1,6 @@
import React, { ReactElement, useState, useCallback, useContext } from "react"; import React, { ReactElement, useState, useCallback, useContext } from "react";
import "./Login.scss"; import "./Login.scss";
import { AppContext } from "@sasjs/test-framework"; import { AppContext } from "./context/AppContext";
import { Redirect } from "react-router-dom"; import { Redirect } from "react-router-dom";
const Login = (): ReactElement<{}> => { const Login = (): ReactElement<{}> => {
@@ -9,11 +9,14 @@ const Login = (): ReactElement<{}> => {
const appContext = useContext(AppContext); const appContext = useContext(AppContext);
const handleSubmit = useCallback( const handleSubmit = useCallback(
(e) => { (e) => { // FIXME: rename 'e' => 'event'
e.preventDefault(); e.preventDefault();
appContext.adapter.logIn(username, password).then((res) => {
appContext.setIsLoggedIn(res.isLoggedIn); appContext.adapter.logIn(username, password)
}); .then(() => {
appContext.setIsLoggedIn(true);
});
// FIXME: catch block
}, },
[username, password, appContext] [username, password, appContext]
); );
@@ -38,7 +41,7 @@ const Login = (): ReactElement<{}> => {
type="password" type="password"
value={password} value={password}
required required
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)} // FIXME: rename 'e' => 'event'
/> />
</div> </div>
<button type="submit" className="submit-button"> <button type="submit" className="submit-button">
@@ -51,4 +54,4 @@ const Login = (): ReactElement<{}> => {
); );
}; };
export default Login; export default Login;

View File

@@ -1,6 +1,6 @@
import React, { ReactElement, useContext, FunctionComponent } from "react"; import React, { ReactElement, useContext, FunctionComponent } from "react";
import { Redirect, Route } from "react-router-dom"; import { Redirect, Route } from "react-router-dom";
import { AppContext } from "@sasjs/test-framework"; import { AppContext } from "./context/AppContext";
interface PrivateRouteProps { interface PrivateRouteProps {
component: FunctionComponent; component: FunctionComponent;
@@ -8,16 +8,14 @@ interface PrivateRouteProps {
path: string; path: string;
} }
const PrivateRoute = ( const PrivateRoute = (props: PrivateRouteProps): ReactElement<PrivateRouteProps> => {
props: PrivateRouteProps
): ReactElement<PrivateRouteProps> => {
const { component, path, exact } = props; const { component, path, exact } = props;
const appContext = useContext(AppContext); const appContext = useContext(AppContext);
return appContext.isLoggedIn ? (
return appContext.isLoggedIn ?
<Route component={component} path={path} exact={exact} /> <Route component={component} path={path} exact={exact} />
) : ( :
<Redirect to="/login" /> <Redirect to="/login" />
);
}; };
export default PrivateRoute; export default PrivateRoute;

View File

@@ -0,0 +1,19 @@
.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;
}
}

View File

@@ -0,0 +1,125 @@
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;

View File

@@ -0,0 +1,87 @@
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;

View File

@@ -0,0 +1,62 @@
.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%;
}
}

View File

@@ -0,0 +1,43 @@
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;

View File

@@ -0,0 +1,101 @@
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;

View File

@@ -0,0 +1,19 @@
.test-suite {
.test-suite-name {
font-size: 1.5em;
font-weight: bold;
color: #1f2027;
&.passed {
color: green;
}
&.failed {
color: red;
}
&.running {
color: yellow;
}
}
}

View File

@@ -0,0 +1,43 @@
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;

View File

@@ -0,0 +1,52 @@
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>
);
};

View File

@@ -1,7 +1,8 @@
body { body {
margin: 0; margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Droid Sans", "Helvetica Neue", sans-serif; "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
background-color: #1f2027; background-color: #1f2027;
@@ -9,7 +10,8 @@ body {
} }
* { * {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
} }
input { input {

View File

@@ -3,7 +3,7 @@ import ReactDOM from "react-dom";
import { Route, HashRouter, Switch } from "react-router-dom"; import { Route, HashRouter, Switch } from "react-router-dom";
import "./index.scss"; import "./index.scss";
import * as serviceWorker from "./serviceWorker"; import * as serviceWorker from "./serviceWorker";
import { AppProvider } from "@sasjs/test-framework"; import { AppProvider } from "./context/AppContext";
import PrivateRoute from "./PrivateRoute"; import PrivateRoute from "./PrivateRoute";
import Login from "./Login"; import Login from "./Login";
import App from "./App"; import App from "./App";

View File

@@ -11,9 +11,9 @@
// opt-in, read https://bit.ly/CRA-PWA // opt-in, read https://bit.ly/CRA-PWA
const isLocalhost = Boolean( const isLocalhost = Boolean(
window.location.hostname === "localhost" || window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address. // [::1] is the IPv6 localhost address.
window.location.hostname === "[::1]" || window.location.hostname === '[::1]' ||
// 127.0.0.0/8 are considered localhost for IPv4. // 127.0.0.0/8 are considered localhost for IPv4.
window.location.hostname.match( window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ /^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) { 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. // The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
if (publicUrl.origin !== window.location.origin) { if (publicUrl.origin !== window.location.origin) {
@@ -31,7 +31,7 @@ export function register(config) {
return; return;
} }
window.addEventListener("load", () => { window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) { if (isLocalhost) {
@@ -42,8 +42,8 @@ export function register(config) {
// service worker/PWA documentation. // service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => { navigator.serviceWorker.ready.then(() => {
console.log( console.log(
"This web app is being served cache-first by a service " + 'This web app is being served cache-first by a service ' +
"worker. To learn more, visit https://bit.ly/CRA-PWA" 'worker. To learn more, visit https://bit.ly/CRA-PWA'
); );
}); });
} else { } else {
@@ -57,21 +57,21 @@ export function register(config) {
function registerValidSW(swUrl, config) { function registerValidSW(swUrl, config) {
navigator.serviceWorker navigator.serviceWorker
.register(swUrl) .register(swUrl)
.then((registration) => { .then(registration => {
registration.onupdatefound = () => { registration.onupdatefound = () => {
const installingWorker = registration.installing; const installingWorker = registration.installing;
if (installingWorker == null) { if (installingWorker == null) {
return; return;
} }
installingWorker.onstatechange = () => { installingWorker.onstatechange = () => {
if (installingWorker.state === "installed") { if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) { if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched, // At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older // but the previous service worker will still serve the older
// content until all client tabs are closed. // content until all client tabs are closed.
console.log( console.log(
"New content is available and will be used when all " + 'New content is available and will be used when all ' +
"tabs for this page are closed. See https://bit.ly/CRA-PWA." 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
); );
// Execute callback // Execute callback
@@ -82,7 +82,7 @@ function registerValidSW(swUrl, config) {
// At this point, everything has been precached. // At this point, everything has been precached.
// It's the perfect time to display a // It's the perfect time to display a
// "Content is cached for offline use." message. // "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 // Execute callback
if (config && config.onSuccess) { if (config && config.onSuccess) {
@@ -93,25 +93,25 @@ function registerValidSW(swUrl, config) {
}; };
}; };
}) })
.catch((error) => { .catch(error => {
console.error("Error during service worker registration:", error); console.error('Error during service worker registration:', error);
}); });
} }
function checkValidServiceWorker(swUrl, config) { function checkValidServiceWorker(swUrl, config) {
// Check if the service worker can be found. If it can't reload the page. // Check if the service worker can be found. If it can't reload the page.
fetch(swUrl, { 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. // 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 ( if (
response.status === 404 || 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. // No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then((registration) => { navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => { registration.unregister().then(() => {
window.location.reload(); window.location.reload();
}); });
@@ -123,18 +123,18 @@ function checkValidServiceWorker(swUrl, config) {
}) })
.catch(() => { .catch(() => {
console.log( 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() { export function unregister() {
if ("serviceWorker" in navigator) { if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready navigator.serviceWorker.ready
.then((registration) => { .then(registration => {
registration.unregister(); registration.unregister();
}) })
.catch((error) => { .catch(error => {
console.error(error.message); console.error(error.message);
}); });
} }

View File

@@ -2,4 +2,4 @@
// allows you to do things like: // allows you to do things like:
// expect(element).toHaveTextContent(/react/i) // expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom // learn more: https://github.com/testing-library/jest-dom
import "@testing-library/jest-dom/extend-expect"; import '@testing-library/jest-dom/extend-expect';

View File

@@ -1,5 +1,5 @@
import SASjs, { ServerType, SASjsConfig } from "@sasjs/adapter"; import SASjs, { ServerType, SASjsConfig } from "sasjs";
import { TestSuite } from "@sasjs/test-framework"; import { TestSuite } from "../types";
const defaultConfig: SASjsConfig = { const defaultConfig: SASjsConfig = {
serverUrl: window.location.origin, serverUrl: window.location.origin,
@@ -9,7 +9,6 @@ const defaultConfig: SASjsConfig = {
serverType: ServerType.SASViya, serverType: ServerType.SASViya,
debug: true, debug: true,
contextName: "SAS Job Execution compute context", contextName: "SAS Job Execution compute context",
useComputeApi: false
}; };
const customConfig = { const customConfig = {
@@ -18,7 +17,7 @@ const customConfig = {
pathSASViya: "viya", pathSASViya: "viya",
appLoc: "/Public/seedapp", appLoc: "/Public/seedapp",
serverType: ServerType.SAS9, serverType: ServerType.SAS9,
debug: false debug: false,
}; };
export const basicTests = ( export const basicTests = (
@@ -34,18 +33,16 @@ export const basicTests = (
test: async () => { test: async () => {
return adapter.logIn(userName, password); return adapter.logIn(userName, password);
}, },
assertion: (response: any) => assertion: (response: any) => // FIXME: be more specific on type declaration
response && response.isLoggedIn && response.userName === userName response && response.isLoggedIn && response.userName === userName,
}, },
{ {
title: "Default config", title: "Default config",
description: description: "Should instantiate with default config when none is provided",
"Should instantiate with default config when none is provided", test: async () => Promise.resolve(new SASjs()),
test: async () => {
return Promise.resolve(new SASjs());
},
assertion: (sasjsInstance: SASjs) => { assertion: (sasjsInstance: SASjs) => {
const sasjsConfig = sasjsInstance.getSasjsConfig(); const sasjsConfig = sasjsInstance.getSasjsConfig();
return ( return (
sasjsConfig.serverUrl === defaultConfig.serverUrl && sasjsConfig.serverUrl === defaultConfig.serverUrl &&
sasjsConfig.pathSAS9 === defaultConfig.pathSAS9 && sasjsConfig.pathSAS9 === defaultConfig.pathSAS9 &&
@@ -54,7 +51,7 @@ export const basicTests = (
sasjsConfig.serverType === defaultConfig.serverType && sasjsConfig.serverType === defaultConfig.serverType &&
sasjsConfig.debug === defaultConfig.debug sasjsConfig.debug === defaultConfig.debug
); );
} },
}, },
{ {
title: "Custom config", title: "Custom config",
@@ -72,7 +69,7 @@ export const basicTests = (
sasjsConfig.serverType === customConfig.serverType && sasjsConfig.serverType === customConfig.serverType &&
sasjsConfig.debug === customConfig.debug sasjsConfig.debug === customConfig.debug
); );
} },
}, },
{ {
title: "Config overrides", title: "Config overrides",
@@ -92,7 +89,7 @@ export const basicTests = (
sasjsConfig.serverType === defaultConfig.serverType && sasjsConfig.serverType === defaultConfig.serverType &&
sasjsConfig.debug === false sasjsConfig.debug === false
); );
} },
} },
] ],
}); });

View File

@@ -1,45 +1,44 @@
import SASjs from "@sasjs/adapter"; import SASjs from "sasjs";
import { TestSuite } from "@sasjs/test-framework"; import { TestSuite } from "../types";
const stringData: any = { table1: [{ col1: "first col value" }] }; const stringData: any = { table1: [{ col1: "first col value" }] }; // TODO: be more specific on type declaration
const numericData: any = { table1: [{ col1: 3.14159265 }] }; const numericData: any = { table1: [{ col1: 3.14159265 }] }; // TODO: be more specific on type declaration
const multiColumnData: any = { const multiColumnData: any = { // TODO: be more specific on type declaration
table1: [{ col1: 42, col2: 1.618, col3: "x", col4: "x" }] table1: [{ col1: 42, col2: 1.618, col3: "x", col4: "x" }],
}; };
const multipleRowsWithNulls: any = { const multipleRowsWithNulls: any = { // TODO: be more specific on type declaration
table1: [ 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: 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" } { col1: 42, col2: 1.62, col3: "x", col4: "x" },
] ],
}; };
const multipleColumnsWithNulls: any = { const multipleColumnsWithNulls: any = { // TODO: be more specific on type declaration
table1: [ 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: 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: "" } { col1: 42, col2: null, col3: "x", col4: "" },
] ],
}; };
const getLongStringData = (length = 32764) => { const getLongStringData = (length = 32764) => { // FIXME: add type declaration
let x = "X"; let x = "X";
for (let i = 1; i <= length; i++) {
x = x + "X"; for (let i = 1; i <= length; i++) x += 'X'
}
const data: any = { table1: [{ col1: x }] }; const data: any = { table1: [{ col1: x }] }; // TODO: be more specific on type declaration
return data; return data;
}; };
const getLargeObjectData = () => { const getLargeObjectData = () => {
const data = { table1: [{ big: "data" }] }; const data = { table1: [{ big: "data" }] };
for (let i = 1; i < 10000; i++) { for (let i = 1; i < 10000; i++) data.table1.push(data.table1[0])
data.table1.push(data.table1[0]);
}
return data; return data;
}; };
@@ -50,12 +49,8 @@ export const sendArrTests = (adapter: SASjs): TestSuite => ({
{ {
title: "Single string value", title: "Single string value",
description: "Should send an array with a single string value", description: "Should send an array with a single string value",
test: () => { test: () => adapter.request("common/sendArr", stringData),
return adapter.request("common/sendArr", stringData); assertion: (res: any) => res.table1[0][0] === stringData.table1[0].col1 // TODO: be more specific on type declaration
},
assertion: (res: any) => {
return res.table1[0][0] === stringData.table1[0].col1;
}
}, },
{ {
title: "Long string value", title: "Long string value",
@@ -64,22 +59,18 @@ export const sendArrTests = (adapter: SASjs): TestSuite => ({
test: () => { test: () => {
return adapter.request("common/sendArr", getLongStringData()); return adapter.request("common/sendArr", getLongStringData());
}, },
assertion: (res: any) => { assertion: (res: any) => res.table1[0][0] === getLongStringData().table1[0].col1 // TODO: be more specific on type declaration
const longStringData = getLongStringData();
return res.table1[0][0] === longStringData.table1[0].col1;
}
}, },
{ {
title: "Overly long string value", title: "Overly long string value",
description: description:
"Should error out with long string values over 32765 characters", "Should error out with long string values over 32765 characters",
test: () => { test: () => adapter
const data = getLongStringData(32767); .request("common/sendArr", getLongStringData(32767))
return adapter.request("common/sendArr", data).catch((e) => e); .catch((e) => e), // TODO: rename
assertion: (error: any) => { // TODO: be more specific on type declaration
return !!error && !!error.MESSAGE; // FIXME: refactor
}, },
assertion: (error: any) => {
return !!error && !!error.MESSAGE;
}
}, },
{ {
title: "Single numeric value", title: "Single numeric value",
@@ -87,9 +78,9 @@ export const sendArrTests = (adapter: SASjs): TestSuite => ({
test: () => { test: () => {
return adapter.request("common/sendArr", numericData); return adapter.request("common/sendArr", numericData);
}, },
assertion: (res: any) => { assertion: (res: any) => { // TODO: be more specific on type declaration
return res.table1[0][0] === numericData.table1[0].col1; return res.table1[0][0] === numericData.table1[0].col1;
} },
}, },
{ {
title: "Multiple columns", title: "Multiple columns",
@@ -97,14 +88,14 @@ export const sendArrTests = (adapter: SASjs): TestSuite => ({
test: () => { test: () => {
return adapter.request("common/sendArr", multiColumnData); return adapter.request("common/sendArr", multiColumnData);
}, },
assertion: (res: any) => { assertion: (res: any) => { // TODO: be more specific on type declaration
return ( return (
res.table1[0][0] === multiColumnData.table1[0].col1 && res.table1[0][0] === multiColumnData.table1[0].col1 &&
res.table1[0][1] === multiColumnData.table1[0].col2 && res.table1[0][1] === multiColumnData.table1[0].col2 &&
res.table1[0][2] === multiColumnData.table1[0].col3 && res.table1[0][2] === multiColumnData.table1[0].col3 &&
res.table1[0][3] === multiColumnData.table1[0].col4 res.table1[0][3] === multiColumnData.table1[0].col4
); );
} },
}, },
{ {
title: "Multiple rows with nulls", title: "Multiple rows with nulls",
@@ -112,9 +103,10 @@ export const sendArrTests = (adapter: SASjs): TestSuite => ({
test: () => { test: () => {
return adapter.request("common/sendArr", multipleRowsWithNulls); return adapter.request("common/sendArr", multipleRowsWithNulls);
}, },
assertion: (res: any) => { assertion: (res: any) => { // TODO: be more specific on type declaration
let result = true; let result = true;
multipleRowsWithNulls.table1.forEach((_: any, index: number) => { multipleRowsWithNulls.table1.forEach((_: any, index: number) => { // TODO: be more specific on type declaration
// FIXME: use loop
result = result =
result && result &&
res.table1[index][0] === multipleRowsWithNulls.table1[index].col1; res.table1[index][0] === multipleRowsWithNulls.table1[index].col1;
@@ -128,8 +120,9 @@ export const sendArrTests = (adapter: SASjs): TestSuite => ({
result && result &&
res.table1[index][3] === multipleRowsWithNulls.table1[index].col4; res.table1[index][3] === multipleRowsWithNulls.table1[index].col4;
}); });
return result; return result;
} },
}, },
{ {
title: "Multiple columns with nulls", title: "Multiple columns with nulls",
@@ -137,9 +130,9 @@ export const sendArrTests = (adapter: SASjs): TestSuite => ({
test: () => { test: () => {
return adapter.request("common/sendArr", multipleColumnsWithNulls); return adapter.request("common/sendArr", multipleColumnsWithNulls);
}, },
assertion: (res: any) => { assertion: (res: any) => { // TODO: be more specific on type declaration
let result = true; let result = true;
multipleColumnsWithNulls.table1.forEach((_: any, index: number) => { multipleColumnsWithNulls.table1.forEach((_: any, index: number) => { // TODO: be more specific on type declaration
result = result =
result && result &&
res.table1[index][0] === res.table1[index][0] ===
@@ -158,9 +151,9 @@ export const sendArrTests = (adapter: SASjs): TestSuite => ({
(multipleColumnsWithNulls.table1[index].col4 || ""); (multipleColumnsWithNulls.table1[index].col4 || "");
}); });
return result; return result;
} },
} },
] ],
}); });
export const sendObjTests = (adapter: SASjs): TestSuite => ({ export const sendObjTests = (adapter: SASjs): TestSuite => ({
@@ -170,12 +163,12 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
title: "Invalid column name", title: "Invalid column name",
description: "Should throw an error", description: "Should throw an error",
test: async () => { test: async () => {
const invalidData: any = { const invalidData: any = { // TODO: be more specific on type declaration
"1 invalid table": [{ col1: 42 }] "1 invalid table": [{ col1: 42 }],
}; };
return adapter.request("common/sendObj", invalidData).catch((e) => e); return adapter.request("common/sendObj", invalidData).catch((e) => e);
}, },
assertion: (error: any) => !!error && !!error.MESSAGE assertion: (error: any) => !!error && !!error.MESSAGE, // TODO: be more specific on type declaration
}, },
{ {
title: "Single string value", title: "Single string value",
@@ -183,9 +176,9 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
test: () => { test: () => {
return adapter.request("common/sendObj", stringData); return adapter.request("common/sendObj", stringData);
}, },
assertion: (res: any) => { assertion: (res: any) => { // TODO: be more specific on type declaration
return res.table1[0].COL1 === stringData.table1[0].col1; return res.table1[0].COL1 === stringData.table1[0].col1;
} },
}, },
{ {
title: "Long string value", title: "Long string value",
@@ -194,10 +187,10 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
test: () => { test: () => {
return adapter.request("common/sendObj", getLongStringData()); return adapter.request("common/sendObj", getLongStringData());
}, },
assertion: (res: any) => { assertion: (res: any) => { // TODO: be more specific on type declaration
const longStringData = getLongStringData(); const longStringData = getLongStringData();
return res.table1[0].COL1 === longStringData.table1[0].col1; return res.table1[0].COL1 === longStringData.table1[0].col1;
} },
}, },
{ {
title: "Overly long string value", title: "Overly long string value",
@@ -208,9 +201,9 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
.request("common/sendObj", getLongStringData(32767)) .request("common/sendObj", getLongStringData(32767))
.catch((e) => e); .catch((e) => e);
}, },
assertion: (error: any) => { assertion: (error: any) => { // TODO: be more specific on type declaration
return !!error && !!error.MESSAGE; return !!error && !!error.MESSAGE;
} },
}, },
{ {
title: "Single numeric value", title: "Single numeric value",
@@ -218,9 +211,9 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
test: () => { test: () => {
return adapter.request("common/sendObj", numericData); return adapter.request("common/sendObj", numericData);
}, },
assertion: (res: any) => { assertion: (res: any) => { // TODO: be more specific on type declaration
return res.table1[0].COL1 === numericData.table1[0].col1; return res.table1[0].COL1 === numericData.table1[0].col1;
} },
}, },
{ {
@@ -229,10 +222,10 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
test: () => { test: () => {
return adapter.request("common/sendObj", getLargeObjectData()); return adapter.request("common/sendObj", getLargeObjectData());
}, },
assertion: (res: any) => { assertion: (res: any) => { // TODO: be more specific on type declaration
const data = getLargeObjectData(); const data = getLargeObjectData();
return res.table1[9000].BIG === data.table1[9000].big; return res.table1[9000].BIG === data.table1[9000].big;
} },
}, },
{ {
title: "Multiple columns", title: "Multiple columns",
@@ -240,14 +233,14 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
test: () => { test: () => {
return adapter.request("common/sendObj", multiColumnData); return adapter.request("common/sendObj", multiColumnData);
}, },
assertion: (res: any) => { assertion: (res: any) => { // TODO: be more specific on type declaration
return ( return (
res.table1[0].COL1 === multiColumnData.table1[0].col1 && res.table1[0].COL1 === multiColumnData.table1[0].col1 &&
res.table1[0].COL2 === multiColumnData.table1[0].col2 && res.table1[0].COL2 === multiColumnData.table1[0].col2 &&
res.table1[0].COL3 === multiColumnData.table1[0].col3 && res.table1[0].COL3 === multiColumnData.table1[0].col3 &&
res.table1[0].COL4 === multiColumnData.table1[0].col4 res.table1[0].COL4 === multiColumnData.table1[0].col4
); );
} },
}, },
{ {
title: "Multiple rows with nulls", title: "Multiple rows with nulls",
@@ -255,9 +248,9 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
test: () => { test: () => {
return adapter.request("common/sendObj", multipleRowsWithNulls); return adapter.request("common/sendObj", multipleRowsWithNulls);
}, },
assertion: (res: any) => { assertion: (res: any) => { // TODO: be more specific on type declaration
let result = true; let result = true;
multipleRowsWithNulls.table1.forEach((_: any, index: number) => { multipleRowsWithNulls.table1.forEach((_: any, index: number) => { // TODO: be more specific on type declaration
result = result =
result && result &&
res.table1[index].COL1 === multipleRowsWithNulls.table1[index].col1; res.table1[index].COL1 === multipleRowsWithNulls.table1[index].col1;
@@ -272,7 +265,7 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
res.table1[index].COL4 === multipleRowsWithNulls.table1[index].col4; res.table1[index].COL4 === multipleRowsWithNulls.table1[index].col4;
}); });
return result; return result;
} },
}, },
{ {
title: "Multiple columns with nulls", title: "Multiple columns with nulls",
@@ -280,9 +273,9 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
test: () => { test: () => {
return adapter.request("common/sendObj", multipleColumnsWithNulls); return adapter.request("common/sendObj", multipleColumnsWithNulls);
}, },
assertion: (res: any) => { assertion: (res: any) => { // TODO: be more specific on type declaration
let result = true; let result = true;
multipleColumnsWithNulls.table1.forEach((_: any, index: number) => { multipleColumnsWithNulls.table1.forEach((_: any, index: number) => { // TODO: be more specific on type declaration
result = result =
result && result &&
res.table1[index].COL1 === res.table1[index].COL1 ===
@@ -301,7 +294,7 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
(multipleColumnsWithNulls.table1[index].col4 || ""); (multipleColumnsWithNulls.table1[index].col4 || "");
}); });
return result; return result;
} },
} },
] ],
}); });

View File

@@ -1,7 +1,7 @@
import SASjs from "@sasjs/adapter"; import SASjs from "sasjs";
import { TestSuite } from "@sasjs/test-framework"; import { TestSuite } from "../types";
const data: any = { table1: [{ col1: "first col value" }] }; const data: any = { table1: [{ col1: "first col value" }] }; // TODO: be more specific on type declaration
export const sasjsRequestTests = (adapter: SASjs): TestSuite => ({ export const sasjsRequestTests = (adapter: SASjs): TestSuite => ({
name: "SASjs Requests", name: "SASjs Requests",
@@ -9,42 +9,12 @@ export const sasjsRequestTests = (adapter: SASjs): TestSuite => ({
{ {
title: "WORK tables", title: "WORK tables",
description: "Should get WORK tables after request", description: "Should get WORK tables after request",
test: async () => { test: async () => adapter.request("common/sendArr", data),
return adapter.request("common/sendArr", data); assertion: (res: any) => {
},
assertion: () => {
const requests = adapter.getSasRequests(); const requests = adapter.getSasRequests();
if (adapter.getSasjsConfig().debug) {
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); return adapter.getSasjsConfig().debug ? requests[0].SASWORK !== null : requests[0].SASWORK === null
});
});
}, },
assertion: (response) => { },
return response; ],
}
}
]
}); });

View File

@@ -1,7 +1,7 @@
import SASjs from "@sasjs/adapter"; import SASjs from "sasjs";
import { TestSuite } from "@sasjs/test-framework"; import { TestSuite } from "../types";
const specialCharData: any = { const specialCharData: any = { // TODO: be more specific on type definition
table1: [ table1: [
{ {
tab: "\t", tab: "\t",
@@ -9,16 +9,16 @@ const specialCharData: any = {
cr: "\r", cr: "\r",
semicolon: ";semi", semicolon: ";semi",
percent: "%", percent: "%",
singleQuote: "'", singleQuote: "'", // TODO: use ``
doubleQuote: '"', doubleQuote: '"', // TODO: use ``
crlf: "\r\n", crlf: "\r\n",
euro: "€euro", euro: "€euro",
banghash: "!#banghash" banghash: "!#banghash",
} },
] ],
}; };
const moreSpecialCharData: any = { const moreSpecialCharData: any = { // TODO: be more specific on type definition
table1: [ table1: [
{ {
speech0: '"speech', speech0: '"speech',
@@ -31,51 +31,53 @@ const moreSpecialCharData: any = {
sigma: "Σsigma", sigma: "Σsigma",
at: "@at", at: "@at",
serbian: "Српски", serbian: "Српски",
dollar: "$" dollar: "$",
} },
] ],
}; };
const getWideData = () => { const getWideData = () => { // FIXME: declared but never used
const cols: any = {}; const cols: any = {}; // TODO: be more specific on type definition
for (let i = 1; i <= 10000; i++) { for (let i = 1; i <= 10000; i++) { // Why 10000?
cols["col" + i] = "test" + i; cols["col" + i] = "test" + i;
} }
const data: any = { const data: any = { // TODO: be more specific on type definition
table1: [cols] table1: [cols],
}; };
return data; return data;
}; };
const getTables = () => { const getTables = () => { // FIXME: declared but never used
const tables: any = {}; const tables: any = {}; // TODO: be more specific on type definition
for (let i = 1; i <= 100; i++) { for (let i = 1; i <= 100; i++) { // why 100
tables["table" + i] = [{ col1: "x", col2: "x", col3: "x", col4: "x" }]; tables["table" + i] = [{ col1: "x", col2: "x", col3: "x", col4: "x" }];
} }
return tables; return tables;
}; };
const getLargeDataset = () => { const getLargeDataset = () => {
const rows: any = []; const rows: any = []; // TODO: be more specific on type definition
const colData: string = const colData: string = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; // FIXME: no need to explicitly mention data type
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
for (let i = 1; i <= 10000; i++) { for (let i = 1; i <= 10000; i++) {
rows.push({ col1: colData, col2: colData, col3: colData, col4: colData }); rows.push({ col1: colData, col2: colData, col3: colData, col4: colData });
} }
const data: any = { const data: any = { // TODO: be more specific on type definition
table1: rows table1: rows,
}; };
return data; return data;
}; };
const errorAndCsrfData: any = { // FIXME: declared but never used
const errorAndCsrfData: any = { // TODO: be more specific on type definition
error: [{ col1: "q", col2: "w", col3: "e", col4: "r" }], 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 => ({ export const specialCaseTests = (adapter: SASjs): TestSuite => ({
@@ -84,10 +86,8 @@ export const specialCaseTests = (adapter: SASjs): TestSuite => ({
{ {
title: "Common special characters", title: "Common special characters",
description: "Should handle common special characters", description: "Should handle common special characters",
test: () => { test: () => adapter.request("common/sendArr", specialCharData),
return adapter.request("common/sendArr", specialCharData); assertion: (res: any) => { // TODO: be more specific on type definition
},
assertion: (res: any) => {
return ( return (
res.table1[0][0] === specialCharData.table1[0].tab && res.table1[0][0] === specialCharData.table1[0].tab &&
res.table1[0][1] === specialCharData.table1[0].lf && res.table1[0][1] === specialCharData.table1[0].lf &&
@@ -100,151 +100,138 @@ export const specialCaseTests = (adapter: SASjs): TestSuite => ({
res.table1[0][8] === specialCharData.table1[0].euro && res.table1[0][8] === specialCharData.table1[0].euro &&
res.table1[0][9] === specialCharData.table1[0].banghash res.table1[0][9] === specialCharData.table1[0].banghash
); );
}
},
{
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
);
}
}, },
// 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: "Wide table with sendArr", title: "Large dataset",
description: "Should handle data with 10000 columns",
test: () => {
return adapter.request("common/sendArr", getWideData());
},
assertion: (res: any) => {
const data = getWideData();
let result = true;
for (let i = 0; i <= 10; i++) {
result =
result && res.table1[0][i] === data.table1[0]["col" + (i + 1)];
}
return result;
}
},
{
title: "Wide table with sendObj",
description: "Should handle data with 10000 columns",
test: () => {
return adapter.request("common/sendObj", getWideData());
},
assertion: (res: any) => {
const data = getWideData();
let result = true;
for (let i = 0; i <= 10; i++) {
result =
result &&
res.table1[0]["COL" + (i + 1)] === data.table1[0]["col" + (i + 1)];
}
return result;
}
},
{
title: "Multiple tables",
description: "Should handle data with 100 tables",
test: () => {
return adapter.request("common/sendArr", getTables());
},
assertion: (res: any) => {
const data = getTables();
return (
res.table1[0][0] === data.table1[0].col1 &&
res.table1[0][1] === data.table1[0].col2 &&
res.table1[0][2] === data.table1[0].col3 &&
res.table1[0][3] === data.table1[0].col4 &&
res.table50[0][0] === data.table50[0].col1 &&
res.table50[0][1] === data.table50[0].col2 &&
res.table50[0][2] === data.table50[0].col3 &&
res.table50[0][3] === data.table50[0].col4
);
}
},
{
title: "Large dataset with sendObj",
description: "Should handle 5mb of data", description: "Should handle 5mb of data",
test: () => { test: () => adapter.request("common/sendArr", getLargeDataset()),
return adapter.request("common/sendObj", getLargeDataset()); assertion: (res: any) => { // TODO: be more specific on type definition
},
assertion: (res: any) => {
const data = getLargeDataset(); const data = getLargeDataset();
let result = true; let result = true; // TODO: rename
for (let i = 0; i <= 10; i++) { for (let i = 0; i <= 10; i++) {
result = result && res.table1[i][0] === data.table1[i][0]; result = result && res.table1[i][0] === data.table1[i][0];
} }
return result; return result;
}
},
{
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", // title: "Error and _csrf tables with sendArr",
test: () => { // description: "Should handle error and _csrf tables",
return adapter.request("common/sendArr", errorAndCsrfData); // test: () => {
}, // return adapter.request("common/sendArr", errorAndCsrfData);
assertion: (res: any) => { // },
return ( // assertion: (res: any) => {
res.error[0][0] === errorAndCsrfData.error[0].col1 && // return (
res.error[0][1] === errorAndCsrfData.error[0].col2 && // res.error[0][0] === errorAndCsrfData.error[0].col1 &&
res.error[0][2] === errorAndCsrfData.error[0].col3 && // res.error[0][1] === errorAndCsrfData.error[0].col2 &&
res.error[0][3] === errorAndCsrfData.error[0].col4 && // res.error[0][2] === errorAndCsrfData.error[0].col3 &&
res._csrf[0][0] === errorAndCsrfData._csrf[0].col1 && // res.error[0][3] === errorAndCsrfData.error[0].col4 &&
res._csrf[0][1] === errorAndCsrfData._csrf[0].col2 && // res._csrf[0][0] === errorAndCsrfData._csrf[0].col1 &&
res._csrf[0][2] === errorAndCsrfData._csrf[0].col3 && // res._csrf[0][1] === errorAndCsrfData._csrf[0].col2 &&
res._csrf[0][3] === errorAndCsrfData._csrf[0].col4 // 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", // title: "Error and _csrf tables with sendObj",
test: () => { // description: "Should handle error and _csrf tables",
return adapter.request("common/sendObj", errorAndCsrfData); // test: () => {
}, // return adapter.request("common/sendObj", errorAndCsrfData);
assertion: (res: any) => { // },
return ( // assertion: (res: any) => {
res.error[0].COL1 === errorAndCsrfData.error[0].col1 && // return (
res.error[0].COL2 === errorAndCsrfData.error[0].col2 && // res.error[0].COL1 === errorAndCsrfData.error[0].col1 &&
res.error[0].COL3 === errorAndCsrfData.error[0].col3 && // res.error[0].COL2 === errorAndCsrfData.error[0].col2 &&
res.error[0].COL4 === errorAndCsrfData.error[0].col4 && // res.error[0].COL3 === errorAndCsrfData.error[0].col3 &&
res._csrf[0].COL1 === errorAndCsrfData._csrf[0].col1 && // res.error[0].COL4 === errorAndCsrfData.error[0].col4 &&
res._csrf[0].COL2 === errorAndCsrfData._csrf[0].col2 && // res._csrf[0].COL1 === errorAndCsrfData._csrf[0].col1 &&
res._csrf[0].COL3 === errorAndCsrfData._csrf[0].col3 && // res._csrf[0].COL2 === errorAndCsrfData._csrf[0].col2 &&
res._csrf[0].COL4 === errorAndCsrfData._csrf[0].col4 // res._csrf[0].COL3 === errorAndCsrfData._csrf[0].col3 &&
); // res._csrf[0].COL4 === errorAndCsrfData._csrf[0].col4
} // );
} // },
] // },
}); ],
});

View File

@@ -0,0 +1,15 @@
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>;
}

View File

@@ -3,20 +3,21 @@ export const assert = (
message = "Assertion failed" message = "Assertion failed"
) => { ) => {
let result; let result;
try { try {
if (typeof expression === "boolean") { if (typeof expression === "boolean") result = expression;
result = expression; else result = expression();
} else {
result = expression();
}
} catch (e) { } catch (e) {
console.error(message); console.error(message);
throw new Error(message); throw new Error(message);
} }
if (!!result) { if (!!result) {
return; return;
} else { } else {
console.error(message); console.error(message);
throw new Error(message); throw new Error(message);
} }
}; };

View File

@@ -0,0 +1,27 @@
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);
});
};

View File

@@ -1,100 +0,0 @@
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);
}
}
});
});
}
}

View File

@@ -3,49 +3,49 @@
* *
*/ */
export class SAS9ApiClient { export class SAS9ApiClient {
constructor(private serverUrl: string) {} constructor(private serverUrl: string) {}
/** /**
* returns on object containing the server URL * @returns an object containing the server URL
*/ */
public getConfig() { public getConfig() {
return { return {
serverUrl: this.serverUrl serverUrl: this.serverUrl,
}; }
} }
/** /**
* Updates serverurl which is not null * Updates serverUrl which is not null
* @param serverUrl - the URL of the server. * @param serverUrl - the URL of the server.
*/ */
public setConfig(serverUrl: string) { public setConfig(serverUrl: string) {
if (serverUrl) this.serverUrl = serverUrl; if (serverUrl) this.serverUrl = serverUrl
} }
/** /**
* Executes code on a SAS9 server. * Executes code on a SAS9 server.
* @param linesOfCode - an array of lines of code to execute * @param linesOfCode - an array of lines of code to execute
* @param serverName - the server to execute the code on * @param serverName - the server to execute the code on
* @param repositoryName - the repository to execute the code on * @param repositoryName - the repository to execute the code on
*/ */
public async executeScript( public async executeScript(
linesOfCode: string[], linesOfCode: string[], // FIXME: rename
serverName: string, serverName: string,
repositoryName: string repositoryName: string
) { ) {
const requestPayload = linesOfCode.join("\n"); const requestPayload = linesOfCode.join('\n')
const executeScriptRequest = { const executeScriptRequest = {
method: "PUT", method: 'PUT',
headers: { headers: {Accept: 'application/json'},
Accept: "application/json" body: `command=${requestPayload}`,
}, }
body: `command=${requestPayload}` // FIXME: use axios instead of fetch
}; const executeScriptResponse = await fetch(
const executeScriptResponse = await fetch( `${this.serverUrl}/sas/servers/${serverName}/cmd?repositoryName=${repositoryName}`,
`${this.serverUrl}/sas/servers/${serverName}/cmd?repositoryName=${repositoryName}`, executeScriptRequest
executeScriptRequest ).then((res) => res.text())
).then((res) => res.text()); // FIXME: no catch block
return executeScriptResponse; return executeScriptResponse
} }
} }

View File

@@ -1,15 +1,22 @@
/**
* TODO: needs to be split into logical blocks:
* - Folder
* - Config
* - Context
* - Session
* - Job
* - Auth
*/
import { import {
isAuthorizeFormRequired, isAuthorizeFormRequired,
parseAndSubmitAuthorizeForm, parseAndSubmitAuthorizeForm,
convertToCSV, convertToCSV,
makeRequest makeRequest,
} from "./utils"; } from "./utils";
import * as NodeFormData from "form-data"; import * as NodeFormData from "form-data";
import * as path from "path"; import * as path from "path";
import { Job, Session, Context, Folder, CsrfToken } from "./types"; import { Job, Session, Context, Folder } from "./types";
import { JobDefinition } from "./types/JobDefinition";
import { formatDataForRequest } from "./utils/formatDataForRequest";
import { SessionManager } from "./SessionManager";
/** /**
* A client for interfacing with the SAS Viya REST API * A client for interfacing with the SAS Viya REST API
@@ -19,21 +26,14 @@ export class SASViyaApiClient {
constructor( constructor(
private serverUrl: string, private serverUrl: string,
private rootFolderName: string, private rootFolderName: string,
private contextName: string,
private setCsrfToken: (csrfToken: CsrfToken) => void,
private rootFolderMap = new Map<string, Job[]>() private rootFolderMap = new Map<string, Job[]>()
) { ) {
if (!rootFolderName) { if (!rootFolderName) {
throw new Error("Root folder must be provided."); throw new Error("Root folder must be provided.");
} }
} }
private csrfToken: CsrfToken | null = null; private csrfToken: { headerName: string; value: string } | null = null;
private rootFolder: Folder | null = null; private rootFolder: Folder | null = null;
private sessionManager = new SessionManager(
this.serverUrl,
this.contextName,
this.setCsrfToken
);
/** /**
* Returns a map containing the directory structure in the currently set root folder. * Returns a map containing the directory structure in the currently set root folder.
@@ -53,7 +53,7 @@ export class SASViyaApiClient {
public getConfig() { public getConfig() {
return { return {
serverUrl: this.serverUrl, serverUrl: this.serverUrl,
rootFolderName: this.rootFolderName rootFolderName: this.rootFolderName,
}; };
} }
@@ -73,12 +73,12 @@ export class SASViyaApiClient {
*/ */
public async getAllContexts(accessToken?: string) { public async getAllContexts(accessToken?: string) {
const headers: any = { const headers: any = {
"Content-Type": "application/json" "Content-Type": "application/json",
}; };
if (accessToken) { if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`; headers.Authorization = `Bearer ${accessToken}`;
} }
const { result: contexts } = await this.request<{ items: Context[] }>( const contexts = await this.request<{ items: Context[] }>(
`${this.serverUrl}/compute/contexts`, `${this.serverUrl}/compute/contexts`,
{ headers } { headers }
); );
@@ -88,7 +88,7 @@ export class SASViyaApiClient {
id: context.id, id: context.id,
name: context.name, name: context.name,
version: context.version, version: context.version,
attributes: {} attributes: {},
})); }));
} }
@@ -98,13 +98,12 @@ export class SASViyaApiClient {
*/ */
public async getExecutableContexts(accessToken?: string) { public async getExecutableContexts(accessToken?: string) {
const headers: any = { const headers: any = {
"Content-Type": "application/json" "Content-Type": "application/json",
}; };
if (accessToken) { if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`; headers.Authorization = `Bearer ${accessToken}`;
} }
const contexts = await this.request<{ items: Context[] }>(
const { result: contexts } = await this.request<{ items: Context[] }>(
`${this.serverUrl}/compute/contexts`, `${this.serverUrl}/compute/contexts`,
{ headers } { headers }
); );
@@ -117,7 +116,9 @@ export class SASViyaApiClient {
`test-${context.name}`, `test-${context.name}`,
linesOfCode, linesOfCode,
context.name, context.name,
accessToken accessToken,
undefined,
true
).catch(() => null); ).catch(() => null);
}); });
const results = await Promise.all(promises); const results = await Promise.all(promises);
@@ -139,8 +140,8 @@ export class SASViyaApiClient {
name: contextsList[index].name, name: contextsList[index].name,
version: contextsList[index].version, version: contextsList[index].version,
attributes: { attributes: {
sysUserId sysUserId,
} },
}); });
} }
}); });
@@ -155,14 +156,14 @@ export class SASViyaApiClient {
*/ */
public async createSession(contextName: string, accessToken?: string) { public async createSession(contextName: string, accessToken?: string) {
const headers: any = { const headers: any = {
"Content-Type": "application/json" "Content-Type": "application/json",
}; };
if (accessToken) { if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`; headers.Authorization = `Bearer ${accessToken}`;
} }
const { result: contexts } = await this.request<{ items: Context[] }>( const contexts = await this.request<{ items: Context[] }>(
`${this.serverUrl}/compute/contexts`, `${this.serverUrl}/compute/contexts`,
{ headers } { headers }
); );
@@ -178,10 +179,10 @@ export class SASViyaApiClient {
method: "POST", method: "POST",
headers: { headers: {
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json" "Content-Type": "application/json",
} },
}; };
const { result: createdSession } = await this.request<Session>( const createdSession = this.request<Session>(
`${this.serverUrl}/compute/contexts/${executionContext.id}/sessions`, `${this.serverUrl}/compute/contexts/${executionContext.id}/sessions`,
createSessionRequest createSessionRequest
); );
@@ -199,69 +200,48 @@ export class SASViyaApiClient {
* @param silent - optional flag to turn of logging. * @param silent - optional flag to turn of logging.
*/ */
public async executeScript( public async executeScript(
jobName: string, fileName: string,
linesOfCode: string[], linesOfCode: string[],
contextName: string, contextName: string,
accessToken?: string, accessToken?: string,
silent = false, sessionId = "",
data = null, silent = false
debug = false ) {
): Promise<any> { const headers: any = {
silent = !debug; "Content-Type": "application/json",
try { };
const headers: any = { if (accessToken) {
"Content-Type": "application/json" headers.Authorization = `Bearer ${accessToken}`;
}; }
if (this.csrfToken) {
if (accessToken) { headers[this.csrfToken.headerName] = this.csrfToken.value;
headers.Authorization = `Bearer ${accessToken}`; }
} 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;
if (executionContext) {
// Request new session in context or use the ID passed in
let executionSessionId: string; let executionSessionId: string;
const session = await this.sessionManager.getSession(accessToken); if (sessionId) {
executionSessionId = session!.id; executionSessionId = sessionId;
} else {
const createSessionRequest = {
method: "POST",
headers,
};
const createdSession = await this.request<Session>(
`${this.serverUrl}/compute/contexts/${executionContext.id}/sessions`,
createSessionRequest
);
const jobArguments: { [key: string]: any } = { executionSessionId = createdSession.id;
_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 // Execute job in session
const postJobRequest = { const postJobRequest = {
method: "POST", method: "POST",
@@ -270,16 +250,12 @@ export class SASViyaApiClient {
name: fileName, name: fileName,
description: "Powered by SASjs", description: "Powered by SASjs",
code: linesOfCode, 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`, `${this.serverUrl}/compute/sessions/${executionSessionId}/jobs`,
postJobRequest postJobRequest
); );
if (!silent) { if (!silent) {
console.log(`Job has been submitted for ${fileName}`); console.log(`Job has been submitted for ${fileName}`);
console.log( console.log(
@@ -289,75 +265,33 @@ export class SASViyaApiClient {
); );
} }
const jobStatus = await this.pollJobState( const jobStatus = await this.pollJobState(postedJob, accessToken, silent);
postedJob, const logLink = postedJob.links.find((l: any) => l.rel === "log");
etag, if (logLink) {
accessToken, const log = await this.request(
silent `${this.serverUrl}${logLink.href}?limit=100000`,
);
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 headers,
} }
).then((res: any) =>
res.result.items.map((i: any) => i.line).join("\n")
); );
}
if (jobStatus === "failed" || jobStatus === "error") { return { jobStatus, log };
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;
} }
} else {
console.error(
`Unable to find execution context ${contextName}.\nPlease check the contextName in the tgtDeployVars and try again.`
);
console.error("Response from server: ", JSON.stringify(contexts));
} }
} }
/** /**
* Creates a folder in the specified location. Either parentFolderPath or * Creates a folder in the specified location. Either parentFolderPath or
* parentFolderUri must be provided. * parentFolderUri must be provided.
* @param folderName - the name of the new folder. * @param folderName - the name of the new folder.
* @param parentFolderPath - the full path to the parent folder. If not * @param parentFolderPath - the full path to the parent folder. If not
* provided, the parentFolderUri must be provided. * provided, the parentFolderUri must be provided.
* @param parentFolderUri - the URI (eg /folders/folders/UUID) of the parent * @param parentFolderUri - the URI (eg /folders/folders/UUID) of the parent
* folder. If not provided, the parentFolderPath must be provided. * folder. If not provided, the parentFolderPath must be provided.
*/ */
public async createFolder( public async createFolder(
@@ -372,27 +306,17 @@ export class SASViyaApiClient {
if (!parentFolderUri && parentFolderPath) { if (!parentFolderUri && parentFolderPath) {
parentFolderUri = await this.getFolderUri(parentFolderPath, accessToken); parentFolderUri = await this.getFolderUri(parentFolderPath, accessToken);
if (!parentFolderUri) { if (!parentFolderUri){
console.log(`Parent folder is not present: ${parentFolderPath}`); console.log(`Parent folder is not present: ${parentFolderPath}`);
const newParentFolderPath = parentFolderPath.substring( const newParentFolderPath = parentFolderPath.substring(0, parentFolderPath.lastIndexOf("/"));
0,
parentFolderPath.lastIndexOf("/")
);
const newFolderName = `${parentFolderPath.split("/").pop()}`; const newFolderName = `${parentFolderPath.split("/").pop()}`;
if (newParentFolderPath === "") { if (newParentFolderPath === ""){
throw new Error("Root Folder should have been present on server"); throw new Error("Root Folder should have been present on server");
} }
console.log( console.log(`Creating Parent Folder:\n${newFolderName} in ${newParentFolderPath}`)
`Creating Parent Folder:\n${newFolderName} in ${newParentFolderPath}` const parentFolder = await this.createFolder(newFolderName, newParentFolderPath, undefined, accessToken)
); console.log(`Parent Folder "${newFolderName}" successfully created.`)
const parentFolder = await this.createFolder(
newFolderName,
newParentFolderPath,
undefined,
accessToken
);
console.log(`Parent Folder "${newFolderName}" successfully created.`);
parentFolderUri = `/folders/folders/${parentFolder.id}`; parentFolderUri = `/folders/folders/${parentFolder.id}`;
} }
} }
@@ -401,8 +325,8 @@ export class SASViyaApiClient {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
name: folderName, name: folderName,
type: "folder" type: "folder",
}) }),
}; };
createFolderRequest.headers = { "Content-Type": "application/json" }; createFolderRequest.headers = { "Content-Type": "application/json" };
@@ -410,7 +334,7 @@ export class SASViyaApiClient {
createFolderRequest.headers.Authorization = `Bearer ${accessToken}`; createFolderRequest.headers.Authorization = `Bearer ${accessToken}`;
} }
const { result: createFolderResponse } = await this.request<Folder>( const createFolderResponse = await this.request<Folder>(
`${this.serverUrl}/folders/folders?parentFolderUri=${parentFolderUri}`, `${this.serverUrl}/folders/folders?parentFolderUri=${parentFolderUri}`,
createFolderRequest createFolderRequest
); );
@@ -436,9 +360,7 @@ export class SASViyaApiClient {
accessToken?: string accessToken?: string
) { ) {
if (!parentFolderPath && !parentFolderUri) { if (!parentFolderPath && !parentFolderUri) {
throw new Error( throw new Error('Either parentFolderPath or parentFolderUri must be provided');
"Either parentFolderPath or parentFolderUri must be provided"
);
} }
if (!parentFolderUri && parentFolderPath) { if (!parentFolderUri && parentFolderPath) {
@@ -449,26 +371,26 @@ export class SASViyaApiClient {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/vnd.sas.job.definition+json", "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({ body: JSON.stringify({
name: jobName, name: jobName,
parameters: [ parameters:[
{ {
name: "_addjesbeginendmacros", "name":"_addjesbeginendmacros",
type: "CHARACTER", "type":"CHARACTER",
defaultValue: "false" "defaultValue":"false"
} }
], ],
type: "Compute", type: "Compute",
code code,
}) }),
}; };
if (accessToken) { if (accessToken) {
createJobDefinitionRequest!.headers = { createJobDefinitionRequest!.headers = {
...createJobDefinitionRequest.headers, ...createJobDefinitionRequest.headers,
Authorization: `Bearer ${accessToken}` Authorization: `Bearer ${accessToken}`,
}; };
} }
@@ -487,7 +409,7 @@ export class SASViyaApiClient {
const authCode = await fetch(authUrl, { const authCode = await fetch(authUrl, {
referrerPolicy: "same-origin", referrerPolicy: "same-origin",
credentials: "include" credentials: "include",
}) })
.then((response) => response.text()) .then((response) => response.text())
.then(async (response) => { .then(async (response) => {
@@ -543,7 +465,7 @@ export class SASViyaApiClient {
token = Buffer.from(clientId + ":" + clientSecret).toString("base64"); token = Buffer.from(clientId + ":" + clientSecret).toString("base64");
} }
const headers = { const headers = {
Authorization: "Basic " + token Authorization: "Basic " + token,
}; };
let formData; let formData;
@@ -562,7 +484,7 @@ export class SASViyaApiClient {
credentials: "include", credentials: "include",
headers, headers,
body: formData as any, body: formData as any,
referrerPolicy: "same-origin" referrerPolicy: "same-origin",
}).then((res) => res.json()); }).then((res) => res.json());
return authResponse; return authResponse;
@@ -587,7 +509,7 @@ export class SASViyaApiClient {
token = Buffer.from(clientId + ":" + clientSecret).toString("base64"); token = Buffer.from(clientId + ":" + clientSecret).toString("base64");
} }
const headers = { const headers = {
Authorization: "Basic " + token Authorization: "Basic " + token,
}; };
let formData; let formData;
@@ -606,7 +528,7 @@ export class SASViyaApiClient {
credentials: "include", credentials: "include",
headers, headers,
body: formData as any, body: formData as any,
referrerPolicy: "same-origin" referrerPolicy: "same-origin",
}).then((res) => res.json()); }).then((res) => res.json());
return authResponse; return authResponse;
@@ -626,90 +548,12 @@ export class SASViyaApiClient {
const deleteResponse = await this.request(url, { const deleteResponse = await this.request(url, {
method: "DELETE", method: "DELETE",
credentials: "include", credentials: "include",
headers headers,
}); });
return deleteResponse; return deleteResponse;
} }
/**
* Executes a job via the SAS Viya Compute API
* @param sasJob - the relative path to the job.
* @param contextName - the name of the context where the job is to be executed.
* @param debug - sets the _debug flag in the job arguments.
* @param data - any data to be passed in as input to the job.
* @param accessToken - an optional access token for an authorized user.
*/
public async executeComputeJob(
sasJob: string,
contextName: string,
debug: boolean,
data?: any,
accessToken?: string
) {
if (!this.rootFolder) {
await this.populateRootFolder(accessToken);
}
if (!this.rootFolder) {
console.error("Root folder was not found");
throw new Error("Root folder was not found");
}
if (!this.rootFolderMap.size) {
await this.populateRootFolderMap(accessToken);
}
if (!this.rootFolderMap.size) {
console.error(
`The job ${sasJob} was not found in ${this.rootFolderName}`
);
throw new Error(
`The job ${sasJob} was not found in ${this.rootFolderName}`
);
}
const headers: any = { "Content-Type": "application/json" };
if (!!accessToken) {
headers.Authorization = `Bearer ${accessToken}`;
}
const folderName = sasJob.split("/")[0];
const jobName = sasJob.split("/")[1];
const jobFolder = this.rootFolderMap.get(folderName);
const jobToExecute = jobFolder?.find((item) => item.name === jobName);
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 * Executes a job via the SAS Viya Job Execution API
* @param sasJob - the relative path to the job. * @param sasJob - the relative path to the job.
@@ -745,7 +589,6 @@ export class SASViyaApiClient {
if (data && Object.keys(data).length) { if (data && Object.keys(data).length) {
files = await this.uploadTables(data, accessToken); files = await this.uploadTables(data, accessToken);
} }
const jobName = path.basename(sasJob); const jobName = path.basename(sasJob);
const jobFolder = sasJob.replace(`/${jobName}`, ""); const jobFolder = sasJob.replace(`/${jobName}`, "");
const allJobsInFolder = this.rootFolderMap.get(jobFolder.replace("/", "")); const allJobsInFolder = this.rootFolderMap.get(jobFolder.replace("/", ""));
@@ -755,14 +598,14 @@ export class SASViyaApiClient {
(l) => l.rel === "getResource" (l) => l.rel === "getResource"
)?.href; )?.href;
const requestInfo: any = { const requestInfo: any = {
method: "GET" method: "GET",
}; };
const headers: any = { "Content-Type": "application/json" }; const headers: any = { "Content-Type": "application/json" };
if (!!accessToken) { if (!!accessToken) {
headers.Authorization = `Bearer ${accessToken}`; headers.Authorization = `Bearer ${accessToken}`;
} }
requestInfo.headers = headers; requestInfo.headers = headers;
const { result: jobDefinition } = await this.request<Job>( const jobDefinition = await this.request<Job>(
`${this.serverUrl}${jobDefinitionLink}`, `${this.serverUrl}${jobDefinitionLink}`,
requestInfo requestInfo
); );
@@ -775,19 +618,19 @@ export class SASViyaApiClient {
_OMITJSONLOG: true, _OMITJSONLOG: true,
_OMITSESSIONRESULTS: true, _OMITSESSIONRESULTS: true,
_OMITTEXTLISTING: true, _OMITTEXTLISTING: true,
_OMITTEXTLOG: true _OMITTEXTLOG: true,
}; };
if (debug) { if (debug) {
jobArguments["_OMITTEXTLOG"] = "false"; jobArguments["_omittextlog"] = "false";
jobArguments["_OMITSESSIONRESULTS"] = "false"; jobArguments["_omitsessionresults"] = "false";
jobArguments["_DEBUG"] = 131; jobArguments["_debug"] = 131;
} }
files.forEach((fileInfo, index) => { files.forEach((fileInfo, index) => {
jobArguments[ jobArguments[
`_webin_fileuri${index + 1}` `_webin_fileuri${index + 1}`
] = `/files/files/${fileInfo.file.id}`; ] = `/files/files/${fileInfo.id}`;
jobArguments[`_webin_name${index + 1}`] = fileInfo.tableName; jobArguments[`_webin_name${index + 1}`] = fileInfo.tableName;
}); });
@@ -798,48 +641,28 @@ export class SASViyaApiClient {
name: `exec-${jobName}`, name: `exec-${jobName}`,
description: "Powered by SASjs", description: "Powered by SASjs",
jobDefinition, jobDefinition,
arguments: jobArguments arguments: jobArguments,
}) }),
}; };
const { result: postedJob, etag } = await this.request<Job>( const postedJob = await this.request<Job>(
`${this.serverUrl}/jobExecution/jobs?_action=wait`, `${this.serverUrl}/jobExecution/jobs?_action=wait`,
postJobRequest postJobRequest
); );
const jobStatus = await this.pollJobState( const jobStatus = await this.pollJobState(postedJob, accessToken, true);
postedJob, const currentJob = await this.request<Job>(
etag,
accessToken,
true
);
const { result: currentJob } = await this.request<Job>(
`${this.serverUrl}/jobExecution/jobs/${postedJob.id}`, `${this.serverUrl}/jobExecution/jobs/${postedJob.id}`,
{ headers } { headers }
); );
let jobResult, log;
if (jobStatus === "failed") {
return Promise.reject(currentJob.error);
}
const resultLink = currentJob.results["_webout.json"]; const resultLink = currentJob.results["_webout.json"];
const logLink = currentJob.links.find((l) => l.rel === "log");
if (resultLink) { if (resultLink) {
jobResult = await this.request<any>( const result = await this.request<any>(
`${this.serverUrl}${resultLink}/content`, `${this.serverUrl}${resultLink}/content`,
{ headers }, { headers }
"text"
); );
return result;
} }
if (debug && logLink) {
log = await this.request<any>( return postedJob;
`${this.serverUrl}${logLink.href}/content`,
{
headers
}
).then((res: any) =>
res.result.items.map((i: any) => i.line).join("\n")
);
}
return { result: jobResult?.result, log };
} else { } else {
throw new Error( throw new Error(
`The job ${sasJob} was not found at the location ${this.rootFolderName}` `The job ${sasJob} was not found at the location ${this.rootFolderName}`
@@ -851,19 +674,19 @@ export class SASViyaApiClient {
const allItems = new Map<string, Job[]>(); const allItems = new Map<string, Job[]>();
const url = "/folders/folders/@item?path=" + this.rootFolderName; const url = "/folders/folders/@item?path=" + this.rootFolderName;
const requestInfo: any = { const requestInfo: any = {
method: "GET" method: "GET",
}; };
if (accessToken) { if (accessToken) {
requestInfo.headers = { Authorization: `Bearer ${accessToken}` }; requestInfo.headers = { Authorization: `Bearer ${accessToken}` };
} }
const { result: folder } = await this.request<Folder>( const folder = await this.request<Folder>(
`${this.serverUrl}${url}`, `${this.serverUrl}${url}`,
requestInfo requestInfo
); );
if (!folder) { if (!folder){
throw new Error("Cannot populate RootFolderMap unless rootFolder exists"); throw new Error("Cannot populate RootFolderMap unless rootFolder exists");
} }
const { result: members } = await this.request<{ items: any[] }>( const members = await this.request<{ items: any[] }>(
`${this.serverUrl}/folders/folders/${folder.id}/members`, `${this.serverUrl}/folders/folders/${folder.id}/members`,
requestInfo requestInfo
); );
@@ -878,7 +701,7 @@ export class SASViyaApiClient {
this.rootFolderName + this.rootFolderName +
"/" + "/" +
member.name; member.name;
const { result: memberDetail } = await this.request<Folder>( const memberDetail = await this.request<Folder>(
`${this.serverUrl}${subFolderUrl}`, `${this.serverUrl}${subFolderUrl}`,
requestInfo requestInfo
); );
@@ -887,7 +710,7 @@ export class SASViyaApiClient {
(l: any) => l.rel === "members" (l: any) => l.rel === "members"
); );
const { result: memberContents } = await this.request<{ items: any[] }>( const memberContents = await this.request<{ items: any[] }>(
`${this.serverUrl}${membersLink!.href}`, `${this.serverUrl}${membersLink!.href}`,
requestInfo requestInfo
); );
@@ -903,59 +726,34 @@ export class SASViyaApiClient {
private async populateRootFolder(accessToken?: string) { private async populateRootFolder(accessToken?: string) {
const url = "/folders/folders/@item?path=" + this.rootFolderName; const url = "/folders/folders/@item?path=" + this.rootFolderName;
const requestInfo: RequestInit = { const requestInfo: RequestInit = {
method: "GET" method: "GET",
}; };
if (accessToken) { if (accessToken) {
requestInfo.headers = { Authorization: `Bearer ${accessToken}` }; requestInfo.headers = { Authorization: `Bearer ${accessToken}` };
} }
let error;
const rootFolder = await this.request<Folder>( const rootFolder = await this.request<Folder>(
`${this.serverUrl}${url}`, `${this.serverUrl}${url}`,
requestInfo requestInfo
); ).catch(() => null);
this.rootFolder = rootFolder?.result || null; this.rootFolder = rootFolder;
if (error) {
throw new Error(JSON.stringify(error));
}
} }
private async pollJobState( private async pollJobState(
postedJob: any, postedJob: any,
etag: string | null,
accessToken?: string, accessToken?: string,
silent = false silent = false
) { ) {
const MAX_POLL_COUNT = 1000;
const POLL_INTERVAL = 100;
let postedJobState = ""; let postedJobState = "";
let pollCount = 0; let pollCount = 0;
const headers: any = { const headers: any = {
"Content-Type": "application/json", "Content-Type": "application/json",
"If-None-Match": etag
}; };
if (accessToken) { if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`; headers.Authorization = `Bearer ${accessToken}`;
} }
const stateLink = postedJob.links.find((l: any) => l.rel === "state"); const stateLink = postedJob.links.find((l: any) => l.rel === "state");
if (!stateLink) { return new Promise((resolve, _) => {
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 () => { const interval = setInterval(async () => {
if ( if (
postedJobState === "running" || postedJobState === "running" ||
@@ -966,10 +764,10 @@ export class SASViyaApiClient {
if (!silent) { if (!silent) {
console.log("Polling job status... \n"); console.log("Polling job status... \n");
} }
const { result: jobState } = await this.request<string>( const jobState = await this.request<string>(
`${this.serverUrl}${stateLink.href}?_action=wait&wait=30`, `${this.serverUrl}${stateLink.href}?wait=30`,
{ {
headers headers,
}, },
"text" "text"
); );
@@ -979,7 +777,7 @@ export class SASViyaApiClient {
console.log(`Current state: ${postedJobState}\n`); console.log(`Current state: ${postedJobState}\n`);
} }
pollCount++; pollCount++;
if (pollCount >= MAX_POLL_COUNT) { if (pollCount >= 100) {
resolve(postedJobState); resolve(postedJobState);
} }
} }
@@ -987,57 +785,14 @@ export class SASViyaApiClient {
clearInterval(interval); clearInterval(interval);
resolve(postedJobState); resolve(postedJobState);
} }
}, POLL_INTERVAL); }, 100);
});
}
private async waitForSession(
session: Session,
etag: string | null,
accessToken?: string,
silent = false
) {
let sessionState = session.state;
let pollCount = 0;
const headers: any = {
"Content-Type": "application/json",
"If-None-Match": etag
};
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`;
}
const stateLink = session.links.find((l: any) => l.rel === "state");
return new Promise(async (resolve, _) => {
if (sessionState === "pending") {
if (stateLink) {
if (!silent) {
console.log("Polling session status... \n");
}
const { result: state } = await this.request<string>(
`${this.serverUrl}${stateLink.href}?wait=30`,
{
headers
},
"text"
);
sessionState = state.trim();
if (!silent) {
console.log(`Current state: ${sessionState}\n`);
}
pollCount++;
resolve(sessionState);
}
} else {
resolve(sessionState);
}
}); });
} }
private async uploadTables(data: any, accessToken?: string) { private async uploadTables(data: any, accessToken?: string) {
const uploadedFiles = []; const uploadedFiles = [];
const headers: any = { const headers: any = {
"Content-Type": "application/json" "Content-Type": "application/json",
}; };
if (accessToken) { if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`; headers.Authorization = `Bearer ${accessToken}`;
@@ -1054,43 +809,36 @@ export class SASViyaApiClient {
const createFileRequest = { const createFileRequest = {
method: "POST", method: "POST",
body: csv, body: csv,
headers headers,
}; };
const uploadResponse = await this.request<any>( const file = await this.request<any>(
`${this.serverUrl}/files/files#rawUpload`, `${this.serverUrl}/files/files#rawUpload`,
createFileRequest createFileRequest
); );
uploadedFiles.push({ tableName, file: uploadResponse.result }); uploadedFiles.push({ tableName, file });
} }
return uploadedFiles; return uploadedFiles;
} }
private async getFolderUri(folderPath: string, accessToken?: string) { private async getFolderUri(folderPath: string, accessToken?: string) {
const url = "/folders/folders/@item?path=" + folderPath; const url = "/folders/folders/@item?path=" + folderPath;
const requestInfo: any = { const requestInfo: any = {
method: "GET" method: "GET",
}; };
if (accessToken) { if (accessToken) {
requestInfo.headers = { Authorization: `Bearer ${accessToken}` }; requestInfo.headers = { Authorization: `Bearer ${accessToken}` };
} }
const { result: folder } = await this.request<Folder>( const folder = await this.request<Folder>(
`${this.serverUrl}${url}`, `${this.serverUrl}${url}`,
requestInfo requestInfo
).catch((err) => { );
return { result: null }; if (!folder)
}); return undefined;
return `/folders/folders/${folder.id}`;
if (!folder) return undefined;
return `/folders/folders/${folder.id}`;
} }
setCsrfTokenLocal = (csrfToken: CsrfToken) => {
this.csrfToken = csrfToken;
this.setCsrfToken(csrfToken);
};
private async request<T>( private async request<T>(
url: string, url: string,
options: RequestInit, options: RequestInit,
@@ -1099,13 +847,13 @@ export class SASViyaApiClient {
if (this.csrfToken) { if (this.csrfToken) {
options.headers = { options.headers = {
...options.headers, ...options.headers,
[this.csrfToken.headerName]: this.csrfToken.value [this.csrfToken.headerName]: this.csrfToken.value,
}; };
} }
return await makeRequest<T>( return await makeRequest<T>(
url, url,
options, options,
this.setCsrfTokenLocal, (csrfToken) => (this.csrfToken = csrfToken),
contentType contentType
); );
} }

View File

@@ -2,7 +2,8 @@ import SASjs from "./index";
const adapter = new SASjs(); const adapter = new SASjs();
it("should parse SAS9 source code", async (done) => { // FIXME: adapter doesn't have 'parseSAS9SourceCode' and 'parseGeneratedCode'
it("should parse SAS9 source code", async done => {
expect(sampleResponse).toBeTruthy(); expect(sampleResponse).toBeTruthy();
const parsedSourceCode = (adapter as any).parseSAS9SourceCode(sampleResponse); const parsedSourceCode = (adapter as any).parseSAS9SourceCode(sampleResponse);
expect(parsedSourceCode).toBeTruthy(); expect(parsedSourceCode).toBeTruthy();
@@ -16,7 +17,7 @@ it("should parse SAS9 source code", async (done) => {
done(); done();
}); });
it("should parse generated code", async (done) => { it("should parse generated code", async done => {
expect(sampleResponse).toBeTruthy(); expect(sampleResponse).toBeTruthy();
const parsedGeneratedCode = (adapter as any).parseGeneratedCode( const parsedGeneratedCode = (adapter as any).parseGeneratedCode(
sampleResponse sampleResponse

File diff suppressed because it is too large Load Diff

View File

@@ -1,170 +0,0 @@
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
);
}
}

View File

@@ -1,5 +1,7 @@
import SASjs from "./SASjs"; import SASjs from './SASjs'
export * from "./types";
export * from "./SASViyaApiClient"; export * from './types'
export * from "./SAS9ApiClient"; export * from './SASViyaApiClient'
export default SASjs; export * from './SAS9ApiClient'
export default SASjs

View File

@@ -3,4 +3,4 @@ export interface Context {
id: string; id: string;
createdBy: string; createdBy: string;
version: number; version: number;
} }

View File

@@ -1,4 +1,4 @@
export interface CsrfToken { export interface CsrfToken {
headerName: string; headerName: string;
value: string; value: string;
} }

View File

@@ -4,4 +4,4 @@ export interface Folder {
id: string; id: string;
uri: string; uri: string;
links: Link[]; links: Link[];
} }

View File

@@ -6,8 +6,6 @@ export interface Job {
name: string; name: string;
uri: string; uri: string;
createdBy: string; createdBy: string;
code?: string;
links: Link[]; links: Link[];
results: JobResult; results: JobResult;
error?: any; }
}

View File

@@ -1,3 +0,0 @@
export interface JobDefinition {
code: string;
}

Some files were not shown because too many files have changed in this diff Show More