1
0
mirror of https://github.com/sasjs/adapter.git synced 2025-12-11 09:24:35 +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
50 changed files with 443 additions and 399 deletions

View File

@@ -1,107 +1,114 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8" http-equiv="X-UA-Compatible" content="IE=edge" /> <meta charset='utf-8' http-equiv='X-UA-Compatible' content='IE=edge' />
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous"> <link rel='stylesheet' href='https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css' integrity='sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh' crossorigin='anonymous'>
<script src="https://cdn.jsdelivr.net/combine/npm/chart.js@2.9.3,npm/jquery@3.5.1,npm/@sasjs/adapter@1"></script> <script src='https://cdn.jsdelivr.net/combine/npm/chart.js@2.9.3,npm/jquery@3.5.1,npm/@sasjs/adapter@1'></script>
<script> <script>
var sasJs = new SASjs.default({appLoc: "/Products/demo/readme" const sasJs = new SASjs.default({
,serverType:"SAS9", 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 = () => {
</head> $('#loading-spinner').show()
<body> $('#myChart').remove()
<div class="container-fluid" style="text-align: center; margin-top: 10px;"> $('#chart-container').append("<canvas id='myChart' style='display: none'></canvas>")
<div class="row">
<div class="col-lg-5 col-md-7 col-sm-10 mx-auto mx-auto"> const type = $('#cars')[0].options[$('#cars')[0].selectedIndex].value
<h1>Demo Seed App for <span class="code">SASjs</span></h1>
<div class="login" id="login-form"> // request data from an endpoint under your appLoc
<div class="form-group"> // send data as an array of objects - each object is one row
<input class="form-control" type="text" id="username" placeholder="Enter username" /> sasJs.request('/common/getdata', {fromjs: [{ type: type }]})
.then((response) => {
$('#myChart').show()
$('#loading-spinner').hide()
const labels = response.areas.map(area => area.MAKE)
const data = response.areas.map(area => area.AVPRICE)
initGraph(labels, data, type)
})
}
const initGraph = (labels, data, type) => {
const myCanvas = document.getElementById('myChart')
const ctx = myCanvas.getContext('2d')
const myChart = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: `Average Invoice Price in USD for ${type} Cars by Manufacturer`,
data: data,
backgroundColor: 'rgba(255,99,132,0.2)',
borderColor: 'rgba(255,99,132,1)',
borderWidth: 1,
hoverBackgroundColor: 'rgba(255,99,132,0.4)',
hoverBorderColor: 'rgba(255,99,132,1)',
}]
},
options: {
maintainAspectRatio: false,
scales: {yAxes: [{ticks: {beginAtZero: true}}]}
}
})
}
</script>
</head>
<body>
<div class='container-fluid' style='text-align: center; margin-top: 10px'>
<div class='row'>
<div class='col-lg-5 col-md-7 col-sm-10 mx-auto mx-auto'>
<h1>Demo Seed App for <span class='code'>SASjs</span></h1>
<div class='login' id='login-form'>
<div class='form-group'>
<input class='form-control' type='text' id='username' placeholder='Enter username' />
</div>
<div class='form-group'>
<input class='form-control' type='password' id='password' placeholder='Enter password' />
</div>
<button id='login' onclick='initSasJs()' class='login btn btn-primary' style='margin-bottom: 5px'>Log In</button>
</div>
<select name='cars' id='cars' style='margin-bottom: 5px; display: none' class='form-control'>
<option value='Hybrid'>Hybrid</option>
<option value='SUV'>SUV</option>
<option value='Sedan'>Sedan</option>
<option value='Sports'>Sports</option>
<option value='Truck'>Truck</option>
<option value='Wagon'>Wagon</option>
</select>
<button id='getDataBtn' onclick='getData()' style='margin-bottom: 5px; display: none' class='btn btn-success'>Get Data</button>
<br>
<br>
<div id='loading-spinner' class='spinner-border text-primary' role='status' style='display: none'>
<span class='sr-only'>Loading...</span>
</div>
<br>
</div>
</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>
</body>
</head> </head>

View File

@@ -44,4 +44,4 @@
"devDependencies": { "devDependencies": {
"node-sass": "^4.14.1" "node-sass": "^4.14.1"
} }
} }

View File

@@ -5,5 +5,6 @@ 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

@@ -9,9 +9,7 @@ const App = (): ReactElement<{}> => {
const { adapter } = useContext(AppContext); const { adapter } = useContext(AppContext);
useEffect(() => { useEffect(() => {
if (adapter) { if (adapter) adapter.setDebugState(debug);
adapter.setDebugState(debug);
}
}, [debug, adapter]); }, [debug, adapter]);
useEffect(() => { useEffect(() => {
@@ -33,7 +31,7 @@ const App = (): ReactElement<{}> => {
<label className="switch"> <label className="switch">
<input <input
type="checkbox" type="checkbox"
onChange={(e) => setDebug(e.target.checked)} onChange={(e) => setDebug(e.target.checked)} // FIXME: rename 'e' => 'event'
/> />
<span className="knob"></span> <span className="knob"></span>
</label> </label>
@@ -45,7 +43,7 @@ const App = (): ReactElement<{}> => {
type="text" type="text"
className="app-loc-input" className="app-loc-input"
value={appLoc} value={appLoc}
onChange={(e) => setAppLoc(e.target.value)} onChange={(e) => setAppLoc(e.target.value)} // FIXME: rename 'e' => 'event'
placeholder="AppLoc" placeholder="AppLoc"
/> />
</div> </div>

View File

@@ -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(() => {
appContext.setIsLoggedIn(true); 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

@@ -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

@@ -2,25 +2,24 @@ import React, { useEffect, useState, ReactElement, useContext } from "react";
import TestSuiteComponent from "./components/TestSuite"; import TestSuiteComponent from "./components/TestSuite";
import TestSuiteCard from "./components/TestSuiteCard"; import TestSuiteCard from "./components/TestSuiteCard";
import { TestSuite, Test } from "./types"; import { TestSuite, Test } from "./types";
import { basicTests } from "./testSuites/Basic"; import { basicTests } from "./testSuites/Basic"; // FIXME: declared but never used
import "./TestSuiteRunner.scss"; import "./TestSuiteRunner.scss";
import SASjs from "sasjs"; import SASjs from "sasjs";
import { AppContext } from "./context/AppContext"; import { AppContext } from "./context/AppContext";
import { sendArrTests, sendObjTests } from "./testSuites/RequestData"; import { sendArrTests, sendObjTests } from "./testSuites/RequestData"; // FIXME: declared but never used
import { specialCaseTests } from "./testSuites/SpecialCases"; import { specialCaseTests } from "./testSuites/SpecialCases";
import { sasjsRequestTests } from "./testSuites/SasjsRequests"; import { sasjsRequestTests } from "./testSuites/SasjsRequests"; // FIXME: declared but never used
interface TestSuiteRunnerProps { interface TestSuiteRunnerProps {
adapter: SASjs; adapter: SASjs;
} }
const TestSuiteRunner = (
props: TestSuiteRunnerProps const TestSuiteRunner = (props: TestSuiteRunnerProps): ReactElement<TestSuiteRunnerProps> => {
): ReactElement<TestSuiteRunnerProps> => {
const { adapter } = props; const { adapter } = props;
const { config } = useContext(AppContext); const { config } = useContext(AppContext); // FIXME: declared but never used
const [testSuites, setTestSuites] = useState<TestSuite[]>([]); const [testSuites, setTestSuites] = useState<TestSuite[]>([]);
const [runTests, setRunTests] = useState(false); const [runTests, setRunTests] = useState(false);
const [completedTestSuites, setCompletedTestSuites] = useState< const [completedTestSuites, setCompletedTestSuites] = useState< // FIXME: create interface
{ {
name: string; name: string;
completedTests: { completedTests: {
@@ -65,20 +64,23 @@ const TestSuiteRunner = (
<> <>
<div className="button-container"> <div className="button-container">
<button <button
className={runTests ? "submit-button disabled" : "submit-button"} className={runTests ? "submit-button disabled" : "submit-button"} // TODO: 'submit-button' class should be assigned by default
onClick={() => setRunTests(true)} onClick={() => setRunTests(true)}
disabled={runTests} disabled={runTests}
> >
{runTests ? ( {runTests ? (
<> <>
<div className="loading-spinner"></div>Running tests... {
// FIXME: fragment is not needed in this case
}
<div className="loading-spinner"></div>Running tests...
</> </>
) : ( ) : (
"Run tests!" "Run tests!"
)} )}
</button> </button>
</div> </div>
{completedTestSuites.map((completedTestSuite, index) => { {completedTestSuites.map((completedTestSuite, index) => { // TODO: refactor
return ( return (
<TestSuiteCard <TestSuiteCard
key={index} key={index}
@@ -100,22 +102,19 @@ const TestSuiteRunner = (
}[] }[]
) => { ) => {
const currentIndex = testSuites.indexOf(currentTestSuite); const currentIndex = testSuites.indexOf(currentTestSuite);
const nextIndex = const nextIndex = currentIndex < testSuites.length - 1 ? currentIndex + 1 : -1;
currentIndex < testSuites.length - 1 ? currentIndex + 1 : -1;
if (nextIndex >= 0) { if (nextIndex >= 0) setCurrentTestSuite(testSuites[nextIndex]);
setCurrentTestSuite(testSuites[nextIndex]); else setCurrentTestSuite(null);
} else {
setCurrentTestSuite(null);
}
const newCompletedTestSuites = [ const newCompletedTestSuites = [
...completedTestSuites, ...completedTestSuites,
{ name, completedTests }, { name, completedTests },
]; ];
setCompletedTestSuites(newCompletedTestSuites); setCompletedTestSuites(newCompletedTestSuites);
if (newCompletedTestSuites.length === testSuites.length) { if (newCompletedTestSuites.length === testSuites.length) setRunTests(false);
setRunTests(false);
}
}} }}
/> />
)} )}
@@ -123,4 +122,4 @@ const TestSuiteRunner = (
); );
}; };
export default TestSuiteRunner; export default TestSuiteRunner;

View File

@@ -1,6 +1,6 @@
import React, { ReactElement, useEffect, useState } from "react"; import React, { ReactElement, useEffect, useState } from "react";
import TestCard from "./TestCard"; import TestCard from "./TestCard";
import { start } from "repl"; import { start } from "repl"; // FIXME: declared but never used
interface TestProps { interface TestProps {
title: string; title: string;
@@ -39,28 +39,36 @@ const Test = (props: TestProps): ReactElement<TestProps> => {
useEffect(() => { useEffect(() => {
if (test && assertion) { if (test && assertion) {
const startTime = new Date().valueOf(); const startTime = new Date().valueOf()
setIsRunning(true); setIsRunning(true);
setIsPassed(false); setIsPassed(false);
beforeTestFunction() beforeTestFunction()
.then(() => test(context)) .then(() => test(context))
.then((res) => { .then((res) => {
setIsRunning(false); setIsRunning(false);
setIsPassed(assertion(res, context)); setIsPassed(assertion(res, context))
return Promise.resolve(assertion(res, context)); return Promise.resolve(assertion(res, context));
}) })
.then((testResult) => { .then((testResult) => {
afterTestFunction(); afterTestFunction();
const endTime = new Date().valueOf(); const endTime = new Date().valueOf();
const executionTime = (endTime - startTime) / 1000; const executionTime = (endTime - startTime) / 1000;
onCompleted({ result: testResult, error: null, executionTime }); onCompleted({ result: testResult, error: null, executionTime });
}) })
.catch((e) => { .catch((e) => {
setIsRunning(false); setIsRunning(false);
setIsPassed(false); setIsPassed(false);
console.error(e); console.error(e);
const endTime = new Date().valueOf(); const endTime = new Date().valueOf();
const executionTime = (endTime - startTime) / 1000; const executionTime = (endTime - startTime) / 1000;
onCompleted({ result: false, error: e, executionTime }); onCompleted({ result: false, error: e, executionTime });
}); });
} }
@@ -76,4 +84,4 @@ const Test = (props: TestProps): ReactElement<TestProps> => {
); );
}; };
export default Test; export default Test;

View File

@@ -18,7 +18,7 @@ const TestCard = (props: TestCardProps): ReactElement<TestCardProps> => {
<span className="execution-time"> <span className="execution-time">
{executionTime ? executionTime.toFixed(2) + "s" : ""} {executionTime ? executionTime.toFixed(2) + "s" : ""}
</span> </span>
{status === "running" && ( {status === "running" && ( // FIXME: use switch statement
<div> <div>
<span className="icon running"></span>Running... <span className="icon running"></span>Running...
</div> </div>
@@ -40,4 +40,4 @@ const TestCard = (props: TestCardProps): ReactElement<TestCardProps> => {
); );
}; };
export default TestCard; export default TestCard;

View File

@@ -22,7 +22,7 @@ interface TestSuiteProps {
const TestSuite = (props: TestSuiteProps): ReactElement<TestSuiteProps> => { const TestSuite = (props: TestSuiteProps): ReactElement<TestSuiteProps> => {
const { name, tests, beforeAll, afterAll, onCompleted } = props; const { name, tests, beforeAll, afterAll, onCompleted } = props;
const [context, setContext] = useState<any>(null); const [context, setContext] = useState<any>(null);
const [completedTests, setCompletedTests] = useState< const [completedTests, setCompletedTests] = useState< // TODO: create an interface
{ {
test: Test; test: Test;
result: boolean; result: boolean;
@@ -31,24 +31,21 @@ const TestSuite = (props: TestSuiteProps): ReactElement<TestSuiteProps> => {
}[] }[]
>([]); >([]);
const [currentTest, setCurrentTest] = useState<Test | null>( const [currentTest, setCurrentTest] = useState<Test | null>(
(null as unknown) as Test (null as unknown) as Test // ?
); );
useEffect(() => { useEffect(() => {
if (beforeAll) { if (beforeAll) beforeAll().then((data) => setContext({ data }))
beforeAll().then((data) => setContext({ data }));
}
}, [beforeAll]); }, [beforeAll]);
useEffect(() => { useEffect(() => {
if (tests.length) { if (tests.length) setCurrentTest(tests[0])
setCurrentTest(tests[0]);
}
setCompletedTests([]); setCompletedTests([]);
setContext(null); setContext(null);
}, [tests]); }, [tests]);
return (!!beforeAll && !!context) || !beforeAll ? ( return (!!beforeAll && !!context) || !beforeAll ? ( // ?
<div className="test-suite"> <div className="test-suite">
<div className="test-suite-name running">{name}</div> <div className="test-suite-name running">{name}</div>
{currentTest && ( {currentTest && (
@@ -64,29 +61,27 @@ const TestSuite = (props: TestSuiteProps): ReactElement<TestSuiteProps> => {
error: completedTest.error, error: completedTest.error,
executionTime: completedTest.executionTime, executionTime: completedTest.executionTime,
}, },
]; ]
setCompletedTests(newCompleteTests); setCompletedTests(newCompleteTests);
const currentIndex = tests.indexOf(currentTest); const currentIndex = tests.indexOf(currentTest);
const nextIndex = const nextIndex = currentIndex < tests.length - 1 ? currentIndex + 1 : -1;
currentIndex < tests.length - 1 ? currentIndex + 1 : -1;
if (nextIndex >= 0) { if (nextIndex >= 0) setCurrentTest(tests[nextIndex]);
setCurrentTest(tests[nextIndex]); else setCurrentTest(null);
} else {
setCurrentTest(null);
}
if (newCompleteTests.length === tests.length) { if (newCompleteTests.length === tests.length) {
if (afterAll) { if (afterAll) afterAll().then(() => onCompleted(name, newCompleteTests))
afterAll().then(() => onCompleted(name, newCompleteTests)); else onCompleted(name, newCompleteTests)
} else {
onCompleted(name, newCompleteTests);
}
} }
}} }}
/> />
)} )}
{completedTests.map((completedTest, index) => { {completedTests.map((test, index) => {
const { test, result, error } = completedTest; const { test, result, error } = test;
const { title, description } = test; const { title, description } = test;
return ( return (
<TestCard <TestCard
key={index} key={index}
@@ -99,8 +94,8 @@ const TestSuite = (props: TestSuiteProps): ReactElement<TestSuiteProps> => {
})} })}
</div> </div>
) : ( ) : (
<></> <></> // FIXME: use {null} instead
); );
}; };
export default TestSuite; export default TestSuite;

View File

@@ -12,20 +12,19 @@ interface TestSuiteCardProps {
executionTime: number; executionTime: number;
}[]; }[];
} }
const TestSuiteCard = ( const TestSuiteCard = (props: TestSuiteCardProps): ReactElement<TestSuiteCardProps> => {
props: TestSuiteCardProps
): ReactElement<TestSuiteCardProps> => {
const { name, tests } = props; const { name, tests } = props;
const overallStatus = tests.map((t) => t.result).reduce((x, y) => x && y); const overallStatus = tests.map((t) => t.result).reduce((x, y) => x && y); // TODO: refactor variable names
return ( return (
<div className="test-suite"> <div className="test-suite">
<div className={`test-suite-name ${overallStatus ? "passed" : "failed"}`}> <div className={`test-suite-name ${overallStatus ? "passed" : "failed"}`}>
{name} {name}
</div> </div>
{tests.map((completedTest, index) => { {tests.map((test, index) => {
const { test, result, error, executionTime } = completedTest; const { test, result, error, executionTime } = test;
const { title, description } = test; const { title, description } = test;
return ( return (
<TestCard <TestCard
key={index} key={index}

View File

@@ -1,9 +1,9 @@
import React, { createContext, useState, useEffect, ReactNode } from "react"; import React, { createContext, useState, useEffect, ReactNode } from "react";
import SASjs from "sasjs"; import SASjs from "sasjs";
export const AppContext = createContext<{ export const AppContext = createContext<{ // TODO: create an interface
config: any; config: any; // TODO: be more specific on type declaration
sasJsConfig: any; sasJsConfig: any; // TODO: be more specific on type declaration
isLoggedIn: boolean; isLoggedIn: boolean;
setIsLoggedIn: (value: boolean) => void; setIsLoggedIn: (value: boolean) => void;
adapter: SASjs; adapter: SASjs;
@@ -16,25 +16,24 @@ export const AppContext = createContext<{
}); });
export const AppProvider = (props: { children: ReactNode }) => { export const AppProvider = (props: { children: ReactNode }) => {
const [config, setConfig] = useState<{ sasJsConfig: any }>({ const [config, setConfig] = useState<{ sasJsConfig: any }>({sasJsConfig: null}); // TODO: be more specific on type declaration
sasJsConfig: null,
});
const [adapter, setAdapter] = useState<SASjs>((null as unknown) as SASjs); const [adapter, setAdapter] = useState<SASjs>((null as unknown) as SASjs);
const [isLoggedIn, setIsLoggedIn] = useState(false); const [isLoggedIn, setIsLoggedIn] = useState(false);
useEffect(() => { useEffect(() => {
fetch("config.json") fetch("config.json") // TODO: use axios instead of fetch
.then((res) => res.json()) .then((res) => res.json())
.then((configJson: any) => { .then((configJson: any) => { // TODO: be more specific on type declaration
setConfig(configJson); setConfig(configJson);
const sasjs = new SASjs(configJson.sasJsConfig); const sasjs = new SASjs(configJson.sasJsConfig);
setAdapter(sasjs); setAdapter(sasjs);
sasjs.checkSession().then((response) => { sasjs.checkSession().then((response) => {
setIsLoggedIn(response.isLoggedIn); setIsLoggedIn(response.isLoggedIn);
}); }); // FIXME: add catch block
}); });// FIXME: add catch block
}, []); }, []);
return ( return (
@@ -50,4 +49,4 @@ export const AppProvider = (props: { children: ReactNode }) => {
{props.children} {props.children}
</AppContext.Provider> </AppContext.Provider>
); );
}; };

View File

@@ -33,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 &&

View File

@@ -1,12 +1,12 @@
import SASjs from "sasjs"; import SASjs from "sasjs";
import { TestSuite } from "../types"; 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: "" },
@@ -15,7 +15,7 @@ const multipleRowsWithNulls: any = {
{ 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 },
@@ -25,21 +25,20 @@ const multipleColumnsWithNulls: any = {
], ],
}; };
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,17 @@ 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
return adapter .request("common/sendArr", getLongStringData(32767))
.request("common/sendArr", getLongStringData(32767)) .catch((e) => e), // TODO: rename
.catch((e) => e); assertion: (error: any) => { // TODO: be more specific on type declaration
}, return !!error && !!error.MESSAGE; // FIXME: refactor
assertion: (error: any) => {
return !!error && !!error.MESSAGE;
}, },
}, },
{ {
@@ -88,7 +78,7 @@ 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;
}, },
}, },
@@ -98,7 +88,7 @@ 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 &&
@@ -113,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;
@@ -129,6 +120,7 @@ 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;
}, },
}, },
@@ -138,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] ===
@@ -171,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",
@@ -184,7 +176,7 @@ 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;
}, },
}, },
@@ -195,7 +187,7 @@ 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;
}, },
@@ -209,7 +201,7 @@ 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;
}, },
}, },
@@ -219,7 +211,7 @@ 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;
}, },
}, },
@@ -230,7 +222,7 @@ 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;
}, },
@@ -241,7 +233,7 @@ 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 &&
@@ -256,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;
@@ -281,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 ===
@@ -305,4 +297,4 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
}, },
}, },
], ],
}); });

View File

@@ -1,7 +1,7 @@
import SASjs from "sasjs"; import SASjs from "sasjs";
import { TestSuite } from "../types"; 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,16 +9,11 @@ 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: (res: any) => {
const requests = adapter.getSasRequests(); const requests = adapter.getSasRequests();
if (adapter.getSasjsConfig().debug) {
return requests[0].SASWORK !== null; return adapter.getSasjsConfig().debug ? requests[0].SASWORK !== null : requests[0].SASWORK === null
} else {
return requests[0].SASWORK === null;
}
}, },
}, },
], ],

View File

@@ -1,7 +1,7 @@
import SASjs from "sasjs"; import SASjs from "sasjs";
import { TestSuite } from "../types"; import { TestSuite } from "../types";
const specialCharData: any = { const specialCharData: any = { // TODO: be more specific on type definition
table1: [ table1: [
{ {
tab: "\t", tab: "\t",
@@ -9,8 +9,8 @@ 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",
@@ -18,7 +18,7 @@ const specialCharData: any = {
], ],
}; };
const moreSpecialCharData: any = { const moreSpecialCharData: any = { // TODO: be more specific on type definition
table1: [ table1: [
{ {
speech0: '"speech', speech0: '"speech',
@@ -36,44 +36,46 @@ const moreSpecialCharData: any = {
], ],
}; };
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" }],
}; };
@@ -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 &&
@@ -102,6 +102,8 @@ export const specialCaseTests = (adapter: SASjs): TestSuite => ({
); );
}, },
}, },
// TODO: delete commented out code
// { // {
// title: "Other special characters", // title: "Other special characters",
// description: "Should handle other special characters", // description: "Should handle other special characters",
@@ -180,15 +182,15 @@ export const specialCaseTests = (adapter: SASjs): TestSuite => ({
{ {
title: "Large dataset", title: "Large dataset",
description: "Should handle 5mb of data", description: "Should handle 5mb of data",
test: () => { test: () => adapter.request("common/sendArr", getLargeDataset()),
return adapter.request("common/sendArr", 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;
}, },
}, },
@@ -232,4 +234,4 @@ export const specialCaseTests = (adapter: SASjs): TestSuite => ({
// }, // },
// }, // },
], ],
}); });

View File

@@ -12,4 +12,4 @@ export interface TestSuite {
tests: Test[]; tests: Test[];
beforeAll?: (...args: any) => Promise<any>; beforeAll?: (...args: any) => Promise<any>;
afterAll?: (...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

@@ -3,21 +3,25 @@ export const uploadFile = (file: File, fileName: string, url: string) => {
const data = new FormData(); const data = new FormData();
data.append("file", file); data.append("file", file);
data.append("filename", fileName); data.append("filename", fileName);
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
xhr.withCredentials = true; xhr.withCredentials = true;
xhr.addEventListener("readystatechange", function () { xhr.addEventListener("readystatechange", function () { // TODO: use ES6
if (this.readyState === 4) { if (this.readyState === 4) {
let response: any; let response: any;
try { try {
response = JSON.parse(this.responseText); response = JSON.parse(this.responseText);
} catch (e) { } catch (e) {
reject(e); reject(e);
} }
resolve(response); resolve(response);
} }
}); });
xhr.open("POST", url); xhr.open("POST", url);
xhr.setRequestHeader("cache-control", "no-cache"); xhr.setRequestHeader("cache-control", "no-cache");
xhr.send(data); xhr.send(data);
}); });
}; };

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,3 +1,13 @@
/**
* TODO: needs to be split into logical blocks:
* - Folder
* - Config
* - Context
* - Session
* - Job
* - Auth
*/
import { import {
isAuthorizeFormRequired, isAuthorizeFormRequired,
parseAndSubmitAuthorizeForm, parseAndSubmitAuthorizeForm,
@@ -32,7 +42,7 @@ export class SASViyaApiClient {
if (this.rootFolderMap.size) { if (this.rootFolderMap.size) {
return this.rootFolderMap; return this.rootFolderMap;
} }
this.populateRootFolderMap(); this.populateRootFolderMap();
return this.rootFolderMap; return this.rootFolderMap;
} }

View File

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

View File

@@ -1,3 +1,16 @@
/**
* TODO: needs to be split into logical blocks:
* - Execute
* - Context
* - Session
* - Folder and services
* - Job
* - Auth
* - Config
* - Debug
* - Response
*/
import "isomorphic-fetch"; import "isomorphic-fetch";
import * as e6p from "es6-promise"; import * as e6p from "es6-promise";
(e6p as any).polyfill(); (e6p as any).polyfill();
@@ -839,6 +852,7 @@ export default class SASjs {
} }
} }
// FIXME: this method never used
private fetchLogFileContent(logLink: string) { private fetchLogFileContent(logLink: string) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fetch(logLink, { fetch(logLink, {
@@ -1038,4 +1052,4 @@ export default class SASjs {
); );
}); });
} }
} }

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

@@ -8,4 +8,4 @@ export interface Job {
createdBy: string; createdBy: string;
links: Link[]; links: Link[];
results: JobResult; results: JobResult;
} }

View File

@@ -1,3 +1,3 @@
export interface JobResult { export interface JobResult {
"_webout.json": string; "_webout.json": string;
} }

View File

@@ -4,4 +4,4 @@ export interface Link {
href: string; href: string;
uri: string; uri: string;
type: string; type: string;
} }

View File

@@ -10,6 +10,7 @@ export class SASjsConfig {
* Can be omitted, eg if serving directly from the SAS Web Server or being * Can be omitted, eg if serving directly from the SAS Web Server or being
* streamed. * streamed.
*/ */
// TODO: we should clarify what location we are talking about
serverUrl: string = ""; serverUrl: string = "";
pathSAS9: string = ""; pathSAS9: string = "";
pathSASViya: string = ""; pathSASViya: string = "";

View File

@@ -9,4 +9,4 @@ export interface SASjsRequest {
generatedCode: string; generatedCode: string;
logFile: string; logFile: string;
SASWORK: any; SASWORK: any;
} }

View File

@@ -2,6 +2,8 @@
* Represents requests that are queued, pending a signon event * Represents requests that are queued, pending a signon event
* *
*/ */
// FIXME: be more specific on type declaration
export interface SASjsWaitingRequest { export interface SASjsWaitingRequest {
requestPromise: { requestPromise: {
promise: any; promise: any;

View File

@@ -5,4 +5,4 @@
export enum ServerType { export enum ServerType {
SASViya = "SASVIYA", SASViya = "SASVIYA",
SAS9 = "SAS9", SAS9 = "SAS9",
} }

View File

@@ -1,3 +1,3 @@
export interface Session { export interface Session {
id: string; id: string;
} }

View File

@@ -7,4 +7,4 @@ export * from "./SASjsConfig";
export * from "./SASjsRequest"; export * from "./SASjsRequest";
export * from "./SASjsWaitingRequest"; export * from "./SASjsWaitingRequest";
export * from "./ServerType"; export * from "./ServerType";
export * from "./Session"; export * from "./Session";

View File

@@ -1,5 +1,6 @@
// FIXME: use ES6
export async function asyncForEach(array: any[], callback: any) { export async function asyncForEach(array: any[], callback: any) {
for (let index = 0; index < array.length; index++) { for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array); await callback(array[index], index, array);
} }
} }

View File

@@ -2,7 +2,6 @@ import { SASjsRequest } from "../types/SASjsRequest";
/** /**
* Comparator for SASjs request timestamps * Comparator for SASjs request timestamps
*
*/ */
export const compareTimestamps = (a: SASjsRequest, b: SASjsRequest) => { export const compareTimestamps = (a: SASjsRequest, b: SASjsRequest) => {
return b.timestamp.getTime() - a.timestamp.getTime(); return b.timestamp.getTime() - a.timestamp.getTime();

View File

@@ -3,43 +3,43 @@
* @param data - the JSON object to convert. * @param data - the JSON object to convert.
*/ */
export const convertToCSV = (data: any) => { export const convertToCSV = (data: any) => {
const replacer = (key: any, value: any) => (value === null ? "" : value); const replacer = (key: any, value: any) => (value === null ? "" : value); // FIXME: 'key' parameter was not used, why do we compare with null (undefined, NaN)?
const headerFields = Object.keys(data[0]); const headerFields = Object.keys(data[0]); // FIXME: data can be of any type, but we are working with it as with object
let csvTest; let csvTest;
let invalidString = false; let invalidString = false;
const headers = headerFields.map((field) => { const headers = headerFields.map((field) => {
let firstFoundType: string | null = null; let firstFoundType: string | null = null;
let hasMixedTypes: boolean = false; let hasMixedTypes: boolean = false; // FIXME: unnecessary type declaration
let rowNumError: number = -1; let rowNumError: number = -1; // FIXME: unnecessary type declaration
const longestValueForField = data const longestValueForField = data
.map((row: any, index: number) => { .map((row: any, index: number) => { // FIXME: row should be of type string | number
if (row[field] || row[field] === "") { if (row[field] || row[field] === "") {
if (firstFoundType) { if (firstFoundType) {
let currentFieldType = let currentFieldType = // FIXME: use const
row[field] === "" || typeof row[field] === "string" row[field] === "" || typeof row[field] === "string" // FIXME: "" is also of type string
? "chars" ? "chars"
: "number"; : "number";
if (!hasMixedTypes) { if (!hasMixedTypes) {
hasMixedTypes = currentFieldType !== firstFoundType; hasMixedTypes = currentFieldType !== firstFoundType;
rowNumError = hasMixedTypes ? index + 1 : -1; rowNumError = hasMixedTypes ? index + 1 : -1; // TODO: refactor
} }
} else { } else {
if (row[field] === "") { if (row[field] === "") {
firstFoundType = "chars"; firstFoundType = "chars";
} else { } else {
firstFoundType = firstFoundType =
typeof row[field] === "string" ? "chars" : "number"; typeof row[field] === "string" ? "chars" : "number"; // TODO: refactor
} }
} }
let byteSize; let byteSize;
if (typeof row[field] === "string") { if (typeof row[field] === "string") {
let doubleQuotesFound = row[field] let doubleQuotesFound = row[field] // FIXME: use const
.split("") .split("")
.filter((char: any) => char === '"'); .filter((char: any) => char === '"'); // FIXME: why char is of type any?
byteSize = getByteSize(row[field]); byteSize = getByteSize(row[field]);
@@ -52,16 +52,18 @@ export const convertToCSV = (data: any) => {
} }
}) })
.sort((a: number, b: number) => b - a)[0]; .sort((a: number, b: number) => b - a)[0];
if (longestValueForField && longestValueForField > 32765) {
if (longestValueForField && longestValueForField > 32765) { // FIXME: longestValueForField is an array and it is not comparable to a number
invalidString = true; invalidString = true;
} }
if (hasMixedTypes) { if (hasMixedTypes) {
console.error( console.error(
`Row (${rowNumError}), Column (${field}) has mixed types: ERROR` `Row (${rowNumError}), Column (${field}) has mixed types: ERROR`
); );
} }
return `${field}:${firstFoundType === "chars" ? "$" : ""}${ return `${field}:${firstFoundType === "chars" ? "$" : ""}${ // TODO: format return string before return statement
longestValueForField longestValueForField
? longestValueForField ? longestValueForField
: firstFoundType === "chars" : firstFoundType === "chars"
@@ -73,10 +75,11 @@ export const convertToCSV = (data: any) => {
if (invalidString) { if (invalidString) {
return "ERROR: LARGE STRING LENGTH"; return "ERROR: LARGE STRING LENGTH";
} }
csvTest = data.map((row: any) => { csvTest = data.map((row: any) => {
const fields = Object.keys(row).map((fieldName, index) => { const fields = Object.keys(row).map((fieldName, index) => {
let value; let value;
let containsSpecialChar = false; let containsSpecialChar = false; // FIXME: should be const
const currentCell = row[fieldName]; const currentCell = row[fieldName];
if (JSON.stringify(currentCell).search(/(\\t|\\n|\\r)/gm) > -1) { if (JSON.stringify(currentCell).search(/(\\t|\\n|\\r)/gm) > -1) {
@@ -89,7 +92,7 @@ export const convertToCSV = (data: any) => {
value = value.replace(/\\\\/gm, "\\"); value = value.replace(/\\\\/gm, "\\");
if (containsSpecialChar) { if (containsSpecialChar) {
if (value.includes(",") || value.includes('"')) { if (value.includes(",") || value.includes('"')) { // FIXME: use `"`
value = '"' + value + '"'; value = '"' + value + '"';
} }
} else { } else {
@@ -112,6 +115,7 @@ export const convertToCSV = (data: any) => {
return value; return value;
}); });
return fields.join(","); return fields.join(",");
}); });
@@ -121,6 +125,7 @@ export const convertToCSV = (data: any) => {
return finalCSV; return finalCSV;
}; };
// TODO: refactor
const getByteSize = (str: string) => { const getByteSize = (str: string) => {
let byteSize = str.length; let byteSize = str.length;
for (let i = str.length - 1; i >= 0; i--) { for (let i = str.length - 1; i >= 0; i--) {
@@ -130,4 +135,4 @@ const getByteSize = (str: string) => {
if (code >= 0xdc00 && code <= 0xdfff) i--; //trail surrogate if (code >= 0xdc00 && code <= 0xdfff) i--; //trail surrogate
} }
return byteSize; return byteSize;
}; };

View File

@@ -1,3 +1,3 @@
export const isAuthorizeFormRequired = (response: string): boolean => { export const isAuthorizeFormRequired = (response: string): boolean => {
return /<form.+action="(.*Logon\/oauth\/authorize[^"]*).*>/gm.test(response); return /<form.+action="(.*Logon\/oauth\/authorize[^"]*).*>/gm.test(response);
}; };

View File

@@ -1,5 +1,6 @@
export const isLogInRequired = (response: string): boolean => { export const isLogInRequired = (response: string): boolean => {
const pattern: RegExp = /<form.+action="(.*Logon[^"]*).*>/gm; const pattern: RegExp = /<form.+action="(.*Logon[^"]*).*>/gm; // FIXME: unnecessary type declaration
const matches = pattern.test(response); const matches = pattern.test(response);
return matches; return matches;
}; };

View File

@@ -1,2 +1 @@
export const isLogInSuccess = (response: string): boolean => export const isLogInSuccess = (response: string): boolean => /You have signed in/gm.test(response); // TODO: maybe we have more reliable way to verify that login was successful?
/You have signed in/gm.test(response);

View File

@@ -2,30 +2,33 @@ import { CsrfToken } from "../types";
export async function makeRequest<T>( export async function makeRequest<T>(
url: string, url: string,
request: RequestInit, request: RequestInit, // Where 'RequestInit' is coming from?
callback: (value: CsrfToken) => any, callback: (value: CsrfToken) => any,
contentType: "text" | "json" = "json" contentType: "text" | "json" = "json"
): Promise<T> { ): Promise<T> {
const responseTransform = const responseTransform = contentType === "json" ?
contentType === "json" (res: Response) => res.json()
? (res: Response) => res.json() :
: (res: Response) => res.text(); (res: Response) => res.text();
const result = await fetch(url, request).then((response) => {
if (!response.ok) { const result = await fetch(url, request).then((response) => { // FIXME: use axios instead of fetch
if (!response.ok) { // FIXME we can just check if status === 403
if (response.status === 403) { if (response.status === 403) {
const tokenHeader = response.headers.get("X-CSRF-HEADER"); const tokenHeader = response.headers.get("X-CSRF-HEADER");
if (tokenHeader) { if (tokenHeader) {
const token = response.headers.get(tokenHeader); const token = response.headers.get(tokenHeader) || ''; // TODO: refactor
callback({ callback({
headerName: tokenHeader, headerName: tokenHeader,
value: token || "", value: token,
}); });
const retryRequest = { const retryRequest = {
...request, ...request,
headers: { ...request.headers, [tokenHeader]: token }, headers: { ...request.headers, [tokenHeader]: token },
}; };
return fetch(url, retryRequest).then(responseTransform); return fetch(url, retryRequest).then(responseTransform);
} }
} }
@@ -33,5 +36,6 @@ export async function makeRequest<T>(
return responseTransform(response); return responseTransform(response);
} }
}); });
return result; return result;
} }

View File

@@ -1,3 +1,4 @@
// TODO: refactor
export const needsRetry = (responseText: string): boolean => { export const needsRetry = (responseText: string): boolean => {
return ( return (
(responseText.includes('"errorCode":403') && (responseText.includes('"errorCode":403') &&
@@ -8,4 +9,4 @@ export const needsRetry = (responseText: string): boolean => {
(responseText.includes('"status":449') && (responseText.includes('"status":449') &&
responseText.includes("Authentication success, retry original request")) responseText.includes("Authentication success, retry original request"))
); );
}; };

View File

@@ -6,7 +6,7 @@ export const parseAndSubmitAuthorizeForm = async (
const params: any = {}; const params: any = {};
const responseBody = response.split("<body>")[1].split("</body>")[0]; const responseBody = response.split("<body>")[1].split("</body>")[0];
const bodyElement = document.createElement("div"); const bodyElement = document.createElement("div"); // TODO: rename
bodyElement.innerHTML = responseBody; bodyElement.innerHTML = responseBody;
const form = bodyElement.querySelector("#application_authorization"); const form = bodyElement.querySelector("#application_authorization");
@@ -24,7 +24,7 @@ export const parseAndSubmitAuthorizeForm = async (
const formData = new FormData(); const formData = new FormData();
for (const key in params) { for (const key in params) { // TODO: use forEach
if (params.hasOwnProperty(key)) { if (params.hasOwnProperty(key)) {
formData.append(key, params[key]); formData.append(key, params[key]);
} }
@@ -32,7 +32,7 @@ export const parseAndSubmitAuthorizeForm = async (
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (authUrl) { if (authUrl) {
fetch(authUrl, { fetch(authUrl, { // TODO use axios instead of fetch
method: "POST", method: "POST",
credentials: "include", credentials: "include",
body: formData, body: formData,
@@ -46,4 +46,4 @@ export const parseAndSubmitAuthorizeForm = async (
reject("Auth form url is null"); reject("Auth form url is null");
} }
}); });
}; };

View File

@@ -1,7 +1,7 @@
export const parseGeneratedCode = (log: string) => { export const parseGeneratedCode = (log: string) => {
const startsWith = "MPRINT"; const startsWith = "MPRINT";
const isGeneratedCodeLine = (line: string) => const isGeneratedCodeLine = (line: string) => line.trim().startsWith(startsWith);
line.trim().startsWith(startsWith);
const logLines = log.split("\n").filter(isGeneratedCodeLine); const logLines = log.split("\n").filter(isGeneratedCodeLine);
return logLines.join("\r\n"); return logLines.join("\r\n");
}; };

View File

@@ -1,12 +1,15 @@
export const parseSasViyaLog = (logResponse: { items: any[] }) => { export const parseSasViyaLog = (logResponse: { items: any[] }) => { // TODO: be more specific on type declaration
let log; let log;
try { try {
log = logResponse.items log = logResponse.items
? logResponse.items.map((i) => i.line).join("\n") ? logResponse.items.map((i) => i.line).join("\n")
: JSON.stringify(logResponse); : JSON.stringify(logResponse);
} catch (e) { } catch (e) { // TODO: rename parameter to err or error
console.error("An error has occurred while parsing the log response", e); console.error("An error has occurred while parsing the log response", e);
log = logResponse; log = logResponse;
} }
return log; return log;
}; };

View File

@@ -1,6 +1,6 @@
export const parseSourceCode = (log: string): string => { export const parseSourceCode = (log: string): string => {
const isSourceCodeLine = (line: string) => const isSourceCodeLine = (line: string) => line.trim().substring(0, 10).trimStart().match(/^\d/);
line.trim().substring(0, 10).trimStart().match(/^\d/);
const logLines = log.split("\n").filter(isSourceCodeLine); const logLines = log.split("\n").filter(isSourceCodeLine);
return logLines.join("\r\n"); return logLines.join("\r\n");
}; };

View File

@@ -1,6 +1,7 @@
export const serialize = (obj: any) => { export const serialize = (obj: any) => { // TODO: be more specific on type declaration
const str: any[] = []; const str: any[] = [];
for (const p in obj) {
for (const p in obj) { // FIXME: name variables properly
if (obj.hasOwnProperty(p)) { if (obj.hasOwnProperty(p)) {
if (obj[p] instanceof Array) { if (obj[p] instanceof Array) {
for (let i = 0, n = obj[p].length; i < n; i++) { for (let i = 0, n = obj[p].length; i < n; i++) {
@@ -11,5 +12,6 @@ export const serialize = (obj: any) => {
} }
} }
} }
return str.join("&"); return str.join("&");
}; };

View File

@@ -1,10 +1,10 @@
export const splitChunks = (content: string) => { export const splitChunks = (content: string) => { // TODO: set return type
const size = 16000; const size = 16000; // why 16000?
const numChunks = Math.ceil(content.length / size); const numChunks = Math.ceil(content.length / size);
const chunks = new Array(numChunks); const chunks = new Array(numChunks);
for (let i = 0, o = 0; i < numChunks; ++i, o += size) { for (let i = 0, o = 0; i < numChunks; ++i, o += size) { // FIXME: name variables properly
chunks[i] = content.substr(o, size); chunks[i] = content.substr(o, size);
} }