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

Compare commits

...

44 Commits

Author SHA1 Message Date
Yury Shkoda
657721d7a3 Merge pull request #736 from sasjs/issue-735
fix(refresh-token): improved error message
2022-07-18 16:56:58 +03:00
Yury Shkoda
a39faa0f4b fix(refresh-token): improved error message 2022-07-18 14:35:33 +03:00
Allan Bowe
7b8fb774cc Update dependabot.yml 2022-07-13 22:25:50 +01:00
Allan Bowe
982c4c329c Merge pull request #734 from sasjs/issue-733
fix: special missing double dot issue
2022-07-07 19:23:02 +01:00
8617e2dc57 fix: special missing double dot issue 2022-07-07 17:58:27 +02:00
Allan Bowe
d3d62f6888 Merge pull request #731 from sasjs/issue-730
fix: do not throw job execution error if response contains >>weboutBEGIN<<
2022-06-29 16:33:03 +02:00
Allan Bowe
bf35e52962 Merge pull request #713 from sasjs/critical-deps-issues
Fixed critical dependencies issues
2022-06-29 14:24:29 +02:00
22eca50e3f fix: do not throw job execution error if response contains >>weboutBEGIN<< 2022-06-29 16:59:03 +05:00
Yury Shkoda
eb83101dbf fix(sasjs-test): addede appLoc to useEffect deps 2022-06-29 08:46:52 +03:00
Yury Shkoda
56d84e1940 fix(sasjs-tests): used appLoc from config 2022-06-29 08:37:59 +03:00
Yury Shkoda
283800dfa6 fix(special-missings): fixed formats table sent as part of sasjs_tables 2022-06-28 10:17:22 +03:00
Yury Shkoda
c073d72dd4 chore(deps): regenerated package-locks 2022-06-24 16:16:36 +03:00
Yury Shkoda
f5d40eaaf7 chore: Merge branch 'deps-fix' into critical-deps-issues 2022-06-24 16:13:43 +03:00
Yury Shkoda
8e9cf98985 fix(deps): semantic-release, @sasjs/test-framework 2022-06-24 15:47:34 +03:00
Allan Bowe
79ba044dea Update README.md 2022-06-24 11:49:48 +01:00
Allan Bowe
9329dc848a Update README.md 2022-06-24 11:46:09 +01:00
Allan Bowe
98c492e85e Merge pull request #729 from sasjs/update-AuthManager
fix: update logout url
2022-06-21 22:10:26 +02:00
d1fcc2ca0a fix: update logout url 2022-06-22 00:53:49 +05:00
Yury Shkoda
122f302bae Merge pull request #728 from sasjs/deps-fix
fix(workflow): added actions/setup-node@v2
2022-06-20 20:44:57 +03:00
Yury Shkoda
c3a0ad1f41 fix(workflow): added actions/setup-node@v2 2022-06-20 20:43:11 +03:00
Yury Shkoda
a28b48f815 Merge pull request #726 from sasjs/deps-fix
fix(workflows): fixed npmpublish workflow
2022-06-20 20:36:13 +03:00
Yury Shkoda
9b6a42e412 fix(workflows): fixed npmpublish workflow 2022-06-20 20:33:44 +03:00
Allan Bowe
db60962c1e Merge pull request #725 from sasjs/allanbowe-patch-1
fix: bumping with README updates
2022-06-20 19:29:15 +02:00
Allan Bowe
1eae59ad3b fix: bumping with README updates 2022-06-20 18:28:47 +01:00
Allan Bowe
d485023d65 Update dependabot.yml 2022-06-20 18:09:07 +01:00
Allan Bowe
c2f21babb4 Merge pull request #723 from sasjs/deps-fix
Regenerated package-lock and fixed linting issues
2022-06-20 19:03:17 +02:00
Yury Shkoda
dd788ae423 chore(lint): fixed linting 2022-06-20 19:48:42 +03:00
Yury Shkoda
a113c95441 chore(deps): added prettier dev dependency 2022-06-20 19:36:12 +03:00
Yury Shkoda
489947bcae chore(lint): fixed linting issues 2022-06-20 19:26:29 +03:00
Yury Shkoda
1596173dda fix(deps): regenerated package-lock 2022-06-20 19:24:09 +03:00
Allan Bowe
bb1b2ddcb2 Merge pull request #719 from sasjs/issue-718
fix: parse the logs before appending the request to request array whe…
2022-06-20 17:53:44 +02:00
a3cc274ef1 chore: no need to appendRequest from then block when there is jobExecutionError 2022-06-10 15:38:03 +05:00
451d0906fa chore: update error message 2022-06-10 15:31:09 +05:00
dd6b89b0d0 fix: parse the logs before appending the request to request array when server type is sasjs 2022-06-09 23:11:47 +05:00
Yury Shkoda
f602d5baf0 chore(deps): added prettier 2022-06-01 10:08:50 +03:00
Yury Shkoda
4744dbf196 fix(deps): fixed critical vulnerabilities 2022-06-01 09:53:22 +03:00
Allan Bowe
f0525c5796 Merge pull request #674 from sasjs/numeric-missing
fix: special missings accept - regular missing .
2022-05-23 14:11:30 +03:00
Allan Bowe
01bcfe176a Merge pull request #712 from sasjs/issue-711
When expires csrf token, re-fetch and empty webout fix
2022-05-23 14:03:42 +03:00
076ed1cc7a chore: added special missing test 2022-05-23 12:59:48 +02:00
0a3289b577 chore(git): Merge branch 'master' into numeric-missing 2022-05-23 12:47:13 +02:00
Allan Bowe
cbb55ff426 chore: updating README in server tests to deploy backend 2022-05-20 17:43:49 +00:00
6d47174a5e fix: csrf token fetch and empty webout promise finish 2022-05-20 16:43:46 +02:00
d2ea67e5d6 chore: docs 2022-03-08 18:32:45 +01:00
b0df4cb7ee fix: special missings accept - regular missing . 2022-03-08 18:28:48 +01:00
42 changed files with 18202 additions and 23381 deletions

View File

@@ -1,7 +1,7 @@
version: 2
updates:
- package-ecosystem: npm
directory: '/'
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: monthly
open-pull-requests-limit: 2
interval: "monthly"
open-pull-requests-limit: 1

View File

@@ -11,10 +11,19 @@ on:
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [lts/fermium]
steps:
- name: Checkout
uses: actions/checkout@v2
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
cache: npm
- name: Install Dependencies
run: npm ci
@@ -23,7 +32,7 @@ jobs:
- name: Build Project
run: npm run build
- name: Semantic Release
uses: cycjimmy/semantic-release-action@v2
env:

View File

@@ -2,7 +2,6 @@
[![npm package][npm-image]][npm-url]
[![Github Workflow][githubworkflow-image]][githubworkflow-url]
[![Dependency Status][dependency-image]][dependency-url]
[![npm](https://img.shields.io/npm/dt/@sasjs/adapter)]()
![Snyk Vulnerabilities for npm package](https://img.shields.io/snyk/vulnerabilities/npm/@sasjs/adapter)
[![License](https://img.shields.io/apm/l/atomic-design-ui.svg)](/LICENSE)
@@ -16,7 +15,6 @@
[githubworkflow-image]:https://github.com/sasjs/adapter/actions/workflows/build.yml/badge.svg
[githubworkflow-url]:https://github.com/sasjs/adapter/blob/main/.github/workflows/build.yml
[dependency-image]:https://david-dm.org/sasjs/adapter.svg
[dependency-url]:https://github.com/sasjs/adapter/blob/main/package.json
SASjs is a open-source framework for building Web Apps on SAS® platforms. You can use as much or as little of it as you like. This repository contains the JS adapter, the part that handles the to/from SAS communication on the client side. There are 3 ways to install it:
@@ -32,7 +30,7 @@ For more information on building web apps with SAS, check out [sasjs.io](https:/
## 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://raw.githubusercontent.com/sasjs/adapter/master/example.html) file to your web server, and update `servertype` to `SAS9`, `SASVIYA`, or `SASJS` depending on your backend.
The backend part can be deployed as follows:
@@ -52,7 +50,7 @@ parmcards4;
%webout(OBJ,areas)
%webout(CLOSE)
;;;;
%mp_createwebservice(path=&appLoc/common,name=getdata)
%mx_createwebservice(path=&appLoc/common,name=getdata)
```
You now have a simple web app with a backend service!
@@ -96,10 +94,10 @@ const sasJs = new SASjs({your config})
More on the config later.
### SAS Logon
All authentication from the adapter is done against SASLogon. There are two approaches that can be taken, which are configured using the `LoginMechanism` attribute of the sasJs config object (above):
All authentication from the adapter is done against SASLogon. There are two approaches that can be taken, which are configured using the `loginMechanism` attribute of the sasJs config object (above):
* `LoginMechanism:'Redirected'` - this approach enables authentication through a SASLogon window, supporting complex authentication flows (such as 2FA) and avoids the need to handle passwords in the application itself. The styling of the window can be modified using CSS.
* `LoginMechanism:'Default'` - this approach requires that the username and password are captured, and used within the `.login()` method. This can be helpful for development, or automated testing.
* `loginMechanism:'Redirected'` - this approach enables authentication through a SASLogon window, supporting complex authentication flows (such as 2FA) and avoids the need to handle passwords in the application itself. The styling of the window can be modified using CSS.
* `loginMechanism:'Default'` - this approach requires that the username and password are captured, and used within the `.login()` method. This can be helpful for development, or automated testing.
Sample code for logging in with the `Default` approach:
@@ -127,7 +125,11 @@ sasJs.request("/path/to/my/service", dataObject)
})
```
We supply the path to the SAS service, and a data object. The data object can be null (for services with no input), or can contain one or more tables in the following format:
We supply the path to the SAS service, and a data object.
If the path starts with a `/` then it should be a full path to the service. If there is no leading `/` then it is relative to the `appLoc`.
The data object can be null (for services with no input), or can contain one or more "tables" in the following format:
```javascript
let dataObject={
@@ -143,7 +145,9 @@ let dataObject={
};
```
There are optional parameters such as a config object and a callback login function.
These tables (`tablewith2cols1row` and `tablewith1col2rows`) will be created in SAS WORK after running `%webout(FETCH)` in your SAS service.
The `request()` method also has optional parameters such as a config object and a callback login function.
The response object will contain returned tables and columns. Table names are always lowercase, and column names uppercase.
@@ -155,7 +159,7 @@ The SAS type (char/numeric) of the values is determined according to a set of ru
* If the values are numeric, the SAS type is numeric
* If the values are all string, the SAS type is character
* If the values contain a single character (a-Z + underscore) AND a numeric, then the SAS type is numeric (with special missing values).
* If the values contain a single character (a-Z + underscore + .) AND a numeric, then the SAS type is numeric (with special missing values).
* `null` is set to either '.' or '' depending on the assigned or derived type per the above rules. If entire column is `null` then the type will be numeric.
The following table illustrates the formats applied to columns under various scenarios:
@@ -221,7 +225,7 @@ The SAS side is handled by a number of macros in the [macro core](https://github
The following snippet shows the process of SAS tables arriving / leaving:
```sas
/* fetch all input tables sent from frontend - they arrive as work tables */
/* convert frontend input tables from into SASWORK datasets */
%webout(FETCH)
/* some sas code */
@@ -250,6 +254,8 @@ Where an entire column is made up of special missing numerics, there would be no
%webout(OBJ,a,missing=STRING,showmeta=YES)
```
The `%webout()` macro itself is just a wrapper for the [mp_jsonout](https://core.sasjs.io/mp__jsonout_8sas.html) macro.
## Configuration
Configuration on the client side involves passing an object on startup, which can also be passed with each request. Technical documentation on the SASjsConfig class is available [here](https://adapter.sasjs.io/classes/types.sasjsconfig.html). The main config items are:
@@ -258,7 +264,7 @@ Configuration on the client side involves passing an object on startup, which ca
* `serverType` - either `SAS9`, `SASVIYA` or `SASJS`. The `SASJS` server type is for use with [sasjs/server](https://github.com/sasjs/server).
* `serverUrl` - the location (including http protocol and port) of the SAS Server. Can be omitted, eg if serving directly from the SAS Web Server, or in streaming mode.
* `debug` - if `true` then SAS Logs and extra debug information is returned.
* `LoginMechanism` - either `Default` or `Redirected`. See [SAS Logon](#sas-logon) section.
* `loginMechanism` - either `Default` or `Redirected`. See [SAS Logon](#sas-logon) section.
* `useComputeApi` - Only relevant when the serverType is `SASVIYA`. If `true` the [Compute API](#using-the-compute-api) is used. If `false` the [JES API](#using-the-jes-api) is used. If `null` or `undefined` the [Web](#using-jes-web-app) approach is used.
* `contextName` - Compute context on which the requests will be called. If missing or not provided, defaults to `Job Execution Compute context`.
* `requestHistoryLimit` - Request history limit. Increasing this limit may affect browser performance, especially with debug (logs) enabled. Default is 10.
@@ -314,7 +320,7 @@ For more information and examples specific to this adapter you can check out the
For more information on building web apps in general, check out these [resources](https://sasjs.io/training/resources/) or contact the [author](https://www.linkedin.com/in/allanbowe/) directly.
If you are a SAS 9 or SAS Viya customer you can also request a copy of [Data Controller](https://datacontroller.io) - free for up to 5 users, this tool makes use of all parts of the SASjs framework.
As a SAS customer you can also request a copy of [Data Controller](https://datacontroller.io) - free for up to 5 users, this tool makes use of all parts of the SASjs framework.
## Star Gazing

10253
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -44,18 +44,16 @@
"license": "ISC",
"devDependencies": {
"@cypress/webpack-preprocessor": "5.9.1",
"@types/form-data": "2.5.0",
"cypress": "7.7.0",
"typedoc-neo-theme": "1.1.1",
"typedoc-plugin-external-module-name": "4.0.6",
"@types/axios": "0.14.0",
"@types/express": "4.17.13",
"@types/form-data": "2.5.0",
"@types/jest": "27.4.0",
"@types/mime": "2.0.3",
"@types/pem": "1.9.6",
"@types/tough-cookie": "4.0.1",
"copyfiles": "2.4.1",
"cp": "0.2.0",
"cypress": "7.7.0",
"dotenv": "16.0.0",
"express": "4.17.3",
"jest": "27.4.7",
@@ -63,15 +61,18 @@
"node-polyfill-webpack-plugin": "1.1.4",
"path": "0.12.7",
"pem": "1.14.6",
"prettier": "2.7.1",
"process": "0.11.10",
"rimraf": "3.0.2",
"semantic-release": "18.0.0",
"semantic-release": "19.0.3",
"terser-webpack-plugin": "5.3.1",
"ts-jest": "27.1.3",
"ts-loader": "9.2.6",
"tslint": "6.1.3",
"tslint-config-prettier": "1.18.0",
"typedoc": "0.22.11",
"typedoc-neo-theme": "1.1.1",
"typedoc-plugin-external-module-name": "4.0.6",
"typedoc-plugin-rename-defaults": "0.4.0",
"typescript": "4.5.5",
"webpack": "5.69.0",

View File

@@ -60,7 +60,7 @@ If you'd like to deploy just `sasjs-tests` without changing the adapter version,
The below services need to be created on your SAS server, at the location specified as the `appLoc` in the SASjs configuration.
### SAS 9
The code below will work on ALL SAS platforms (Viya, SAS 9 EBI, SASjs Server).
```sas
filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
@@ -70,76 +70,32 @@ parmcards4;
%webout(FETCH)
%webout(OPEN)
%macro x();
%do i=1 %to &_webin_file_count; %webout(OBJ,&&_webin_name&i,missing=STRING,showmeta=YES) %end;
%mend; %x()
%webout(CLOSE)
;;;;
%mm_createwebservice(path=/Public/app/common,name=sendObj)
parmcards4;
%webout(FETCH)
%webout(OPEN)
%macro x();
%do i=1 %to &_webin_file_count; %webout(ARR,&&_webin_name&i,missing=STRING,showmeta=YES) %end;
%mend; %x()
%webout(CLOSE)
;;;;
%mm_createwebservice(path=/Public/app/common,name=sendArr)
parmcards4;
data work.macvars;
set sashelp.vmacro;
run;
%webout(OPEN)
%webout(OBJ,macvars)
%webout(CLOSE)
;;;;
%mm_createwebservice(path=/Public/app/common,name=sendMacVars)
parmcards4;
let he who hath understanding, reckon the number of the beast
;;;;
%mm_createwebservice(path=/Public/app/common,name=makeErr)
parmcards4;
%webout(OPEN)
data _null_;
file _webout;
put ' the discovery channel ';
run;
%webout(CLOSE)
;;;;
%mm_createwebservice(path=/Public/app/common,name=invalidJSON)
```
### SAS Viya
```sas
filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
%inc mc;
filename ft15f001 temp lrecl=1000;
parmcards4;
%webout(FETCH)
%webout(OPEN)
%macro x();
%do i=1 %to %sysfunc(countw(&sasjs_tables));
%if %symexist(sasjs_tables) %then %do i=1 %to %sysfunc(countw(&sasjs_tables));
%let table=%scan(&sasjs_tables,&i);
%webout(OBJ,&table,missing=STRING,showmeta=YES)
%end;
%mend;
%x()
%else %do i=1 %to &_webin_file_count;
%webout(OBJ,&&_webin_name&i,missing=STRING,showmeta=YES)
%end;
%mend; %x()
%webout(CLOSE)
;;;;
%mp_createwebservice(path=/Public/app/common,name=sendObj)
%mx_createwebservice(path=/Public/app/common,name=sendObj)
parmcards4;
%webout(FETCH)
%webout(OPEN)
%macro x();
%do i=1 %to %sysfunc(countw(&sasjs_tables));
%if %symexist(sasjs_tables) %then %do i=1 %to %sysfunc(countw(&sasjs_tables));
%let table=%scan(&sasjs_tables,&i);
%webout(ARR,&table,missing=STRING,showmeta=YES)
%end;
%mend;
%x()
%else %do i=1 %to &_webin_file_count;
%webout(ARR,&&_webin_name&i,missing=STRING,showmeta=YES)
%end;
%mend; %x()
%webout(CLOSE)
;;;;
%mp_createwebservice(path=/Public/app/common,name=sendArr)
%mx_createwebservice(path=/Public/app/common,name=sendArr)
parmcards4;
data work.macvars;
set sashelp.vmacro;
@@ -148,14 +104,14 @@ parmcards4;
%webout(OBJ,macvars)
%webout(CLOSE)
;;;;
%mp_createwebservice(path=/Public/app/common,name=sendMacVars)
%mx_createwebservice(path=/Public/app/common,name=sendMacVars)
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)
%mx_createwebservice(path=/Public/app/common,name=makeErr)
parmcards4;
%webout(OPEN)
data _null_;
@@ -164,7 +120,7 @@ data _null_;
run;
%webout(CLOSE)
;;;;
%mp_createwebservice(path=/Public/app/common,name=invalidJSON)
%mx_createwebservice(path=/Public/app/common,name=invalidJSON)
```
You should now be able to access the tests in your browser at the deployed path on your server.

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@
"private": true,
"dependencies": {
"@sasjs/adapter": "file:../build/sasjs-adapter-5.0.0.tgz",
"@sasjs/test-framework": "^1.4.3",
"@sasjs/test-framework": "^1.5.6",
"@types/jest": "^26.0.20",
"@types/node": "^14.14.41",
"@types/react": "^17.0.1",
@@ -14,7 +14,7 @@
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-router-dom": "^5.2.0",
"react-scripts": "^4.0.2",
"react-scripts": "^5.0.1",
"typescript": "^4.1.3"
},
"scripts": {
@@ -43,6 +43,6 @@
]
},
"devDependencies": {
"node-sass": "^6.0.1"
"node-sass": "^7.0.1"
}
}

View File

@@ -11,12 +11,13 @@ import { fileUploadTests } from './testSuites/FileUpload'
const App = (): ReactElement<{}> => {
const { adapter, config } = useContext(AppContext)
const [testSuites, setTestSuites] = useState<TestSuite[]>([])
const appLoc = config.sasJsConfig.appLoc
useEffect(() => {
if (adapter) {
const testSuites = [
basicTests(adapter, config.userName, config.password),
sendArrTests(adapter),
sendArrTests(adapter, appLoc),
sendObjTests(adapter),
specialCaseTests(adapter),
sasjsRequestTests(adapter),
@@ -24,12 +25,12 @@ const App = (): ReactElement<{}> => {
]
if (adapter.getSasjsConfig().serverType === 'SASVIYA') {
testSuites.push(computeTests(adapter))
testSuites.push(computeTests(adapter, appLoc))
}
setTestSuites(testSuites)
}
}, [adapter, config])
}, [adapter, config, appLoc])
return (
<div className="app">

View File

@@ -9,7 +9,7 @@ const Login = (): ReactElement<{}> => {
const appContext = useContext(AppContext)
const handleSubmit = useCallback(
(e) => {
(e: any) => {
e.preventDefault()
appContext.adapter.logIn(username, password).then((res) => {
appContext.setIsLoggedIn(res.isLoggedIn)
@@ -28,7 +28,7 @@ const Login = (): ReactElement<{}> => {
placeholder="User Name"
value={username}
required
onChange={(e) => setUsername(e.target.value)}
onChange={(e: any) => setUsername(e.target.value)}
/>
</div>
<div className="row">
@@ -38,7 +38,7 @@ const Login = (): ReactElement<{}> => {
type="password"
value={password}
required
onChange={(e) => setPassword(e.target.value)}
onChange={(e: any) => setPassword(e.target.value)}
/>
</div>
<button type="submit" className="submit-button">

View File

@@ -3,7 +3,7 @@ import { TestSuite } from '@sasjs/test-framework'
const stringData: any = { table1: [{ col1: 'first col value' }] }
export const computeTests = (adapter: SASjs): TestSuite => ({
export const computeTests = (adapter: SASjs, appLoc: string): TestSuite => ({
name: 'Compute',
tests: [
{
@@ -35,7 +35,7 @@ export const computeTests = (adapter: SASjs): TestSuite => ({
description: 'Should start a compute job and return the session',
test: () => {
const data: any = { table1: [{ col1: 'first col value' }] }
return adapter.startComputeJob('/Public/app/common/sendArr', data)
return adapter.startComputeJob(`${appLoc}/common/sendArr`, data)
},
assertion: (res: any) => {
const expectedProperties = ['id', 'applicationName', 'attributes']

View File

@@ -45,14 +45,14 @@ const getLargeObjectData = () => {
return data
}
export const sendArrTests = (adapter: SASjs): TestSuite => ({
export const sendArrTests = (adapter: SASjs, appLoc: string): TestSuite => ({
name: 'sendArr',
tests: [
{
title: 'Absolute paths',
description: 'Should work with absolute paths to SAS jobs',
test: () => {
return adapter.request('/Public/app/common/sendArr', stringData)
return adapter.request(`${appLoc}/common/sendArr`, stringData)
},
assertion: (res: any) => {
return res.table1[0][0] === stringData.table1[0].col1
@@ -86,7 +86,7 @@ export const sendArrTests = (adapter: SASjs): TestSuite => ({
'Should error out with long string values over 32765 characters',
test: () => {
const data = getLongStringData(32767)
return adapter.request('common/sendArr', data).catch((e) => e)
return adapter.request('common/sendArr', data).catch((e: any) => e)
},
assertion: (error: any) => {
return !!error && !!error.error && !!error.error.message
@@ -182,7 +182,9 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
const invalidData: any = {
'1InvalidTable': [{ col1: 42 }]
}
return adapter.request('common/sendObj', invalidData).catch((e) => e)
return adapter
.request('common/sendObj', invalidData)
.catch((e: any) => e)
},
assertion: (error: any) =>
!!error && !!error.error && !!error.error.message
@@ -194,7 +196,9 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
const invalidData: any = {
'an invalidTable': [{ col1: 42 }]
}
return adapter.request('common/sendObj', invalidData).catch((e) => e)
return adapter
.request('common/sendObj', invalidData)
.catch((e: any) => e)
},
assertion: (error: any) =>
!!error && !!error.error && !!error.error.message
@@ -206,7 +210,9 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
const invalidData: any = {
'anInvalidTable#': [{ col1: 42 }]
}
return adapter.request('common/sendObj', invalidData).catch((e) => e)
return adapter
.request('common/sendObj', invalidData)
.catch((e: any) => e)
},
assertion: (error: any) =>
!!error && !!error.error && !!error.error.message
@@ -219,7 +225,9 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx: [{ col1: 42 }]
}
return adapter.request('common/sendObj', invalidData).catch((e) => e)
return adapter
.request('common/sendObj', invalidData)
.catch((e: any) => e)
},
assertion: (error: any) =>
!!error && !!error.error && !!error.error.message
@@ -231,7 +239,9 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
const invalidData: any = {
inData: [[{ data: 'value' }]]
}
return adapter.request('common/sendObj', invalidData).catch((e) => e)
return adapter
.request('common/sendObj', invalidData)
.catch((e: any) => e)
},
assertion: (error: any) =>
!!error && !!error.error && !!error.error.message
@@ -265,7 +275,7 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
test: () => {
return adapter
.request('common/sendObj', getLongStringData(32767))
.catch((e) => e)
.catch((e: any) => e)
},
assertion: (error: any) => {
return !!error && !!error.error && !!error.error.message

View File

@@ -9,7 +9,7 @@ export const assert = (
} else {
result = expression()
}
} catch (e) {
} catch (e: any) {
console.error(message)
throw new Error(message)
}

View File

@@ -31,7 +31,7 @@ describe('SASViyaApiClient', () => {
.mockImplementation(() => Promise.reject('Not Found'))
const error = await sasViyaApiClient
.createFolder('test', '/foo')
.catch((e) => e)
.catch((e: any) => e)
expect(error).toBeInstanceOf(RootFolderNotFoundError)
})
})

View File

@@ -240,7 +240,7 @@ export async function executeScript(
jobResult = await requestClient
.get<any>(resultLink, access_token, 'text/plain')
.catch(async (e) => {
.catch(async (e: any) => {
if (e instanceof NotFoundError) {
if (logLink) {
const logUrl = `${logLink.href}/content`
@@ -271,7 +271,7 @@ export async function executeScript(
})
return { result: jobResult?.result, log }
} catch (e) {
} catch (e: any) {
interface HttpError {
status: number
}

View File

@@ -80,7 +80,7 @@ describe('executeScript', () => {
'test',
['%put hello'],
'test context'
).catch((e) => e)
).catch((e: any) => e)
expect(error).toContain('Error while getting session.')
})
@@ -128,7 +128,7 @@ describe('executeScript', () => {
false,
defaultPollOptions,
true
).catch((e) => e)
).catch((e: any) => e)
expect(error).toContain('Error while getting session variable.')
})
@@ -297,7 +297,7 @@ describe('executeScript', () => {
false,
defaultPollOptions,
true
).catch((e) => e)
).catch((e: any) => e)
expect(error).toContain('Error while posting job')
})
@@ -367,7 +367,7 @@ describe('executeScript', () => {
true,
defaultPollOptions,
true
).catch((e) => e)
).catch((e: any) => e)
expect(error).toContain('Error while polling job status.')
})
@@ -393,7 +393,7 @@ describe('executeScript', () => {
true,
defaultPollOptions,
true
).catch((e) => e)
).catch((e: any) => e)
expect(fetchLogsModule.fetchLogByChunks).toHaveBeenCalledWith(
requestClient,
@@ -468,7 +468,7 @@ describe('executeScript', () => {
true,
defaultPollOptions,
true
).catch((e) => e)
).catch((e: any) => e)
expect(fetchLogsModule.fetchLogByChunks).toHaveBeenCalledWith(
requestClient,
@@ -501,7 +501,7 @@ describe('executeScript', () => {
true,
defaultPollOptions,
true
).catch((e) => e)
).catch((e: any) => e)
expect(fetchLogsModule.fetchLogByChunks).toHaveBeenCalledWith(
requestClient,
@@ -561,7 +561,7 @@ describe('executeScript', () => {
true,
defaultPollOptions,
true
).catch((e) => e)
).catch((e: any) => e)
expect(requestClient.get).toHaveBeenCalledWith(
`/compute/sessions/${mockSession.id}/filerefs/_webout/content`,
@@ -622,7 +622,7 @@ describe('executeScript', () => {
true,
defaultPollOptions,
true
).catch((e) => e)
).catch((e: any) => e)
expect(error).toContain('Error while clearing session.')
})

View File

@@ -59,7 +59,7 @@ describe('pollJobState', () => {
false,
undefined,
defaultPollOptions
).catch((e) => e)
).catch((e: any) => e)
expect((error as Error).message).toContain('Job state link was not found.')
})
@@ -238,7 +238,7 @@ describe('pollJobState', () => {
false,
undefined,
defaultPollOptions
).catch((e) => e)
).catch((e: any) => e)
expect(error.message).toEqual(
'Error while polling job state for job j0b: Status Error'

View File

@@ -17,7 +17,7 @@ describe('saveLog', () => {
it('should throw an error when a valid access token is not provided', async () => {
const error = await saveLog(mockJob, requestClient, 0, 100, stream).catch(
(e) => e
(e: any) => e
)
expect(error.message).toContain(
@@ -33,7 +33,7 @@ describe('saveLog', () => {
100,
stream,
't0k3n'
).catch((e) => e)
).catch((e: any) => e)
expect(error.message).toContain(
`Log URL for job ${mockJob.id} was not found.`

View File

@@ -30,7 +30,7 @@ describe('uploadTables', () => {
.mockImplementation(() => 'ERROR: LARGE STRING LENGTH')
const error = await uploadTables(requestClient, data, 't0k3n').catch(
(e) => e
(e: any) => e
)
expect(requestClient.uploadFile).not.toHaveBeenCalled()
@@ -46,7 +46,7 @@ describe('uploadTables', () => {
.mockImplementation(() => Promise.reject('Upload Error'))
const error = await uploadTables(requestClient, data, 't0k3n').catch(
(e) => e
(e: any) => e
)
expect(error).toContain('Error while uploading file.')

View File

@@ -28,7 +28,7 @@ describe('writeStream', () => {
jest
.spyOn(stream, 'write')
.mockImplementation((_, callback) => callback(new Error('Test Error')))
const error = await writeStream(stream, content).catch((e) => e)
const error = await writeStream(stream, content).catch((e: any) => e)
expect(error.message).toEqual('Test Error')
})

View File

@@ -4,7 +4,7 @@ export const writeStream = async (
stream: WriteStream,
content: string
): Promise<void> =>
stream.write(content + '\n', (e) => {
stream.write(content + '\n', (e: any) => {
if (e) return Promise.reject(e)
return Promise.resolve()

View File

@@ -23,7 +23,7 @@ export class AuthManager {
? '/SASLogon/logout?'
: this.serverType === ServerType.SasViya
? '/SASLogon/logout.do?'
: '/SASjsApi/auth/logout'
: '/SASLogon/logout'
}
/**
@@ -334,19 +334,9 @@ export class AuthManager {
/**
* Logs out of the configured SAS server.
* @param accessToken - an optional access token is required for SASjs server type.
*
*/
public async logOut() {
if (this.serverType === ServerType.Sasjs) {
return this.requestClient
.delete(this.logoutUrl)
.catch(() => true)
.finally(() => {
this.requestClient.clearLocalStorageTokens()
return true
})
}
this.requestClient.clearCsrfTokens()
return this.requestClient.get(this.logoutUrl, undefined).then(() => true)

View File

@@ -28,7 +28,7 @@ export async function refreshTokensForSasjs(
}
})
.catch((err) => {
throw prefixMessage(err, 'Error while refreshing tokens')
throw prefixMessage(err, 'Error while refreshing tokens: ')
})
return authResponse

View File

@@ -42,7 +42,7 @@ export async function refreshTokensForViya(
)
.then((res) => res.result)
.catch((err) => {
throw prefixMessage(err, 'Error while refreshing tokens')
throw prefixMessage(err, 'Error while refreshing tokens: ')
})
return authResponse

View File

@@ -53,7 +53,7 @@ describe('getAccessTokenForSasjs', () => {
requestClient,
authConfig.client,
authConfig.refresh_token
).catch((e) => e)
).catch((e: any) => e)
expect(error).toContain('Error while getting access token')
})

View File

@@ -64,7 +64,7 @@ describe('getAccessTokenForViya', () => {
authConfig.client,
authConfig.secret,
authConfig.refresh_token
).catch((e) => e)
).catch((e: any) => e)
expect(error).toContain('Error while getting access token')
})

View File

@@ -62,7 +62,9 @@ describe('getTokens', () => {
const expectedError =
'Unable to obtain new access token. Your refresh token has expired.'
const error = await getTokens(requestClient, authConfig).catch((e) => e)
const error = await getTokens(requestClient, authConfig).catch(
(e: any) => e
)
expect(error.message).toEqual(expectedError)
})

View File

@@ -27,17 +27,20 @@ describe('refreshTokensForSasjs', () => {
it('should handle errors while refreshing tokens', async () => {
setupMocks()
const refresh_token = generateToken(30)
const tokenError = 'unable to verify the first certificate'
jest
.spyOn(requestClient, 'post')
.mockImplementation(() => Promise.reject('Token Error'))
.mockImplementation(() => Promise.reject(tokenError))
const error = await refreshTokensForSasjs(
requestClient,
refresh_token
).catch((e) => e)
).catch((e: any) => e)
expect(error).toContain('Error while refreshing tokens')
expect(error).toEqual(`Error while refreshing tokens: ${tokenError}`)
})
})

View File

@@ -46,26 +46,29 @@ describe('refreshTokensForViya', () => {
it('should handle errors while refreshing tokens', async () => {
setupMocks()
const access_token = generateToken(30)
const refresh_token = generateToken(30)
const tokenError = 'unable to verify the first certificate'
const authConfig: AuthConfig = {
access_token,
refresh_token,
client: 'cl13nt',
secret: 's3cr3t'
}
jest
.spyOn(requestClient, 'post')
.mockImplementation(() => Promise.reject('Token Error'))
.mockImplementation(() => Promise.reject(tokenError))
const error = await refreshTokensForViya(
requestClient,
authConfig.client,
authConfig.secret,
authConfig.refresh_token
).catch((e) => e)
).catch((e: any) => e)
expect(error).toContain('Error while refreshing tokens')
expect(error).toEqual(`Error while refreshing tokens: ${tokenError}`)
})
})

View File

@@ -1,5 +1,5 @@
import * as NodeFormData from 'form-data'
import { convertToCSV } from '../utils/convertToCsv'
import { convertToCSV, isFormatsTable } from '../utils/convertToCsv'
import { splitChunks } from '../utils/splitChunks'
export const generateTableUploadForm = (
@@ -13,7 +13,8 @@ export const generateTableUploadForm = (
for (const tableName in data) {
tableCounter++
sasjsTables.push(tableName)
// Formats table should not be sent as part of 'sasjs_tables'
if (!isFormatsTable(tableName)) sasjsTables.push(tableName)
const csv = convertToCSV(data, tableName)

View File

@@ -7,7 +7,6 @@ describe('generateFileUploadForm', () => {
}
const BlobMock = jest.fn()
;(global as any).FormData = FormDataMock
;(global as any).Blob = BlobMock
})

View File

@@ -169,15 +169,23 @@ export class WebJobExecutor extends BaseJobExecutor {
? res.result.log.map((logLine: any) => logLine.line).join('\n')
: res.result.log
const resObj = res
const resObj =
this.serverType === ServerType.Sasjs
? {
result: res.result._webout,
log: parsedSasjsServerLog
}
: res
if (this.serverType === ServerType.Sasjs) {
if (res.result._webout < 1)
throw new JobExecutionError(
0,
'Job execution failed',
parsedSasjsServerLog
)
if (
this.serverType === ServerType.Sasjs &&
res.result._webout.length < 1
) {
throw new JobExecutionError(
0,
`No webout was returned by job ${program}. Server type is SASJS and the calling function is WebJobExecutor. Please check the SAS log for more info.`,
parsedSasjsServerLog
)
}
this.requestClient!.appendRequest(resObj, sasJob, config.debug)

View File

@@ -19,6 +19,7 @@ import {
parseSourceCode,
createAxiosInstance
} from '../utils'
import { InvalidCsrfError } from '../types/errors/InvalidCsrfError'
export interface HttpClient {
get<T>(
@@ -206,7 +207,7 @@ export class RequestClient implements HttpClient {
return this.parseResponse<T>(response)
})
.catch(async (e) => {
.catch(async (e: any) => {
return await this.handleError(
e,
() =>
@@ -247,7 +248,7 @@ export class RequestClient implements HttpClient {
return this.parseResponse<T>(response)
})
.catch(async (e) => {
.catch(async (e: any) => {
return await this.handleError(e, () =>
this.post<T>(url, data, accessToken, contentType, overrideHeaders)
)
@@ -271,7 +272,7 @@ export class RequestClient implements HttpClient {
throwIfError(response)
return this.parseResponse<T>(response)
})
.catch(async (e) => {
.catch(async (e: any) => {
return await this.handleError(e, () =>
this.put<T>(url, data, accessToken, overrideHeaders)
)
@@ -290,7 +291,7 @@ export class RequestClient implements HttpClient {
throwIfError(response)
return this.parseResponse<T>(response)
})
.catch(async (e) => {
.catch(async (e: any) => {
return await this.handleError(e, () => this.delete<T>(url, accessToken))
})
}
@@ -308,7 +309,7 @@ export class RequestClient implements HttpClient {
throwIfError(response)
return this.parseResponse<T>(response)
})
.catch(async (e) => {
.catch(async (e: any) => {
return await this.handleError(e, () =>
this.patch<T>(url, data, accessToken)
)
@@ -498,6 +499,24 @@ export class RequestClient implements HttpClient {
throw e
}
if (e instanceof InvalidCsrfError) {
// Fetching root will inject CSRF token in cookie
await this.httpClient
.get('/', {
withCredentials: true
})
.catch((err) => {
throw prefixMessage(err, 'Error while re-fetching CSRF token.')
})
return await callback().catch((err: any) => {
throw prefixMessage(
err,
'Error while executing callback in handleError. '
)
})
}
if (response?.status === 403 || response?.status === 449) {
this.parseAndSetCsrfToken(response)
@@ -584,9 +603,17 @@ export class RequestClient implements HttpClient {
export const throwIfError = (response: AxiosResponse) => {
switch (response.status) {
case 400:
if (typeof response.data === 'object') {
if (
typeof response.data === 'object' &&
response.data.error === 'invalid_grant'
) {
// In SASVIYA when trying to get access token, if auth code is wrong status code will be 400 so in such case we return login required error.
throw new LoginRequiredError(response.data)
}
if (response.data.toLowerCase() === 'invalid csrf token!') {
throw new InvalidCsrfError()
}
break
case 401:
if (typeof response.data === 'object') {
@@ -676,7 +703,14 @@ const parseError = (data: string) => {
} catch (_) {}
try {
// There are some edge cases in which the SAS mp_abort macro
// (https://core.sasjs.io/mp__abort_8sas.html) is unable to
// provide a clean exit. In this case the JSON response will
// be wrapped in >>weboutBEGIN<< and >>weboutEND<< strings.
// Therefore, if the first string exists, we won't throw an
// error just yet (the parser may yet throw one instead)
const hasError =
!data?.match(/>>weboutBEGIN<</) &&
!!data?.match(/Stored Process Error/i) &&
!!data?.match(/This request completed with errors./i)
if (hasError) {

View File

@@ -68,7 +68,7 @@ export class Sas9RequestClient extends RequestClient {
throwIfError(response)
return this.parseResponse<T>(response)
})
.catch(async (e) => {
.catch(async (e: any) => {
return await this.handleError(
e,
() =>
@@ -113,7 +113,7 @@ export class Sas9RequestClient extends RequestClient {
throwIfError(response)
return this.parseResponse<T>(response)
})
.catch(async (e) => {
.catch(async (e: any) => {
return await this.handleError(e, () =>
this.post<T>(url, data, accessToken, contentType, overrideHeaders)
)

View File

@@ -11,13 +11,16 @@ describe('formatDataForRequest', () => {
{ var1: 'string', var2: 232, nullvar: '_' },
{ var1: 'string', var2: 232, nullvar: 0 },
{ var1: 'string', var2: 232, nullvar: 'z' },
{ var1: 'string', var2: 232, nullvar: null }
{ var1: 'string', var2: 232, nullvar: null },
{ var1: 'string', var2: 232, nullvar: '.A' },
{ var1: 'string', var2: 232, nullvar: '._' },
{ var1: 'string', var2: 232, nullvar: '.' }
],
[`$${testTable}`]: { formats: { var1: '$char12.', nullvar: 'best.' } }
}
const expectedOutput = {
sasjs1data: `var1:$char12. var2:best. nullvar:best.\r\nstring,232,.a\r\nstring,232,.b\r\nstring,232,._\r\nstring,232,0\r\nstring,232,.z\r\nstring,232,.`,
sasjs1data: `var1:$char12. var2:best. nullvar:best.\r\nstring,232,.a\r\nstring,232,.b\r\nstring,232,._\r\nstring,232,0\r\nstring,232,.z\r\nstring,232,.\r\nstring,232,.a\r\nstring,232,._\r\nstring,232,.`,
sasjs_tables: testTable
}
@@ -54,6 +57,17 @@ describe('formatDataForRequest', () => {
expect(formatDataForRequest(data)).toEqual(expectedOutput)
})
it('should accept . as special missing value', () => {
let tableWithMissingValues = {
[testTable]: [{ var: '.' }, { var: 0 }],
[`$${testTable}`]: { formats: { var: 'best.' } }
}
expect(() =>
formatDataForRequest(tableWithMissingValues)
).not.toThrowError()
})
it('should throw an error if special missing values is not valid', () => {
let tableWithMissingValues = {
[testTable]: [{ var: 'AA' }, { var: 0 }],

View File

@@ -0,0 +1,9 @@
export class InvalidCsrfError extends Error {
constructor() {
const message = 'Invalid CSRF token!'
super(`Auth error: ${message}`)
this.name = 'InvalidCsrfError'
Object.setPrototypeOf(this, InvalidCsrfError.prototype)
}
}

View File

@@ -1,4 +1,4 @@
import { convertToCSV } from './convertToCsv'
import { convertToCSV, isFormatsTable } from './convertToCsv'
describe('convertToCsv', () => {
const tableName = 'testTable'
@@ -216,7 +216,9 @@ describe('convertToCsv', () => {
const data = { [tableName]: [{ var1: 'string' }] }
expect(() => convertToCSV(data, 'wrongTableName')).toThrow(
new Error('No table provided to be converted to CSV')
new Error(
'Error while converting to CSV. No table provided to be converted to CSV.'
)
)
})
@@ -226,3 +228,15 @@ describe('convertToCsv', () => {
expect(convertToCSV(data, tableName)).toEqual('')
})
})
describe('isFormatsTable', () => {
const tableName = 'sometable'
it('should return true if table name match pattern of formats table', () => {
expect(isFormatsTable(`$${tableName}`)).toEqual(true)
})
it('should return false if table name does not match pattern of formats table', () => {
expect(isFormatsTable(tableName)).toEqual(false)
})
})

View File

@@ -1,4 +1,5 @@
import { isSpecialMissing } from '@sasjs/utils/input/validators'
import { prefixMessage } from '@sasjs/utils/error'
/**
* Converts the given JSON object array to a CSV string.
@@ -9,7 +10,10 @@ export const convertToCSV = (
tableName: string
) => {
if (!data[tableName]) {
throw new Error('No table provided to be converted to CSV')
throw prefixMessage(
'No table provided to be converted to CSV.',
'Error while converting to CSV. '
)
}
const table = data[tableName]
@@ -137,7 +141,9 @@ export const convertToCSV = (
)
}
return `.${value.toLowerCase()}`
const dot = value.includes('.') ? '' : '.'
return `${dot}${value.toLowerCase()}`
}
// if there any present, it should have preceding (") for escaping
@@ -170,6 +176,12 @@ export const convertToCSV = (
return finalCSV
}
/**
* Checks if table is table of formats (table name should start from '$' character).
* @param tableName - table name.
*/
export const isFormatsTable = (tableName: string) => /^\$.*/.test(tableName)
const getByteSize = (str: string) => {
let byteSize = str.length
for (let i = str.length - 1; i >= 0; i--) {

View File

@@ -1,4 +1,4 @@
import { convertToCSV } from './convertToCsv'
import { convertToCSV, isFormatsTable } from './convertToCsv'
import { splitChunks } from './splitChunks'
export const formatDataForRequest = (data: any) => {
@@ -8,7 +8,7 @@ export const formatDataForRequest = (data: any) => {
for (const tableName in data) {
if (
tableName.match(/^\$.*/) &&
isFormatsTable(tableName) &&
Object.keys(data).includes(tableName.replace(/^\$/, ''))
) {
continue
@@ -16,7 +16,8 @@ export const formatDataForRequest = (data: any) => {
tableCounter++
sasjsTables.push(tableName)
// Formats table should not be sent as part of 'sasjs_tables'
if (!isFormatsTable(tableName)) sasjsTables.push(tableName)
const csv = convertToCSV(data, tableName)

View File

@@ -15,7 +15,7 @@ export const getValidJson = (str: string | object): object => {
if (str === '') return {}
return JSON.parse(str)
} catch (e) {
} catch (e: any) {
if (e instanceof JsonParseArrayError) throw e
throw new InvalidJsonError()
}

View File

@@ -4,7 +4,7 @@ export const parseSasViyaLog = (logResponse: { items: any[] }) => {
log = logResponse.items
? logResponse.items.map((i) => i.line).join('\n')
: JSON.stringify(logResponse)
} catch (e) {
} catch (e: any) {
console.error('An error has occurred while parsing the log response', e)
log = logResponse
}

View File

@@ -8,7 +8,7 @@ export const parseWeboutResponse = (response: string, url?: string): string => {
sasResponse = response
.split('>>weboutBEGIN<<')[1]
.split('>>weboutEND<<')[0]
} catch (e) {
} catch (e: any) {
if (url) throw new WeboutResponseError(url)
sasResponse = ''