1
0
mirror of https://github.com/sasjs/adapter.git synced 2025-12-11 09:24:35 +00:00

Compare commits

..

49 Commits

Author SHA1 Message Date
Allan Bowe
f1df27fdf1 Merge pull request #748 from sasjs/issue-741
fix: error when multiple redirections happen while debug is on
2022-08-24 14:44:45 +01:00
eb739a83a4 chore: fix 2022-08-24 15:16:47 +02:00
d8b686dd7e fix: error when multiple redirections happen while debug is on 2022-08-24 15:12:13 +02:00
Allan Bowe
a0b8316d7c Update README.md 2022-08-12 15:48:38 +01:00
Allan Bowe
92be5a2dca Merge pull request #744 from sasjs/sasjs-server-csrf-cookie
fix(server): csrf cookie is created explicitly
2022-08-04 02:03:06 +01:00
Saad Jutt
f58f2eba97 chore: error needs to be more specific 2022-08-04 05:59:39 +05:00
Saad Jutt
e37bb182c3 fix(server): csrf cookie is created explicitly 2022-08-04 05:04:43 +05:00
Allan Bowe
504777603c Merge pull request #743 from sasjs/issue-722
fix: improve input validations
2022-07-28 13:03:03 +01:00
706cbe5513 chore: add unit tests for validateInput 2022-07-28 14:23:00 +05:00
88eadd27aa chore: moved utils specs to spec folder 2022-07-28 14:22:16 +05:00
4ed9f87434 fix: moved validateInput method to separate file and added some additional validation 2022-07-28 14:20:41 +05:00
Allan Bowe
f0f80a1c1f Merge pull request #742 from sasjs/issue-721
fix: add additional check for string type before converting data to lower case
2022-07-27 20:42:48 +01:00
d0d8d58945 fix: add additional check for string type before converting data to lower case 2022-07-27 18:30:47 +05:00
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
46 changed files with 18363 additions and 23414 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.
@@ -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 */
@@ -233,7 +237,8 @@ run;
%webout(OBJ,a) /* Rows in table `a` are objects (easy to use) */
%webout(ARR,b) /* Rows in table `b` are arrays (compact) */
%webout(OBJ,c,fmt=N) /* Table `c` is sent unformatted (raw) */
%webout(OBJ,c,label=d) /* Rename as `d` on JS side */
%webout(OBJ,c,label=d) /* Rename table as `d` in output JSON */
%webout(OBJ,c,label=e, maxobs=10) /* send only 10 rows back */
%webout(CLOSE) /* Close the JSON and add default variables */
```
@@ -250,6 +255,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 +265,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 +321,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",

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

@@ -1,4 +1,4 @@
import { compareTimestamps, asyncForEach } from './utils'
import { compareTimestamps, asyncForEach, validateInput } from './utils'
import {
SASjsConfig,
UploadFile,
@@ -686,7 +686,7 @@ export default class SASjs {
...config
}
const validationResult = this.validateInput(data)
const validationResult = validateInput(data)
// status is true if the data passes validation checks above
if (validationResult.status) {
@@ -748,74 +748,6 @@ export default class SASjs {
}
}
/**
* This function validates the input data structure and table naming convention
*
* @param data A json object that contains one or more tables, it can also be null
* @returns An object which contains two attributes: 1) status: boolean, 2) msg: string
*/
private validateInput(data: { [key: string]: any } | null): {
status: boolean
msg: string
} {
if (data === null) return { status: true, msg: '' }
const isSasFormatsTable = (key: string) =>
key.match(/^\$.*/) && Object.keys(data).includes(key.replace(/^\$/, ''))
for (const key in data) {
if (!key.match(/^[a-zA-Z_]/) && !isSasFormatsTable(key)) {
return {
status: false,
msg: 'First letter of table should be alphabet or underscore.'
}
}
if (!key.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/) && !isSasFormatsTable(key)) {
return { status: false, msg: 'Table name should be alphanumeric.' }
}
if (key.length > 32) {
return {
status: false,
msg: 'Maximum length for table name could be 32 characters.'
}
}
if (this.getType(data[key]) !== 'Array' && !isSasFormatsTable(key)) {
return {
status: false,
msg: 'Parameter data contains invalid table structure.'
}
}
for (let i = 0; i < data[key].length; i++) {
if (this.getType(data[key][i]) !== 'object') {
return {
status: false,
msg: `Table ${key} contains invalid structure.`
}
}
}
}
return { status: true, msg: '' }
}
/**
* this function returns the type of variable
*
* @param data it could be anything, like string, array, object etc.
* @returns a string which tells the type of input parameter
*/
private getType(data: any): string {
if (Array.isArray(data)) {
return 'Array'
} else {
return typeof data
}
}
/**
* Creates the folders and services at the given location `appLoc` on the given server `serverUrl`.
* @param serviceJson - the JSON specifying the folders and services to be created.

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'
}
/**
@@ -223,9 +223,17 @@ export class AuthManager {
private async getNewLoginForm() {
if (this.serverType === ServerType.Sasjs) {
// server will be sending CSRF cookie,
// server will be sending CSRF token in response,
// need to save in cookie so that,
// http client will use it automatically
return this.requestClient.get('/', undefined)
return this.requestClient.get('/', undefined).then(({ result }) => {
const cookie =
/<script>document.cookie = '(XSRF-TOKEN=.*; Max-Age=86400; SameSite=Strict; Path=\/;)'<\/script>/.exec(
result as string
)?.[1]
if (cookie) document.cookie = cookie
})
}
const { result: formResponse } = await this.requestClient.get<string>(
@@ -334,19 +342,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.length < 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,7 +19,7 @@ import {
parseSourceCode,
createAxiosInstance
} from '../utils'
import { InvalidCsrfError } from '../types/errors/InvalidCsrfError'
import { InvalidSASjsCsrfError } from '../types/errors/InvalidSASjsCsrfError'
export interface HttpClient {
get<T>(
@@ -154,8 +154,12 @@ export class RequestClient implements HttpClient {
sasWork = log
}
} else if (response?.result) {
sourceCode = parseSourceCode(response.result)
generatedCode = parseGeneratedCode(response.result)
// We parse only if it's a string, otherwise it would throw error
if (typeof response.result === 'string') {
sourceCode = parseSourceCode(response.result)
generatedCode = parseGeneratedCode(response.result)
}
sasWork = response.result.WORK
}
}
@@ -207,7 +211,7 @@ export class RequestClient implements HttpClient {
return this.parseResponse<T>(response)
})
.catch(async (e) => {
.catch(async (e: any) => {
return await this.handleError(
e,
() =>
@@ -248,7 +252,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)
)
@@ -272,7 +276,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)
)
@@ -291,7 +295,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))
})
}
@@ -309,7 +313,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)
)
@@ -499,12 +503,20 @@ export class RequestClient implements HttpClient {
throw e
}
if (e instanceof InvalidCsrfError) {
// Fetching root will inject CSRF token in cookie
if (e instanceof InvalidSASjsCsrfError) {
// Fetching root and creating CSRF cookie
await this.httpClient
.get('/', {
withCredentials: true
})
.then((response) => {
const cookie =
/<script>document.cookie = '(XSRF-TOKEN=.*; Max-Age=86400; SameSite=Strict; Path=\/;)'<\/script>/.exec(
response.data
)?.[1]
if (cookie) document.cookie = cookie
})
.catch((err) => {
throw prefixMessage(err, 'Error while re-fetching CSRF token.')
})
@@ -611,8 +623,11 @@ export const throwIfError = (response: AxiosResponse) => {
throw new LoginRequiredError(response.data)
}
if (response.data.toLowerCase() === 'invalid csrf token!') {
throw new InvalidCsrfError()
if (
typeof response.data === 'string' &&
response.data.toLowerCase() === 'invalid csrf token!'
) {
throw new InvalidSASjsCsrfError()
}
break
case 401:
@@ -703,7 +718,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
}

View File

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

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

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

@@ -1,20 +1,21 @@
export * from './appendExtraResponseAttributes'
export * from './asyncForEach'
export * from './compareTimestamps'
export * from './convertToCsv'
export * from './createAxiosInstance'
export * from './delay'
export * from './fetchLogByChunks'
export * from './getValidJson'
export * from './isNode'
export * from './isRelativePath'
export * from './isUri'
export * from './isUrl'
export * from './needsRetry'
export * from './parseGeneratedCode'
export * from './parseSourceCode'
export * from './parseSasViyaLog'
export * from './parseSourceCode'
export * from './parseViyaDebugResponse'
export * from './parseWeboutResponse'
export * from './serialize'
export * from './splitChunks'
export * from './parseWeboutResponse'
export * from './fetchLogByChunks'
export * from './getValidJson'
export * from './parseViyaDebugResponse'
export * from './appendExtraResponseAttributes'
export * from './validateInput'

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 = ''

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

@@ -0,0 +1,84 @@
import {
validateInput,
INVALID_TABLE_STRUCTURE,
MORE_INFO
} from '../validateInput'
const tableArray = [{ col1: 'first col value' }]
const stringData: any = { table1: tableArray }
describe('validateInput', () => {
it('should not return an error message if input data valid', () => {
const validationResult = validateInput(stringData)
expect(validationResult).toEqual({
status: true,
msg: ''
})
})
it('should not return an error message if input data is null', () => {
const validationResult = validateInput(null)
expect(validationResult).toEqual({
status: true,
msg: ''
})
})
it('should return an error message if input data is an array', () => {
const validationResult = validateInput(tableArray)
expect(validationResult).toEqual({
status: false,
msg: INVALID_TABLE_STRUCTURE
})
})
it('should return an error message if first letter of table is neither alphabet nor underscore', () => {
const validationResult = validateInput({ '1stTable': tableArray })
expect(validationResult).toEqual({
status: false,
msg: 'First letter of table should be alphabet or underscore.'
})
})
it('should return an error message if table name contains a character other than alphanumeric or underscore', () => {
const validationResult = validateInput({ 'table!': tableArray })
expect(validationResult).toEqual({
status: false,
msg: 'Table name should be alphanumeric.'
})
})
it('should return an error message if length of table name contains exceeds 32', () => {
const validationResult = validateInput({
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx: tableArray
})
expect(validationResult).toEqual({
status: false,
msg: 'Maximum length for table name could be 32 characters.'
})
})
it('should return an error message if table does not have array of objects', () => {
const validationResult = validateInput({ table: stringData })
expect(validationResult).toEqual({
status: false,
msg: INVALID_TABLE_STRUCTURE
})
})
it('should return an error message if a table array has an item other than object', () => {
const validationResult = validateInput({ table1: ['invalid'] })
expect(validationResult).toEqual({
status: false,
msg: `Table table1 contains invalid structure. ${MORE_INFO}`
})
})
it('should return an error message if a row in a table contains an column with undefined value', () => {
const validationResult = validateInput({ table1: [{ column: undefined }] })
expect(validationResult).toEqual({
status: false,
msg: `A row in table table1 contains invalid value. Can't assign undefined to column.`
})
})
})

View File

@@ -0,0 +1,90 @@
export const MORE_INFO =
'For more info see https://sasjs.io/sasjs-adapter/#request-response'
export const INVALID_TABLE_STRUCTURE = `Parameter data contains invalid table structure. ${MORE_INFO}`
/**
* This function validates the input data structure and table naming convention
*
* @param data A json object that contains one or more tables, it can also be null
* @returns An object which contains two attributes: 1) status: boolean, 2) msg: string
*/
export const validateInput = (
data: { [key: string]: any } | null
): {
status: boolean
msg: string
} => {
if (data === null) return { status: true, msg: '' }
if (getType(data) !== 'object') {
return {
status: false,
msg: INVALID_TABLE_STRUCTURE
}
}
const isSasFormatsTable = (key: string) =>
key.match(/^\$.*/) && Object.keys(data).includes(key.replace(/^\$/, ''))
for (const key in data) {
if (!key.match(/^[a-zA-Z_]/) && !isSasFormatsTable(key)) {
return {
status: false,
msg: 'First letter of table should be alphabet or underscore.'
}
}
if (!key.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/) && !isSasFormatsTable(key)) {
return { status: false, msg: 'Table name should be alphanumeric.' }
}
if (key.length > 32) {
return {
status: false,
msg: 'Maximum length for table name could be 32 characters.'
}
}
if (getType(data[key]) !== 'Array' && !isSasFormatsTable(key)) {
return {
status: false,
msg: INVALID_TABLE_STRUCTURE
}
}
for (const item of data[key]) {
if (getType(item) !== 'object') {
return {
status: false,
msg: `Table ${key} contains invalid structure. ${MORE_INFO}`
}
} else {
const attributes = Object.keys(item)
for (const attribute of attributes) {
if (item[attribute] === undefined) {
return {
status: false,
msg: `A row in table ${key} contains invalid value. Can't assign undefined to ${attribute}.`
}
}
}
}
}
}
return { status: true, msg: '' }
}
/**
* this function returns the type of variable
*
* @param data it could be anything, like string, array, object etc.
* @returns a string which tells the type of input parameter
*/
const getType = (data: any): string => {
if (Array.isArray(data)) {
return 'Array'
} else {
return typeof data
}
}