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

Compare commits

...

20 Commits

Author SHA1 Message Date
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
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
19 changed files with 13417 additions and 21719 deletions

View File

@@ -4,4 +4,4 @@ updates:
directory: "/" directory: "/"
schedule: schedule:
interval: "monthly" interval: "monthly"
open-pull-requests-limit: 2 open-pull-requests-limit: 1

View File

@@ -94,10 +94,10 @@ const sasJs = new SASjs({your config})
More on the config later. More on the config later.
### SAS Logon ### 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:'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:'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: Sample code for logging in with the `Default` approach:
@@ -125,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 ```javascript
let dataObject={ let dataObject={
@@ -141,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. The response object will contain returned tables and columns. Table names are always lowercase, and column names uppercase.
@@ -248,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) %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
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: 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:
@@ -256,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). * `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. * `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. * `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. * `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`. * `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. * `requestHistoryLimit` - Request history limit. Increasing this limit may affect browser performance, especially with debug (logs) enabled. Default is 10.

4113
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -64,7 +64,7 @@
"prettier": "2.7.1", "prettier": "2.7.1",
"process": "0.11.10", "process": "0.11.10",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"semantic-release": "18.0.0", "semantic-release": "19.0.3",
"terser-webpack-plugin": "5.3.1", "terser-webpack-plugin": "5.3.1",
"ts-jest": "27.1.3", "ts-jest": "27.1.3",
"ts-loader": "9.2.6", "ts-loader": "9.2.6",

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@
"private": true, "private": true,
"dependencies": { "dependencies": {
"@sasjs/adapter": "file:../build/sasjs-adapter-5.0.0.tgz", "@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/jest": "^26.0.20",
"@types/node": "^14.14.41", "@types/node": "^14.14.41",
"@types/react": "^17.0.1", "@types/react": "^17.0.1",
@@ -14,7 +14,7 @@
"react": "^17.0.1", "react": "^17.0.1",
"react-dom": "^17.0.1", "react-dom": "^17.0.1",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-scripts": "^4.0.2", "react-scripts": "^5.0.1",
"typescript": "^4.1.3" "typescript": "^4.1.3"
}, },
"scripts": { "scripts": {
@@ -43,6 +43,6 @@
] ]
}, },
"devDependencies": { "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 App = (): ReactElement<{}> => {
const { adapter, config } = useContext(AppContext) const { adapter, config } = useContext(AppContext)
const [testSuites, setTestSuites] = useState<TestSuite[]>([]) const [testSuites, setTestSuites] = useState<TestSuite[]>([])
const appLoc = config.sasJsConfig.appLoc
useEffect(() => { useEffect(() => {
if (adapter) { if (adapter) {
const testSuites = [ const testSuites = [
basicTests(adapter, config.userName, config.password), basicTests(adapter, config.userName, config.password),
sendArrTests(adapter), sendArrTests(adapter, appLoc),
sendObjTests(adapter), sendObjTests(adapter),
specialCaseTests(adapter), specialCaseTests(adapter),
sasjsRequestTests(adapter), sasjsRequestTests(adapter),
@@ -24,12 +25,12 @@ const App = (): ReactElement<{}> => {
] ]
if (adapter.getSasjsConfig().serverType === 'SASVIYA') { if (adapter.getSasjsConfig().serverType === 'SASVIYA') {
testSuites.push(computeTests(adapter)) testSuites.push(computeTests(adapter, appLoc))
} }
setTestSuites(testSuites) setTestSuites(testSuites)
} }
}, [adapter, config]) }, [adapter, config, appLoc])
return ( return (
<div className="app"> <div className="app">

View File

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

View File

@@ -45,14 +45,14 @@ const getLargeObjectData = () => {
return data return data
} }
export const sendArrTests = (adapter: SASjs): TestSuite => ({ export const sendArrTests = (adapter: SASjs, appLoc: string): TestSuite => ({
name: 'sendArr', name: 'sendArr',
tests: [ tests: [
{ {
title: 'Absolute paths', title: 'Absolute paths',
description: 'Should work with absolute paths to SAS jobs', description: 'Should work with absolute paths to SAS jobs',
test: () => { test: () => {
return adapter.request('/Public/app/common/sendArr', stringData) return adapter.request(`${appLoc}/common/sendArr`, stringData)
}, },
assertion: (res: any) => { assertion: (res: any) => {
return res.table1[0][0] === stringData.table1[0].col1 return res.table1[0][0] === stringData.table1[0].col1

View File

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

View File

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

View File

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

View File

@@ -46,17 +46,20 @@ describe('refreshTokensForViya', () => {
it('should handle errors while refreshing tokens', async () => { it('should handle errors while refreshing tokens', async () => {
setupMocks() setupMocks()
const access_token = generateToken(30) const access_token = generateToken(30)
const refresh_token = generateToken(30) const refresh_token = generateToken(30)
const tokenError = 'unable to verify the first certificate'
const authConfig: AuthConfig = { const authConfig: AuthConfig = {
access_token, access_token,
refresh_token, refresh_token,
client: 'cl13nt', client: 'cl13nt',
secret: 's3cr3t' secret: 's3cr3t'
} }
jest jest
.spyOn(requestClient, 'post') .spyOn(requestClient, 'post')
.mockImplementation(() => Promise.reject('Token Error')) .mockImplementation(() => Promise.reject(tokenError))
const error = await refreshTokensForViya( const error = await refreshTokensForViya(
requestClient, requestClient,
@@ -65,7 +68,7 @@ describe('refreshTokensForViya', () => {
authConfig.refresh_token authConfig.refresh_token
).catch((e: any) => 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 * as NodeFormData from 'form-data'
import { convertToCSV } from '../utils/convertToCsv' import { convertToCSV, isFormatsTable } from '../utils/convertToCsv'
import { splitChunks } from '../utils/splitChunks' import { splitChunks } from '../utils/splitChunks'
export const generateTableUploadForm = ( export const generateTableUploadForm = (
@@ -13,7 +13,8 @@ export const generateTableUploadForm = (
for (const tableName in data) { for (const tableName in data) {
tableCounter++ 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) const csv = convertToCSV(data, tableName)

View File

@@ -611,7 +611,10 @@ export const throwIfError = (response: AxiosResponse) => {
throw new LoginRequiredError(response.data) throw new LoginRequiredError(response.data)
} }
if (response.data.toLowerCase() === 'invalid csrf token!') { if (
typeof response.data === 'string' &&
response.data.toLowerCase() === 'invalid csrf token!'
) {
throw new InvalidCsrfError() throw new InvalidCsrfError()
} }
break break
@@ -703,7 +706,14 @@ const parseError = (data: string) => {
} catch (_) {} } catch (_) {}
try { 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 = const hasError =
!data?.match(/>>weboutBEGIN<</) &&
!!data?.match(/Stored Process Error/i) && !!data?.match(/Stored Process Error/i) &&
!!data?.match(/This request completed with errors./i) !!data?.match(/This request completed with errors./i)
if (hasError) { if (hasError) {

View File

@@ -11,13 +11,16 @@ describe('formatDataForRequest', () => {
{ var1: 'string', var2: 232, nullvar: '_' }, { var1: 'string', var2: 232, nullvar: '_' },
{ var1: 'string', var2: 232, nullvar: 0 }, { var1: 'string', var2: 232, nullvar: 0 },
{ var1: 'string', var2: 232, nullvar: 'z' }, { 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.' } } [`$${testTable}`]: { formats: { var1: '$char12.', nullvar: 'best.' } }
} }
const expectedOutput = { 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 sasjs_tables: testTable
} }

View File

@@ -1,4 +1,4 @@
import { convertToCSV } from './convertToCsv' import { convertToCSV, isFormatsTable } from './convertToCsv'
describe('convertToCsv', () => { describe('convertToCsv', () => {
const tableName = 'testTable' const tableName = 'testTable'
@@ -216,7 +216,9 @@ describe('convertToCsv', () => {
const data = { [tableName]: [{ var1: 'string' }] } const data = { [tableName]: [{ var1: 'string' }] }
expect(() => convertToCSV(data, 'wrongTableName')).toThrow( 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('') 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 { isSpecialMissing } from '@sasjs/utils/input/validators'
import { prefixMessage } from '@sasjs/utils/error'
/** /**
* Converts the given JSON object array to a CSV string. * Converts the given JSON object array to a CSV string.
@@ -9,7 +10,10 @@ export const convertToCSV = (
tableName: string tableName: string
) => { ) => {
if (!data[tableName]) { 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] 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 // if there any present, it should have preceding (") for escaping
@@ -170,6 +176,12 @@ export const convertToCSV = (
return finalCSV 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) => { 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--) {

View File

@@ -1,4 +1,4 @@
import { convertToCSV } from './convertToCsv' import { convertToCSV, isFormatsTable } from './convertToCsv'
import { splitChunks } from './splitChunks' import { splitChunks } from './splitChunks'
export const formatDataForRequest = (data: any) => { export const formatDataForRequest = (data: any) => {
@@ -8,7 +8,7 @@ export const formatDataForRequest = (data: any) => {
for (const tableName in data) { for (const tableName in data) {
if ( if (
tableName.match(/^\$.*/) && isFormatsTable(tableName) &&
Object.keys(data).includes(tableName.replace(/^\$/, '')) Object.keys(data).includes(tableName.replace(/^\$/, ''))
) { ) {
continue continue
@@ -16,7 +16,8 @@ export const formatDataForRequest = (data: any) => {
tableCounter++ 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) const csv = convertToCSV(data, tableName)