1
0
mirror of https://github.com/sasjs/adapter.git synced 2025-12-16 19:24:36 +00:00

Compare commits

..

1 Commits

Author SHA1 Message Date
Yury Shkoda
0f881fba72 chore(deps): regenerate package-lock.json 2021-12-23 11:49:42 +03:00
34 changed files with 10483 additions and 9204 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: 10

View File

@@ -2,8 +2,11 @@ groups:
- name: SASjs Devs # name of the group - name: SASjs Devs # name of the group
reviewers: 1 # how many reviewers do you want to assign? reviewers: 1 # how many reviewers do you want to assign?
usernames: # github usernames of the reviewers usernames: # github usernames of the reviewers
- krishna-acondy
- YuryShkoda - YuryShkoda
- saadjutt01
- medjedovicm - medjedovicm
- allanbowe
- sabhas - sabhas
- name: SASjs QA - name: SASjs QA
reviewers: 1 reviewers: 1

View File

@@ -22,8 +22,6 @@ jobs:
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: npm cache: npm
- name: Check npm audit
run: npm audit --production --audit-level=low
- name: Install Dependencies - name: Install Dependencies
run: npm ci run: npm ci
- name: Check code style - name: Check code style

View File

@@ -1,2 +0,0 @@
tasks:
- init: npm install && npm run build

View File

@@ -142,71 +142,6 @@ The response object will contain returned tables and columns. Table names are a
The adapter will also cache the logs (if debug enabled) and even the work tables. For performance, it is best to keep debug mode off. The adapter will also cache the logs (if debug enabled) and even the work tables. For performance, it is best to keep debug mode off.
### Variable Types
The SAS type (char/numeric) of the values is determined according to a set of rules:
* 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).
* `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:
|JS Values |SAS Format|
|---|---|
|'a', 'a' |$char1.|
|0, '_' |best.|
|'Z', 0 |best.|
|'a', 'aaa' |$char3.|
|null, 'a', 'aaa' | $char3.|
|null, 'a', 0 | best.|
|null, null | best.|
|null, '' | $char1.|
|null, 'a' | $char1.|
|'a' | $char1.|
|'a', null | $char1.|
|'a', null, 0 | best.|
Validation is also performed on the values. The following combinations will throw errors:
|JS Values |SAS Format|
|---|---|
|null, 'aaaa', 0 | Error: mixed types. 'aaaa' is not a special missing value.|
|0, 'a', '!' | Error: mixed types. '!' is not a special missing value|
|1.1, '.', 0| Error: mixed types. For regular nulls, use `null`|
### Variable Format Override
The auto-detect functionality above is thwarted in the following scenarios:
* A character column containing only `null` values (is considered numeric)
* A numeric column containing only special missing values (is considered character)
To cater for these scenarios, an optional array of formats can be passed along with the data to ensure that SAS will read them in correctly.
To understand these formats, it should be noted that the JSON data is NOT passed directly (as JSON) to SAS. It is first converted into CSV, and the header row is actually an `infile` statement in disguise. It looks a bit like this:
```csv
CHARVAR1:$char4. CHARVAR2:$char1. NUMVAR:best.
LOAD,,0
ABCD,X,.
```
To provide overrides to this header row, the tables object can be constructed as follows (with a leading '$' in the table name):
```javascript
let specialData={
"tablewith2cols2rows": [
{"col1": "val1","specialMissingsCol": "A"},
{"col1": "val2","specialMissingsCol": "_"}
],
"$tablewith2cols2rows":{"formats":{"specialMissingsCol":"best."}
}
};
```
It is not necessary to provide formats for ALL the columns, only the ones that need to be overridden.
## SAS Inputs / Outputs ## SAS Inputs / Outputs
The SAS side is handled by a number of macros in the [macro core](https://github.com/sasjs/core) library. The SAS side is handled by a number of macros in the [macro core](https://github.com/sasjs/core) library.
@@ -218,29 +153,16 @@ The following snippet shows the process of SAS tables arriving / leaving:
%webout(FETCH) %webout(FETCH)
/* some sas code */ /* some sas code */
data a b c; data some sas tables;
set from js; set from js;
run; run;
%webout(OPEN) /* Open the JSON to be returned */ %webout(OPEN) /* open the JSON to be returned */
%webout(OBJ,a) /* Rows in table `a` are objects (easy to use) */ %webout(OBJ,some) /* `some` table is sent in object format */
%webout(ARR,b) /* Rows in table `b` are arrays (compact) */ %webout(ARR,sas) /* `sas` table is sent in array format, smaller filesize */
%webout(OBJ,c,fmt=N) /* Table `c` is sent unformatted (raw) */ %webout(OBJ,tables,fmt=N) /* unformatted (raw) data */
%webout(OBJ,c,label=d) /* Rename as `d` on JS side */ %webout(OBJ,tables,label=newtable) /* rename tables on export */
%webout(CLOSE) /* Close the JSON and add default variables */ %webout(CLOSE) /* close the JSON and send some extra useful variables too */
```
By default, special SAS numeric missings (_a-Z) are converted to `null` in the JSON. If you'd like to preserve these, use the `missing=STRING` option as follows:
```sas
%webout(OBJ,a,missing=STRING)
```
In this case, special missings (such as `.a`, `.b`) are converted to javascript string values (`'A', 'B'`).
Where an entire column is made up of special missing numerics, there would be no way to distinguish it from a single-character column by looking at the values. To cater for this scenario, it is possible to export the variable types (and other attributes such as label and format) by adding a `showmeta` param to the `webout()` macro as follows:
```sas
%webout(OBJ,a,missing=STRING,showmeta=YES)
``` ```
## Configuration ## Configuration
@@ -248,13 +170,12 @@ Where an entire column is made up of special missing numerics, there would be no
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:
* `appLoc` - this is the folder under which the SAS services will be created. * `appLoc` - this is the folder under which the SAS services will be created.
* `serverType` - either `SAS9`, `SASVIYA` or `SASJS`. The `SASJS` server type is for use with [sasjs/server](https://github.com/sasjs/server). * `serverType` - either `SAS9` or `SASVIYA`.
* `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`. If `Redirected` then authentication occurs through the injection of an additional screen, which contains the SASLogon prompt. This allows for more complex authentication flows (such as 2FA) and avoids the need to handle passwords in the application itself. The styling of the redirect flow can also be modified. If left at "Default" then the developer must capture the username and password and use these with the `.login()` method. * `LoginMechanism` - either `Default` or `Redirected`. If `Redirected` then authentication occurs through the injection of an additional screen, which contains the SASLogon prompt. This allows for more complex authentication flows (such as 2FA) and avoids the need to handle passwords in the application itself. The styling of the redirect flow can also be modified. If left at "Default" then the developer must capture the username and password and use these with the `.login()` method.
* `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.
The adapter supports a number of approaches for interfacing with Viya (`serverType` is `SASVIYA`). For maximum performance, be sure to [configure your compute context](https://sasjs.io/guide-viya/#shared-account-and-server-re-use) with `reuseServerProcesses` as `true` and a system account in `runServerAs`. This functionality is available since Viya 3.5. This configuration is supported when [creating contexts using the CLI](https://sasjs.io/sasjs-cli-context/#sasjs-context-create). The adapter supports a number of approaches for interfacing with Viya (`serverType` is `SASVIYA`). For maximum performance, be sure to [configure your compute context](https://sasjs.io/guide-viya/#shared-account-and-server-re-use) with `reuseServerProcesses` as `true` and a system account in `runServerAs`. This functionality is available since Viya 3.5. This configuration is supported when [creating contexts using the CLI](https://sasjs.io/sasjs-cli-context/#sasjs-context-create).

View File

@@ -127,7 +127,7 @@ module.exports = {
setupFiles: [], setupFiles: [],
// A list of paths to modules that run some code to configure or set up the testing framework before each test // A list of paths to modules that run some code to configure or set up the testing framework before each test
setupFilesAfterEnv: ['jest-extended/all'], setupFilesAfterEnv: ['jest-extended'],
// A list of paths to snapshot serializer modules Jest should use for snapshot testing // A list of paths to snapshot serializer modules Jest should use for snapshot testing
// snapshotSerializers: [], // snapshotSerializers: [],

6270
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -40,44 +40,44 @@
}, },
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"@types/axios": "0.14.0", "@types/axios": "^0.14.0",
"@types/express": "4.17.13", "@types/express": "^4.17.13",
"@types/form-data": "2.5.0", "@types/form-data": "^2.5.0",
"@types/jest": "27.0.2", "@types/jest": "^27.0.2",
"@types/mime": "2.0.3", "@types/mime": "^2.0.3",
"@types/pem": "1.9.6", "@types/pem": "^1.9.6",
"@types/tough-cookie": "4.0.1", "@types/tough-cookie": "^4.0.1",
"copyfiles": "2.4.1", "copyfiles": "^2.4.1",
"cp": "0.2.0", "cp": "^0.2.0",
"dotenv": "10.0.0", "dotenv": "^10.0.0",
"express": "4.17.1", "express": "^4.17.1",
"jest": "27.4.7", "jest": "^27.2.0",
"jest-extended": "2.0.0", "jest-extended": "^0.11.5",
"node-polyfill-webpack-plugin": "1.1.4", "node-polyfill-webpack-plugin": "^1.1.4",
"path": "0.12.7", "path": "^0.12.7",
"pem": "1.14.4", "pem": "^1.14.4",
"process": "0.11.10", "process": "^0.11.10",
"rimraf": "3.0.2", "rimraf": "^3.0.2",
"semantic-release": "18.0.0", "semantic-release": "^18.0.0",
"terser-webpack-plugin": "5.3.0", "terser-webpack-plugin": "^5.2.4",
"ts-jest": "27.1.3", "ts-jest": "^27.0.3",
"ts-loader": "9.2.6", "ts-loader": "^9.2.6",
"tslint": "6.1.3", "tslint": "^6.1.3",
"tslint-config-prettier": "1.18.0", "tslint-config-prettier": "^1.18.0",
"typedoc": "0.22.11", "typedoc": "0.19.2",
"typedoc-neo-theme": "1.1.1", "typedoc-neo-theme": "^1.1.1",
"typedoc-plugin-external-module-name": "4.0.6", "typedoc-plugin-external-module-name": "^4.0.6",
"typescript": "4.5.4", "typescript": "4.3.5",
"webpack": "5.66.0", "webpack": "^5.56.0",
"webpack-cli": "4.7.2" "webpack-cli": "^4.7.2"
}, },
"main": "index.js", "main": "index.js",
"dependencies": { "dependencies": {
"@sasjs/utils": "2.35.0", "@sasjs/utils": "^2.32.0",
"axios": "0.26.0", "axios": "^0.21.4",
"axios-cookiejar-support": "1.0.1", "axios-cookiejar-support": "^1.0.1",
"form-data": "4.0.0", "form-data": "^4.0.0",
"https": "1.0.0", "https": "^1.0.0",
"tough-cookie": "4.0.0" "tough-cookie": "^4.0.0"
} }
} }

View File

@@ -70,7 +70,7 @@ parmcards4;
%webout(FETCH) %webout(FETCH)
%webout(OPEN) %webout(OPEN)
%macro x(); %macro x();
%do i=1 %to &_webin_file_count; %webout(OBJ,&&_webin_name&i,missing=STRING) %end; %do i=1 %to &_webin_file_count; %webout(OBJ,&&_webin_name&i) %end;
%mend; %x() %mend; %x()
%webout(CLOSE) %webout(CLOSE)
;;;; ;;;;
@@ -79,7 +79,7 @@ parmcards4;
%webout(FETCH) %webout(FETCH)
%webout(OPEN) %webout(OPEN)
%macro x(); %macro x();
%do i=1 %to &_webin_file_count; %webout(ARR,&&_webin_name&i,missing=STRING) %end; %do i=1 %to &_webin_file_count; %webout(ARR,&&_webin_name&i) %end;
%mend; %x() %mend; %x()
%webout(CLOSE) %webout(CLOSE)
;;;; ;;;;
@@ -111,7 +111,7 @@ parmcards4;
%macro x(); %macro x();
%do i=1 %to %sysfunc(countw(&sasjs_tables)); %do i=1 %to %sysfunc(countw(&sasjs_tables));
%let table=%scan(&sasjs_tables,&i); %let table=%scan(&sasjs_tables,&i);
%webout(OBJ,&table,missing=STRING) %webout(OBJ,&table)
%end; %end;
%mend; %mend;
%x() %x()
@@ -125,7 +125,7 @@ parmcards4;
%macro x(); %macro x();
%do i=1 %to %sysfunc(countw(&sasjs_tables)); %do i=1 %to %sysfunc(countw(&sasjs_tables));
%let table=%scan(&sasjs_tables,&i); %let table=%scan(&sasjs_tables,&i);
%webout(ARR,&table,missing=STRING) %webout(ARR,&table)
%end; %end;
%mend; %mend;
%x() %x()

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.4.2",
"@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",
@@ -22,7 +22,7 @@
"build": "react-scripts build", "build": "react-scripts build",
"test": "react-scripts test", "test": "react-scripts test",
"eject": "react-scripts eject", "eject": "react-scripts eject",
"update:adapter": "cd .. && npm run package:lib && cd sasjs-tests && npm i ../build/sasjs-adapter-5.0.0.tgz --legacy-peer-deps", "update:adapter": "cd .. && npm run package:lib && cd sasjs-tests && npm i ../build/sasjs-adapter-5.0.0.tgz",
"deploy:tests": "rsync -avhe ssh ./build/* --delete $SSH_ACCOUNT:$DEPLOY_PATH || npm run deploy:tests-win", "deploy:tests": "rsync -avhe ssh ./build/* --delete $SSH_ACCOUNT:$DEPLOY_PATH || npm run deploy:tests-win",
"deploy:tests-win": "scp %DEPLOY_PATH% ./build/*", "deploy:tests-win": "scp %DEPLOY_PATH% ./build/*",
"deploy": "npm run update:adapter && npm run build && npm run deploy:tests" "deploy": "npm run update:adapter && npm run build && npm run deploy:tests"
@@ -43,6 +43,6 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"node-sass": "^6.0.1" "node-sass": "^5.0.0"
} }
} }

View File

@@ -6,7 +6,6 @@ const stringData: any = { table1: [{ col1: 'first col value' }] }
const defaultConfig: SASjsConfig = { const defaultConfig: SASjsConfig = {
serverUrl: window.location.origin, serverUrl: window.location.origin,
pathSASJS: '/SASjsApi/stp/execute',
pathSAS9: '/SASStoredProcess/do', pathSAS9: '/SASStoredProcess/do',
pathSASViya: '/SASJobExecution', pathSASViya: '/SASJobExecution',
appLoc: '/Public/seedapp', appLoc: '/Public/seedapp',

View File

@@ -79,19 +79,6 @@ const errorAndCsrfData: any = {
_csrf: [{ col1: 'q', col2: 'w', col3: 'e', col4: 'r' }] _csrf: [{ col1: 'q', col2: 'w', col3: 'e', col4: 'r' }]
} }
const testTable = 'sometable'
export const testTableWithNullVars: { [key: string]: any } = {
[testTable]: [
{ var1: 'string', var2: 232, nullvar: 'A' },
{ var1: 'string', var2: 232, nullvar: 'B' },
{ var1: 'string', var2: 232, nullvar: '_' },
{ var1: 'string', var2: 232, nullvar: 0 },
{ var1: 'string', var2: 232, nullvar: 'z' },
{ var1: 'string', var2: 232, nullvar: null }
],
[`$${testTable}`]: { formats: { var1: '$char12.', nullvar: 'best.' } }
}
export const specialCaseTests = (adapter: SASjs): TestSuite => ({ export const specialCaseTests = (adapter: SASjs): TestSuite => ({
name: 'Special Cases', name: 'Special Cases',
tests: [ tests: [
@@ -260,39 +247,6 @@ export const specialCaseTests = (adapter: SASjs): TestSuite => ({
res._csrf[0].COL4 === errorAndCsrfData._csrf[0].col4 res._csrf[0].COL4 === errorAndCsrfData._csrf[0].col4
) )
} }
},
{
title: 'Special missing values',
description: 'Should support special missing values',
test: () => {
return adapter.request('common/sendObj', testTableWithNullVars)
},
assertion: (res: any) => {
let assertionRes = true
testTableWithNullVars[testTable].forEach(
(row: { [key: string]: any }, i: number) =>
Object.keys(row).forEach((col: string) => {
const resValue = res[testTable][i][col.toUpperCase()]
if (
typeof row[col] === 'string' &&
testTableWithNullVars[`$${testTable}`].formats[col] ===
'best.' &&
row[col].toUpperCase() !== resValue
) {
assertionRes = false
} else if (
typeof row[col] !== 'string' &&
row[col] !== resValue
) {
assertionRes = false
}
})
)
return assertionRes
}
} }
] ]
}) })

View File

@@ -5,8 +5,9 @@ import {
EditContextInput, EditContextInput,
PollOptions, PollOptions,
LoginMechanism, LoginMechanism,
ExecutionQuery, FolderMember,
FileTree ServiceMember,
ExecutionQuery
} from './types' } from './types'
import { SASViyaApiClient } from './SASViyaApiClient' import { SASViyaApiClient } from './SASViyaApiClient'
import { SAS9ApiClient } from './SAS9ApiClient' import { SAS9ApiClient } from './SAS9ApiClient'
@@ -27,7 +28,6 @@ import {
ComputeJobExecutor, ComputeJobExecutor,
JesJobExecutor, JesJobExecutor,
Sas9JobExecutor, Sas9JobExecutor,
SasJsJobExecutor,
FileUploader FileUploader
} from './job-execution' } from './job-execution'
import { ErrorResponse } from './types/errors' import { ErrorResponse } from './types/errors'
@@ -63,7 +63,6 @@ export default class SASjs {
private computeJobExecutor: JobExecutor | null = null private computeJobExecutor: JobExecutor | null = null
private jesJobExecutor: JobExecutor | null = null private jesJobExecutor: JobExecutor | null = null
private sas9JobExecutor: JobExecutor | null = null private sas9JobExecutor: JobExecutor | null = null
private sasJsJobExecutor: JobExecutor | null = null
constructor(config?: Partial<SASjsConfig>) { constructor(config?: Partial<SASjsConfig>) {
this.sasjsConfig = { this.sasjsConfig = {
@@ -78,12 +77,6 @@ export default class SASjs {
return this.requestClient?.getCsrfToken(type) return this.requestClient?.getCsrfToken(type)
} }
/**
* Executes the sas code against SAS9 server
* @param linesOfCode - lines of sas code from the file to run.
* @param username - a string representing the username.
* @param password - a string representing the password.
*/
public async executeScriptSAS9( public async executeScriptSAS9(
linesOfCode: string[], linesOfCode: string[],
userName: string, userName: string,
@@ -98,38 +91,6 @@ export default class SASjs {
) )
} }
/**
* Executes the sas code against SASViya server
* @param fileName - name of the file to run. It will be converted to path to the file being submitted for execution.
* @param linesOfCode - lines of sas code from the file to run.
* @param contextName - context name on which code will be run on the server.
* @param authConfig - (optional) the access token, refresh token, client and secret for authorizing the request.
* @param debug - (optional) if true, global debug config will be overriden
*/
public async executeScriptSASViya(
fileName: string,
linesOfCode: string[],
contextName: string,
authConfig?: AuthConfig,
debug?: boolean
) {
this.isMethodSupported('executeScriptSASViya', [ServerType.SasViya])
if (!contextName) {
throw new Error(
'Context name is undefined. Please set a `contextName` in your SASjs or override config.'
)
}
return await this.sasViyaApiClient!.executeScript(
fileName,
linesOfCode,
contextName,
authConfig,
null,
debug ? debug : this.sasjsConfig.debug
)
}
/** /**
* Gets compute contexts. * Gets compute contexts.
* @param accessToken - an access token for an authorized user. * @param accessToken - an access token for an authorized user.
@@ -293,6 +254,38 @@ export default class SASjs {
return await this.sasViyaApiClient!.createSession(contextName, accessToken) return await this.sasViyaApiClient!.createSession(contextName, accessToken)
} }
/**
* Executes the sas code against given sas server
* @param fileName - name of the file to run. It will be converted to path to the file being submitted for execution.
* @param linesOfCode - lines of sas code from the file to run.
* @param contextName - context name on which code will be run on the server.
* @param authConfig - (optional) the access token, refresh token, client and secret for authorizing the request.
* @param debug - (optional) if true, global debug config will be overriden
*/
public async executeScriptSASViya(
fileName: string,
linesOfCode: string[],
contextName: string,
authConfig?: AuthConfig,
debug?: boolean
) {
this.isMethodSupported('executeScriptSASViya', [ServerType.SasViya])
if (!contextName) {
throw new Error(
'Context name is undefined. Please set a `contextName` in your SASjs or override config.'
)
}
return await this.sasViyaApiClient!.executeScript(
fileName,
linesOfCode,
contextName,
authConfig,
null,
debug ? debug : this.sasjsConfig.debug
)
}
/** /**
* Creates a folder in the logical SAS folder tree * Creates a folder in the logical SAS folder tree
* @param folderName - name of the folder to be created. * @param folderName - name of the folder to be created.
@@ -683,16 +676,7 @@ export default class SASjs {
const validationResult = this.validateInput(data) const validationResult = this.validateInput(data)
if (validationResult.status) { if (validationResult.status) {
if (config.serverType === ServerType.Sasjs) { if (
return await this.sasJsJobExecutor!.execute(
sasJob,
data,
config,
loginRequiredCallback,
authConfig,
extraResponseAttributes
)
} else if (
config.serverType !== ServerType.Sas9 && config.serverType !== ServerType.Sas9 &&
config.useComputeApi !== undefined && config.useComputeApi !== undefined &&
config.useComputeApi !== null config.useComputeApi !== null
@@ -752,19 +736,15 @@ export default class SASjs {
msg: string msg: string
} { } {
if (data === null) return { status: true, msg: '' } if (data === null) return { status: true, msg: '' }
const isSasFormatsTable = (key: string) =>
key.match(/^\$.*/) && Object.keys(data).includes(key.replace(/^\$/, ''))
for (const key in data) { for (const key in data) {
if (!key.match(/^[a-zA-Z_]/) && !isSasFormatsTable(key)) { if (!key.match(/^[a-zA-Z_]/)) {
return { return {
status: false, status: false,
msg: 'First letter of table should be alphabet or underscore.' msg: 'First letter of table should be alphabet or underscore.'
} }
} }
if (!key.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/) && !isSasFormatsTable(key)) { if (!key.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/)) {
return { status: false, msg: 'Table name should be alphanumeric.' } return { status: false, msg: 'Table name should be alphanumeric.' }
} }
@@ -775,7 +755,7 @@ export default class SASjs {
} }
} }
if (this.getType(data[key]) !== 'Array' && !isSasFormatsTable(key)) { if (this.getType(data[key]) !== 'Array') {
return { return {
status: false, status: false,
msg: 'Parameter data contains invalid table structure.' msg: 'Parameter data contains invalid table structure.'
@@ -791,7 +771,6 @@ export default class SASjs {
} }
} }
} }
return { status: true, msg: '' } return { status: true, msg: '' }
} }
@@ -881,22 +860,8 @@ export default class SASjs {
) )
} }
/** public async deployToSASjs(members: [FolderMember, ServiceMember]) {
* Creates the folders and services at the given location `appLoc` on the given server `serverUrl`. return await this.sasJSApiClient?.deploy(members, this.sasjsConfig.appLoc)
* @param members - the JSON specifying the folders and services to be created.
* @param appLoc - the base folder in which to create the new folders and
* services. If not provided, is taken from SASjsConfig.
* @param authConfig - a valid client, secret, refresh and access tokens that are authorised to execute compute jobs.
*/
public async deployToSASjs(
members: FileTree,
appLoc?: string,
authConfig?: AuthConfig
) {
if (!appLoc) {
appLoc = this.sasjsConfig.appLoc
}
return await this.sasJSApiClient?.deploy(members, appLoc, authConfig)
} }
public async executeJobSASjs(query: ExecutionQuery) { public async executeJobSASjs(query: ExecutionQuery) {
@@ -1034,8 +999,7 @@ export default class SASjs {
: RequestClient : RequestClient
this.requestClient = new RequestClientClass( this.requestClient = new RequestClientClass(
this.sasjsConfig.serverUrl, this.sasjsConfig.serverUrl,
this.sasjsConfig.httpsAgentOptions, this.sasjsConfig.httpsAgentOptions
this.sasjsConfig.requestHistoryLimit
) )
} else { } else {
this.requestClient.setConfig( this.requestClient.setConfig(
@@ -1049,7 +1013,7 @@ export default class SASjs {
? this.sasjsConfig.pathSASViya ? this.sasjsConfig.pathSASViya
: this.sasjsConfig.serverType === ServerType.Sas9 : this.sasjsConfig.serverType === ServerType.Sas9
? this.sasjsConfig.pathSAS9 ? this.sasjsConfig.pathSAS9
: this.sasjsConfig.pathSASJS : this.sasjsConfig.pathSASJS || ''
this.authManager = new AuthManager( this.authManager = new AuthManager(
this.sasjsConfig.serverUrl, this.sasjsConfig.serverUrl,
@@ -1114,13 +1078,6 @@ export default class SASjs {
this.sasViyaApiClient! this.sasViyaApiClient!
) )
this.sasJsJobExecutor = new SasJsJobExecutor(
this.sasjsConfig.serverUrl,
this.sasjsConfig.serverType!,
this.jobsPath,
this.requestClient
)
this.sas9JobExecutor = new Sas9JobExecutor( this.sas9JobExecutor = new Sas9JobExecutor(
this.sasjsConfig.serverUrl, this.sasjsConfig.serverUrl,
this.sasjsConfig.serverType!, this.sasjsConfig.serverType!,

View File

@@ -1,11 +1,8 @@
import { AuthConfig, ServerType } from '@sasjs/utils/types' import { FolderMember, ServiceMember, ExecutionQuery } from './types'
import { FileTree, ExecutionQuery } from './types'
import { RequestClient } from './request/RequestClient' import { RequestClient } from './request/RequestClient'
import { getAccessTokenForSasjs } from './auth/getAccessTokenForSasjs' import { getAccessTokenForSasjs } from './auth/getAccessTokenForSasjs'
import { refreshTokensForSasjs } from './auth/refreshTokensForSasjs' import { refreshTokensForSasjs } from './auth/refreshTokensForSasjs'
import { getAuthCodeForSasjs } from './auth/getAuthCodeForSasjs' import { getAuthCodeForSasjs } from './auth/getAuthCodeForSasjs'
import { parseWeboutResponse } from './utils'
import { getTokens } from './auth/getTokens'
export class SASjsApiClient { export class SASjsApiClient {
constructor( constructor(
@@ -17,19 +14,7 @@ export class SASjsApiClient {
if (serverUrl) this.serverUrl = serverUrl if (serverUrl) this.serverUrl = serverUrl
} }
public async deploy( public async deploy(members: [FolderMember, ServiceMember], appLoc: string) {
members: FileTree,
appLoc: string,
authConfig?: AuthConfig
) {
let access_token = (authConfig || {}).access_token
if (authConfig) {
;({ access_token } = await getTokens(
this.requestClient,
authConfig,
ServerType.Sasjs
))
}
const { result } = await this.requestClient.post<{ const { result } = await this.requestClient.post<{
status: string status: string
message: string message: string
@@ -37,7 +22,7 @@ export class SASjsApiClient {
}>( }>(
'SASjsApi/drive/deploy', 'SASjsApi/drive/deploy',
{ fileTree: members, appLoc: appLoc }, { fileTree: members, appLoc: appLoc },
access_token undefined
) )
return Promise.resolve(result) return Promise.resolve(result)
@@ -50,13 +35,8 @@ export class SASjsApiClient {
log?: string log?: string
logPath?: string logPath?: string
error?: {} error?: {}
_webout?: string
}>('SASjsApi/stp/execute', query, undefined) }>('SASjsApi/stp/execute', query, undefined)
if (Object.keys(result).includes('_webout')) {
result._webout = parseWeboutResponse(result._webout!)
}
return Promise.resolve(result) return Promise.resolve(result)
} }

View File

@@ -272,13 +272,7 @@ export async function executeScript(
return { result: jobResult?.result, log } return { result: jobResult?.result, log }
} catch (e) { } catch (e) {
interface HttpError { if (e && e.status === 404) {
status: number
}
const error = e as HttpError
if (error.status === 404) {
return executeScript( return executeScript(
requestClient, requestClient,
sessionManager, sessionManager,
@@ -293,7 +287,7 @@ export async function executeScript(
true true
) )
} else { } else {
throw prefixMessage(e as Error, 'Error while executing script. ') throw prefixMessage(e, 'Error while executing script. ')
} }
} }
} }

View File

@@ -1,34 +1,24 @@
import { WriteStream } from '../../../types' import { WriteStream } from '../../../types'
import { writeStream } from '../writeStream' import { writeStream } from '../writeStream'
import { import 'jest-extended'
createWriteStream,
fileExists,
readFile,
deleteFile
} from '@sasjs/utils'
describe('writeStream', () => { describe('writeStream', () => {
const filename = 'test.txt' const stream: WriteStream = {
const content = 'test' write: jest.fn(),
let stream: WriteStream path: 'test'
}
beforeAll(async () => {
stream = await createWriteStream(filename)
})
it('should resolve when the stream is written successfully', async () => { it('should resolve when the stream is written successfully', async () => {
await expect(writeStream(stream, content)).toResolve() expect(writeStream(stream, 'test')).toResolve()
await expect(fileExists(filename)).resolves.toEqual(true)
await expect(readFile(filename)).resolves.toEqual(content + '\n')
await deleteFile(filename) expect(stream.write).toHaveBeenCalledWith('test\n', expect.anything())
}) })
it('should reject when the write errors out', async () => { it('should reject when the write errors out', async () => {
jest jest
.spyOn(stream, 'write') .spyOn(stream, 'write')
.mockImplementation((_, callback) => callback(new Error('Test Error'))) .mockImplementation((_, callback) => callback(new Error('Test Error')))
const error = await writeStream(stream, content).catch((e) => e) const error = await writeStream(stream, 'test').catch((e) => e)
expect(error.message).toEqual('Test Error') expect(error.message).toEqual('Test Error')
}) })

View File

@@ -3,9 +3,13 @@ import { WriteStream } from '../../types'
export const writeStream = async ( export const writeStream = async (
stream: WriteStream, stream: WriteStream,
content: string content: string
): Promise<void> => ): Promise<void> => {
return new Promise((resolve, reject) => {
stream.write(content + '\n', (e) => { stream.write(content + '\n', (e) => {
if (e) return Promise.reject(e) if (e) {
return reject(e)
return Promise.resolve() }
return resolve()
}) })
})
}

View File

@@ -5,11 +5,8 @@ export const generateFileUploadForm = (
data: any data: any
): FormData => { ): FormData => {
for (const tableName in data) { for (const tableName in data) {
if (!Array.isArray(data[tableName])) continue
const name = tableName const name = tableName
const csv = convertToCSV(data[tableName]) const csv = convertToCSV(data[tableName])
if (csv === 'ERROR: LARGE STRING LENGTH') { if (csv === 'ERROR: LARGE STRING LENGTH') {
throw new Error( throw new Error(
'The max length of a string value in SASjs is 32765 characters.' 'The max length of a string value in SASjs is 32765 characters.'

View File

@@ -1,55 +0,0 @@
import { generateFileUploadForm } from '../generateFileUploadForm'
describe('generateFileUploadForm', () => {
beforeAll(() => {
function FormDataMock(this: any) {
this.append = () => {}
}
const BlobMock = jest.fn()
;(global as any).FormData = FormDataMock
;(global as any).Blob = BlobMock
})
it('should generate file upload form from data', () => {
const formData = new FormData()
const testTable = 'sometable'
const testTableWithNullVars: { [key: string]: any } = {
[testTable]: [
{ var1: 'string', var2: 232, nullvar: 'A' },
{ var1: 'string', var2: 232, nullvar: 'B' },
{ var1: 'string', var2: 232, nullvar: '_' },
{ var1: 'string', var2: 232, nullvar: 0 },
{ var1: 'string', var2: 232, nullvar: 'z' },
{ var1: 'string', var2: 232, nullvar: null }
],
[`$${testTable}`]: { formats: { var1: '$char12.', nullvar: 'best.' } }
}
const tableName = Object.keys(testTableWithNullVars).filter((key: string) =>
Array.isArray(testTableWithNullVars[key])
)[0]
jest.spyOn(formData, 'append').mockImplementation(() => {})
generateFileUploadForm(formData, testTableWithNullVars)
expect(formData.append).toHaveBeenCalledOnce()
expect(formData.append).toHaveBeenCalledWith(
tableName,
{},
`${tableName}.csv`
)
})
it('should throw an error if too large string was provided', () => {
const formData = new FormData()
const data = { testTable: [{ var1: 'z'.repeat(32765 + 1) }] }
expect(() => generateFileUploadForm(formData, data)).toThrow(
new Error(
'The max length of a string value in SASjs is 32765 characters.'
)
)
})
})

View File

@@ -1,131 +0,0 @@
import {
AuthConfig,
ExtraResponseAttributes,
ServerType
} from '@sasjs/utils/types'
import {
ErrorResponse,
JobExecutionError,
LoginRequiredError
} from '../types/errors'
import { RequestClient } from '../request/RequestClient'
import {
isRelativePath,
appendExtraResponseAttributes,
getValidJson
} from '../utils'
import { BaseJobExecutor } from './JobExecutor'
import { parseWeboutResponse } from '../utils/parseWeboutResponse'
export class SasJsJobExecutor extends BaseJobExecutor {
constructor(
serverUrl: string,
serverType: ServerType,
private jobsPath: string,
private requestClient: RequestClient
) {
super(serverUrl, serverType)
}
async execute(
sasJob: string,
data: any,
config: any,
loginRequiredCallback?: any,
authConfig?: AuthConfig,
extraResponseAttributes: ExtraResponseAttributes[] = []
) {
const loginCallback = loginRequiredCallback || (() => Promise.resolve())
const program = isRelativePath(sasJob)
? config.appLoc
? config.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
: sasJob
: sasJob
let apiUrl = `${config.serverUrl}${this.jobsPath}/?${'_program=' + program}`
const requestParams = this.getRequestParams(config)
const requestPromise = new Promise((resolve, reject) => {
this.requestClient!.post(
apiUrl,
{ ...requestParams, ...data },
authConfig?.access_token
)
.then(async (res: any) => {
const parsedSasjsServerLog = res.result.log
.map((logLine: any) => logLine.line)
.join('\n')
const resObj = {
result: res.result._webout,
log: parsedSasjsServerLog
}
this.requestClient!.appendRequest(resObj, sasJob, config.debug)
let jsonResponse = res.result
if (config.debug) {
if (typeof res.result._webout === 'object') {
jsonResponse = res.result._webout
} else {
const webout = parseWeboutResponse(res.result._webout, apiUrl)
jsonResponse = getValidJson(webout)
}
} else {
jsonResponse = getValidJson(res.result._webout)
}
const responseObject = appendExtraResponseAttributes(
{ result: jsonResponse },
extraResponseAttributes
)
resolve(responseObject)
})
.catch(async (e: Error) => {
if (e instanceof JobExecutionError) {
this.requestClient!.appendRequest(e, sasJob, config.debug)
reject(new ErrorResponse(e?.message, e))
}
if (e instanceof LoginRequiredError) {
this.appendWaitingRequest(() => {
return this.execute(
sasJob,
data,
config,
loginRequiredCallback,
authConfig,
extraResponseAttributes
).then(
(res: any) => {
resolve(res)
},
(err: any) => {
reject(err)
}
)
})
await loginCallback()
} else {
reject(new ErrorResponse(e?.message, e))
}
})
})
return requestPromise
}
private getRequestParams(config: any): any {
const requestParams: any = {}
if (config.debug) {
requestParams['_omittextlog'] = 'false'
requestParams['_omitsessionresults'] = 'false'
requestParams['_debug'] = 131
}
return requestParams
}
}

View File

@@ -146,16 +146,11 @@ export class WebJobExecutor extends BaseJobExecutor {
const requestPromise = new Promise((resolve, reject) => { const requestPromise = new Promise((resolve, reject) => {
this.requestClient!.post(apiUrl, formData, authConfig?.access_token) this.requestClient!.post(apiUrl, formData, authConfig?.access_token)
.then(async (res: any) => { .then(async (res: any) => {
const parsedSasjsServerLog =
this.serverType === ServerType.Sasjs
? res.result.log.map((logLine: any) => logLine.line).join('\n')
: res.result.log
const resObj = const resObj =
this.serverType === ServerType.Sasjs this.serverType === ServerType.Sasjs
? { ? {
result: res.result._webout, result: res.result._webout,
log: parsedSasjsServerLog log: res.result.log
} }
: res : res
this.requestClient!.appendRequest(resObj, sasJob, config.debug) this.requestClient!.appendRequest(resObj, sasJob, config.debug)
@@ -178,12 +173,8 @@ export class WebJobExecutor extends BaseJobExecutor {
: res.result : res.result
break break
case ServerType.Sasjs: case ServerType.Sasjs:
if (typeof res.result._webout === 'object') {
jsonResponse = res.result._webout
} else {
const webout = parseWeboutResponse(res.result._webout, apiUrl) const webout = parseWeboutResponse(res.result._webout, apiUrl)
jsonResponse = getValidJson(webout) jsonResponse = getValidJson(webout)
}
break break
} }
} else if (this.serverType === ServerType.Sasjs) { } else if (this.serverType === ServerType.Sasjs) {

View File

@@ -1,7 +1,6 @@
export * from './ComputeJobExecutor' export * from './ComputeJobExecutor'
export * from './FileUploader'
export * from './JesJobExecutor' export * from './JesJobExecutor'
export * from './JobExecutor' export * from './JobExecutor'
export * from './Sas9JobExecutor' export * from './Sas9JobExecutor'
export * from './SasJsJobExecutor'
export * from './WebJobExecutor' export * from './WebJobExecutor'
export * from './FileUploader'

View File

@@ -56,7 +56,6 @@ export interface HttpClient {
export class RequestClient implements HttpClient { export class RequestClient implements HttpClient {
private requests: SASjsRequest[] = [] private requests: SASjsRequest[] = []
private requestsLimit: number = 10
protected csrfToken: CsrfToken = { headerName: '', value: '' } protected csrfToken: CsrfToken = { headerName: '', value: '' }
protected fileUploadCsrfToken: CsrfToken | undefined protected fileUploadCsrfToken: CsrfToken | undefined
@@ -64,11 +63,9 @@ export class RequestClient implements HttpClient {
constructor( constructor(
protected baseUrl: string, protected baseUrl: string,
httpsAgentOptions?: https.AgentOptions, httpsAgentOptions?: https.AgentOptions
requestsLimit?: number
) { ) {
this.createHttpClient(baseUrl, httpsAgentOptions) this.createHttpClient(baseUrl, httpsAgentOptions)
if (requestsLimit) this.requestsLimit = requestsLimit
} }
public setConfig(baseUrl: string, httpsAgentOptions?: https.AgentOptions) { public setConfig(baseUrl: string, httpsAgentOptions?: https.AgentOptions) {
@@ -152,7 +149,7 @@ export class RequestClient implements HttpClient {
SASWORK: sasWork SASWORK: sasWork
}) })
if (this.requests.length > this.requestsLimit) { if (this.requests.length > 20) {
this.requests.splice(0, 1) this.requests.splice(0, 1)
} }
} }

View File

@@ -13,7 +13,7 @@ export class SasjsRequestClient extends RequestClient {
headers.Accept = contentType === 'application/json' ? contentType : '*/*' headers.Accept = contentType === 'application/json' ? contentType : '*/*'
if (!accessToken && typeof window !== 'undefined') if (!accessToken)
accessToken = localStorage.getItem('accessToken') ?? undefined accessToken = localStorage.getItem('accessToken') ?? undefined
if (accessToken) headers.Authorization = `Bearer ${accessToken}` if (accessToken) headers.Authorization = `Bearer ${accessToken}`

View File

@@ -423,7 +423,7 @@ describe('ContextManager', () => {
true true
) )
} catch (error) { } catch (error) {
editError = error as Error editError = error
} }
await expect( await expect(
@@ -542,7 +542,7 @@ describe('ContextManager', () => {
true true
) )
} catch (error) { } catch (error) {
deleteError = error as Error deleteError = error
} }
await expect( await expect(

View File

@@ -1,95 +0,0 @@
import { formatDataForRequest } from '../../utils/formatDataForRequest'
describe('formatDataForRequest', () => {
const testTable = 'sometable'
it('should format table with special missing values', () => {
const tableWithMissingValues = {
[testTable]: [
{ var1: 'string', var2: 232, nullvar: 'A' },
{ var1: 'string', var2: 232, nullvar: 'B' },
{ var1: 'string', var2: 232, nullvar: '_' },
{ var1: 'string', var2: 232, nullvar: 0 },
{ var1: 'string', var2: 232, nullvar: 'z' },
{ var1: 'string', var2: 232, nullvar: null }
],
[`$${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,.`,
sasjs_tables: testTable
}
expect(formatDataForRequest(tableWithMissingValues)).toEqual(expectedOutput)
})
it('should return error if string is more than 32765 characters', () => {
const data = { testTable: [{ var1: 'z'.repeat(32765 + 1) }] }
expect(() => formatDataForRequest(data)).toThrow(
new Error(
'The max length of a string value in SASjs is 32765 characters.'
)
)
})
it('should return error if string is more than 32765 characters', () => {
const charsCount = 16 * 1000 + 1
const allChars = 'z'.repeat(charsCount)
const data = { [testTable]: [{ var1: allChars }] }
const firstChunk = `var1:$char${charsCount}.\r\n`
const firstChunkChars = 'z'.repeat(16000 - firstChunk.length)
const secondChunkChars = 'z'.repeat(
charsCount - (16000 - firstChunk.length)
)
const expectedOutput = {
sasjs1data0: 2,
sasjs1data1: `${firstChunk}${firstChunkChars}`,
sasjs1data2: secondChunkChars,
sasjs_tables: testTable
}
expect(formatDataForRequest(data)).toEqual(expectedOutput)
})
it('should throw an error if special missing values is not valid', () => {
let tableWithMissingValues = {
[testTable]: [{ var: 'AA' }, { var: 0 }],
[`$${testTable}`]: { formats: { var: 'best.' } }
}
expect(() => formatDataForRequest(tableWithMissingValues)).toThrow(
new Error(
'Special missing value can only be a single character from A to Z or _'
)
)
})
it('should auto-detect special missing values type as best.', () => {
const tableWithMissingValues = {
[testTable]: [{ var: 'a' }, { var: 'A' }, { var: '_' }, { var: 0 }]
}
const expectedOutput = {
sasjs1data: `var:best.\r\n.a\r\n.a\r\n._\r\n0`,
sasjs_tables: testTable
}
expect(formatDataForRequest(tableWithMissingValues)).toEqual(expectedOutput)
})
it('should auto-detect values type as $char1.', () => {
const tableWithMissingValues = {
[testTable]: [{ var: 'a' }, { var: 'A' }, { var: '_' }]
}
const expectedOutput = {
sasjs1data: `var:$char1.\r\na\r\nA\r\n_`,
sasjs_tables: testTable
}
expect(formatDataForRequest(tableWithMissingValues)).toEqual(expectedOutput)
})
})

View File

@@ -16,7 +16,7 @@ export class SASjsConfig {
* The location of the STP Process Web Application. By default the adapter * The location of the STP Process Web Application. By default the adapter
* will use '/SASjsApi/stp/execute' on SAS JS. * will use '/SASjsApi/stp/execute' on SAS JS.
*/ */
pathSASJS: string = '' pathSASJS?: string = ''
/** /**
* The location of the Stored Process Web Application. By default the adapter * The location of the Stored Process Web Application. By default the adapter
* will use '/SASStoredProcess/do' on SAS 9. * will use '/SASStoredProcess/do' on SAS 9.
@@ -60,7 +60,7 @@ export class SASjsConfig {
*/ */
useComputeApi: boolean | null = null useComputeApi: boolean | null = null
/** /**
* Optional setting to configure HTTPS Agent. * Optional settings to configure HTTPS Agent.
* By providing `key`, `cert`, `ca` to connect with server * By providing `key`, `cert`, `ca` to connect with server
* Other options can be set `rejectUnauthorized` and `requestCert` * Other options can be set `rejectUnauthorized` and `requestCert`
*/ */
@@ -69,11 +69,6 @@ export class SASjsConfig {
* Supported login mechanisms are - Redirected and Default * Supported login mechanisms are - Redirected and Default
*/ */
loginMechanism: LoginMechanism = LoginMechanism.Default loginMechanism: LoginMechanism = LoginMechanism.Default
/**
* Optional setting to configure request history limit. Increasing this limit
* may affect browser performance, especially with debug (logs) enabled.
*/
requestHistoryLimit?: number = 10
} }
export enum LoginMechanism { export enum LoginMechanism {

View File

@@ -1 +0,0 @@
import 'jest-extended'

View File

@@ -167,17 +167,4 @@ describe('convertToCsv', () => {
convertToCSV([{ slashWithSpecialExtra: '\\\ts\tl\ta\ts\t\th\t' }]) convertToCSV([{ slashWithSpecialExtra: '\\\ts\tl\ta\ts\t\th\t' }])
).toEqual(`slashWithSpecialExtra:$char13.\r\n\"\\\ts\tl\ta\ts\t\th\t\"`) ).toEqual(`slashWithSpecialExtra:$char13.\r\n\"\\\ts\tl\ta\ts\t\th\t\"`)
}) })
it('should console log error if data has mixed types', () => {
const colName = 'var1'
const data = [{ [colName]: 'string' }, { [colName]: 232 }]
jest.spyOn(console, 'error').mockImplementation(() => {})
convertToCSV(data)
expect(console.error).toHaveBeenCalledWith(
`Row (2), Column (${colName}) has mixed types: ERROR`
)
})
}) })

View File

@@ -2,45 +2,12 @@
* Converts the given JSON object array to a CSV string. * Converts the given JSON object array to a CSV string.
* @param data - the array of JSON objects to convert. * @param data - the array of JSON objects to convert.
*/ */
export const convertToCSV = ( export const convertToCSV = (data: any) => {
data: any[], const replacer = (key: any, value: any) => (value === null ? '' : value)
sasFormats?: { formats: { [key: string]: string } } const headerFields = Object.keys(data[0])
) => {
let formats = sasFormats?.formats
let headers: string[] = []
let csvTest let csvTest
let invalidString = false let invalidString = false
const specialMissingValueRegExp = /^[a-z_]{1}$/i const headers = headerFields.map((field) => {
if (formats) {
headers = Object.keys(formats).map((key) => `${key}:${formats![key]}`)
}
const headerFields = Object.keys(data[0])
headerFields.forEach((field) => {
if (!formats || !Object.keys(formats).includes(field)) {
let hasNullOrNumber = false
let hasSpecialMissingString = false
data.forEach((row: { [key: string]: any }) => {
if (row[field] === null || typeof row[field] === 'number') {
hasNullOrNumber = true
} else if (
typeof row[field] === 'string' &&
specialMissingValueRegExp.test(row[field])
) {
hasSpecialMissingString = true
}
})
if (hasNullOrNumber && hasSpecialMissingString) {
headers.push(`${field}:best.`)
if (!formats) formats = {}
formats[field] = 'best.'
} else {
let firstFoundType: string | null = null let firstFoundType: string | null = null
let hasMixedTypes: boolean = false let hasMixedTypes: boolean = false
let rowNumError: number = -1 let rowNumError: number = -1
@@ -76,41 +43,29 @@ export const convertToCSV = (
return byteSize return byteSize
} }
}) })
.sort((a: any, b: any) => b - a)[0] .sort((a: number, b: number) => b - a)[0]
if (longestValueForField && longestValueForField > 32765) { if (longestValueForField && longestValueForField > 32765) {
invalidString = true invalidString = true
} }
if (hasMixedTypes) { if (hasMixedTypes) {
console.error( console.error(
`Row (${rowNumError}), Column (${field}) has mixed types: ERROR` `Row (${rowNumError}), Column (${field}) has mixed types: ERROR`
) )
} }
headers.push( return `${field}:${firstFoundType === 'chars' ? '$char' : ''}${
`${field}:${firstFoundType === 'chars' ? '$char' : ''}${
longestValueForField longestValueForField
? longestValueForField ? longestValueForField
: firstFoundType === 'chars' : firstFoundType === 'chars'
? '1' ? '1'
: 'best' : 'best'
}.` }.`
)
}
}
}) })
if (sasFormats) { if (invalidString) {
headers = headers.sort( return 'ERROR: LARGE STRING LENGTH'
(a, b) =>
headerFields.indexOf(a.replace(/:.*/, '')) -
headerFields.indexOf(b.replace(/:.*/, ''))
)
} }
if (invalidString) return 'ERROR: LARGE STRING LENGTH'
csvTest = data.map((row: any) => { csvTest = data.map((row: any) => {
const fields = Object.keys(row).map((fieldName, index) => { const fields = Object.keys(row).map((fieldName, index) => {
let value let value
@@ -121,17 +76,6 @@ export const convertToCSV = (
// stringify with replacer converts null values to empty strings // stringify with replacer converts null values to empty strings
value = currentCell === null ? '' : currentCell value = currentCell === null ? '' : currentCell
if (formats && formats[fieldName] === 'best.') {
if (value && !specialMissingValueRegExp.test(value)) {
console.log(`🤖[value]🤖`, value)
throw new Error(
'Special missing value can only be a single character from A to Z or _'
)
}
return `.${value.toLowerCase()}`
}
// if there any present, it should have preceding (") for escaping // if there any present, it should have preceding (") for escaping
value = value.replace(/"/g, `""`) value = value.replace(/"/g, `""`)

View File

@@ -7,17 +7,9 @@ export const formatDataForRequest = (data: any) => {
const result: any = {} const result: any = {}
for (const tableName in data) { for (const tableName in data) {
if (
tableName.match(/^\$.*/) &&
Object.keys(data).includes(tableName.replace(/^\$/, ''))
) {
continue
}
tableCounter++ tableCounter++
sasjsTables.push(tableName) sasjsTables.push(tableName)
const csv = convertToCSV(data[tableName], data[`$${tableName}`]) const csv = convertToCSV(data[tableName])
if (csv === 'ERROR: LARGE STRING LENGTH') { if (csv === 'ERROR: LARGE STRING LENGTH') {
throw new Error( throw new Error(
'The max length of a string value in SASjs is 32765 characters.' 'The max length of a string value in SASjs is 32765 characters.'
@@ -35,7 +27,6 @@ export const formatDataForRequest = (data: any) => {
result[`sasjs${tableCounter}data`] = csv result[`sasjs${tableCounter}data`] = csv
} }
} }
result['sasjs_tables'] = sasjsTables.join(' ') result['sasjs_tables'] = sasjsTables.join(' ')
return result return result

View File

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

View File

@@ -6,8 +6,7 @@
"declaration": true, "declaration": true,
"outDir": "./build", "outDir": "./build",
"strict": true, "strict": true,
"sourceMap": true, "sourceMap": true
"typeRoots": ["./node_modules/@types", "./src/types/system"]
}, },
"include": ["src"], "include": ["src"],
"exclude": ["node_modules"] "exclude": ["node_modules"]