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

Compare commits

...

51 Commits

Author SHA1 Message Date
Allan Bowe
c392390147 Merge pull request #642 from sasjs/sasjs-server-job-executor
feat: execute job on SASJS server
2022-02-17 12:48:41 +02:00
Allan Bowe
78945d9f45 Merge pull request #638 from sasjs/sasjsconfig-pathsasjs
fix: made pathsasjs non optional in sasjs config
2022-02-17 11:39:29 +02:00
Allan Bowe
1e0365de1c Merge pull request #643 from sasjs/sas9-deploy-bug
fix: reverted axios-cookiejar-support package
2022-02-17 11:38:29 +02:00
Saad Jutt
c8d71b0267 fix: reverted axios-cookiejar-support package 2022-02-17 07:20:48 +05:00
Saad Jutt
bfc534da15 feat: execute job on SASJS server 2022-02-17 02:01:19 +05:00
Yury Shkoda
4026b03005 Merge pull request #636 from sasjs/cli-issue-1108
feat(executeJobSASjs): add parse _webout in response
2022-02-15 12:46:55 +03:00
Yury Shkoda
eab19a0e6e chore(deps): bump axios to fix follow-redirects issue 2022-02-15 09:59:26 +03:00
Yury Shkoda
1d7b7d654d chore(git): Merge remote-tracking branch 'origin/master' into cli-issue-1108 2022-02-15 09:52:05 +03:00
2c4152a593 fix: made pathsasjs non optional in sasjs config 2022-02-14 02:02:41 +05:00
Allan Bowe
de51946850 Merge pull request #637 from sasjs/sasjs-server-deployment-with-auth
fix(SASJS): sasjs server deployment with auth + refresh token bug
2022-02-11 18:20:35 +02:00
Saad Jutt
ebd55c5b02 chore: provided JSDoc for deployToSASjs 2022-02-11 21:18:18 +05:00
Saad Jutt
f48089cb8c fix(SASJS): sasjs server deployment with auth + refresh token bug 2022-02-11 21:04:51 +05:00
Yury Shkoda
30a99f9cc5 fix(executeJobSASjs): add parse webout response 2022-02-11 13:10:38 +03:00
Yury Shkoda
b1979f63ef feat(executeJobSASjs): add _returnLog query option 2022-02-10 16:58:15 +03:00
Yury Shkoda
97c3cfd574 Merge pull request #622 from sasjs/update-dependencies
chore: update dependencies
2022-02-01 15:51:40 +03:00
Yury Shkoda
56df578ab2 chore(writeStream): refactor function and improve test 2022-01-31 10:21:20 +03:00
Yury Shkoda
556ab608c5 chore(jest-extended): fix jest-extended import 2022-01-31 10:20:53 +03:00
Yury Shkoda
0633a6de84 chore(deps): fix issues after deps bump 2022-01-31 10:20:05 +03:00
Vladislav Parhomchik
a39f9bb7e8 chore: update dependencies 2022-01-26 14:11:41 +03:00
Yury Shkoda
a0172337ea Merge pull request #617 from sasjs/axios-bump
Fix axios vulnerability
2022-01-18 22:01:35 +03:00
Yury Shkoda
56683bcee2 chore(npm): fixed all dependencies 2022-01-18 20:08:25 +03:00
Yury Shkoda
8d99612695 chore(ci/cd): add check npm audit step 2022-01-18 19:33:18 +03:00
Yury Shkoda
f232fe158c chore(ci/cd): improve user list for review lottery 2022-01-18 19:32:40 +03:00
Yury Shkoda
11b218702b fix(dependencies): numped axios 2022-01-18 19:31:24 +03:00
Yury Shkoda
a957e1d359 Merge pull request #615 from sasjs/issue-607
Support special missing values
2022-01-18 14:58:24 +03:00
Allan Bowe
c6b25d2d49 chore(docs): adding SASJS as a valid serverType 2022-01-09 13:47:09 +00:00
Allan Bowe
ca5eb1dbbe chore(docs): explaining the ability to export types using the webout() macro 2022-01-09 13:45:28 +00:00
Yury Shkoda
c3bc3c051b chore(git): Merge branch 'issue-607' of https://github.com/sasjs/adapter into issue-607 2022-01-06 16:50:42 +03:00
Yury Shkoda
dbb95b7763 feat: improve convertToCsv function 2022-01-06 16:49:58 +03:00
Allan Bowe
6ca887ba56 chore(docs): fixing missing param 2022-01-06 13:01:31 +00:00
Allan Bowe
70e26c57d9 chore(docs): updating readme with new special missing support. Also adding a .gitpod.yml file for a better gitpod experience. 2022-01-06 12:34:01 +00:00
Yury Shkoda
1a5c84cd0f test(formatDataForRequest): improved test coverage 2022-01-05 18:35:02 +03:00
Yury Shkoda
fbce35b272 feat(nullVars): add SAS null vars support 2022-01-05 16:54:16 +03:00
Yury Shkoda
84ed3e7d03 chore(sasjs-tests): fixed npm dependencies and scripts 2022-01-05 16:51:00 +03:00
Yury Shkoda
0f7f3e0a11 Merge pull request #614 from sasjs/hot-fix-sasjs-server
fix(sasjs): restrict usage of localstorage for node env
2022-01-05 14:51:33 +03:00
Saad Jutt
437bbe114b fix(sasjs): restrict usage of localstorage for node env 2022-01-05 16:39:59 +05:00
Yury Shkoda
9f7870b804 Merge pull request #608 from sasjs/readme_update
chore(readme): support for special missings
2021-12-30 14:32:08 +03:00
munja
9f00cd646e chore(readme): support for special missings 2021-12-30 09:56:08 +00:00
Yury Shkoda
4e125ce38f Merge pull request #605 from sasjs/issue-604
Improve error handling and job/session state polling
2021-12-22 19:11:37 +03:00
Yury Shkoda
4a963ffbf5 feat(polling-state): improve logging 2021-12-22 18:46:06 +03:00
Yury Shkoda
f25d9ec09d test(RequestClient): cover handleError method 2021-12-21 16:41:08 +03:00
Yury Shkoda
4197ad5aa8 test(RequestClient): fix error handling 2021-12-21 11:40:59 +03:00
Yury Shkoda
2ebd6e11ba test(RequestClient): fix error handling 2021-12-21 11:40:27 +03:00
Yury Shkoda
098e7f8590 fix(errors): fixed error handling function 2021-12-20 10:57:53 +03:00
Yury Shkoda
42aec96410 feat(pollJobState): improved loggging 2021-12-20 10:56:52 +03:00
Allan Bowe
f2905ee169 Merge pull request #602 from sasjs/hot-fix-sasjs-server
fix: response from SASJS Server with/without debug on
2021-12-15 17:42:29 +00:00
Saad Jutt
1ba9291746 fix: response from SASJS Server with/without debug on 2021-12-15 21:32:02 +05:00
Muhammad Saad
c44766ea14 Merge pull request #600 from sasjs/sasjs-server-access-refresh-tokens
feat: get access token & refresh tokens for server type SASJS
2021-12-15 12:56:57 +05:00
Saad Jutt
2c10b9c65c chore: typedoc updated 2021-12-13 17:01:59 +05:00
Saad Jutt
c56874fe00 feat: login for web with server type SASJS 2021-12-09 17:13:47 +05:00
Saad Jutt
ebe9c2ffeb feat: get access token & refresh tokens for server type SASJS 2021-12-09 11:43:50 +05:00
130 changed files with 28028 additions and 13230 deletions

View File

@@ -2,11 +2,8 @@ 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,6 +22,8 @@ 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

2
.gitpod.yml Normal file
View File

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

View File

@@ -142,6 +142,71 @@ 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.
@@ -153,16 +218,29 @@ The following snippet shows the process of SAS tables arriving / leaving:
%webout(FETCH) %webout(FETCH)
/* some sas code */ /* some sas code */
data some sas tables; data a b c;
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,some) /* `some` table is sent in object format */ %webout(OBJ,a) /* Rows in table `a` are objects (easy to use) */
%webout(ARR,sas) /* `sas` table is sent in array format, smaller filesize */ %webout(ARR,b) /* Rows in table `b` are arrays (compact) */
%webout(OBJ,tables,fmt=N) /* unformatted (raw) data */ %webout(OBJ,c,fmt=N) /* Table `c` is sent unformatted (raw) */
%webout(OBJ,tables,label=newtable) /* rename tables on export */ %webout(OBJ,c,label=d) /* Rename as `d` on JS side */
%webout(CLOSE) /* close the JSON and send some extra useful variables too */ %webout(CLOSE) /* Close the JSON and add default variables */
```
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
@@ -170,7 +248,7 @@ run;
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` or `SASVIYA`. * `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`. 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.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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'], setupFilesAfterEnv: ['jest-extended/all'],
// 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: [],

6246
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.2.0", "jest": "27.4.7",
"jest-extended": "^0.11.5", "jest-extended": "2.0.0",
"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.2.4", "terser-webpack-plugin": "5.3.0",
"ts-jest": "^27.0.3", "ts-jest": "27.1.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.19.2", "typedoc": "0.22.11",
"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.3.5", "typescript": "4.5.4",
"webpack": "^5.56.0", "webpack": "5.66.0",
"webpack-cli": "^4.7.2" "webpack-cli": "4.7.2"
}, },
"main": "index.js", "main": "index.js",
"dependencies": { "dependencies": {
"@sasjs/utils": "^2.32.0", "@sasjs/utils": "2.35.0",
"axios": "^0.21.4", "axios": "0.26.0",
"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

@@ -47,7 +47,7 @@ npm i -g copyfiles
``` ```
and then run to build: and then run to build:
```bash ```bash
npm run update:adapter && npm run build npm run update:adapter && npm run build
``` ```
when it finishes run to deploy: when it finishes run to deploy:
```bash ```bash
@@ -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) %end; %do i=1 %to &_webin_file_count; %webout(OBJ,&&_webin_name&i,missing=STRING) %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) %end; %do i=1 %to &_webin_file_count; %webout(ARR,&&_webin_name&i,missing=STRING) %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) %webout(OBJ,&table,missing=STRING)
%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) %webout(ARR,&table,missing=STRING)
%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.2", "@sasjs/test-framework": "^1.4.3",
"@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", "update:adapter": "cd .. && npm run package:lib && cd sasjs-tests && npm i ../build/sasjs-adapter-5.0.0.tgz --legacy-peer-deps",
"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": "^5.0.0" "node-sass": "^6.0.1"
} }
} }

View File

@@ -6,6 +6,7 @@ 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,6 +79,19 @@ const errorAndCsrfData: any = {
_csrf: [{ col1: 'q', col2: 'w', col3: 'e', col4: 'r' }] _csrf: [{ col1: 'q', col2: 'w', col3: 'e', col4: 'r' }]
} }
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.' } }
}
export const specialCaseTests = (adapter: SASjs): TestSuite => ({ export const specialCaseTests = (adapter: SASjs): TestSuite => ({
name: 'Special Cases', name: 'Special Cases',
tests: [ tests: [
@@ -247,6 +260,39 @@ 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

@@ -22,8 +22,8 @@ import { pollJobState } from './api/viya/pollJobState'
import { getTokens } from './auth/getTokens' import { getTokens } from './auth/getTokens'
import { uploadTables } from './api/viya/uploadTables' import { uploadTables } from './api/viya/uploadTables'
import { executeScript } from './api/viya/executeScript' import { executeScript } from './api/viya/executeScript'
import { getAccessToken } from './auth/getAccessToken' import { getAccessTokenForViya } from './auth/getAccessTokenForViya'
import { refreshTokens } from './auth/refreshTokens' import { refreshTokensForViya } from './auth/refreshTokensForViya'
/** /**
* A client for interfacing with the SAS Viya REST API. * A client for interfacing with the SAS Viya REST API.
@@ -534,21 +534,26 @@ export class SASViyaApiClient {
clientSecret: string, clientSecret: string,
authCode: string authCode: string
): Promise<SasAuthResponse> { ): Promise<SasAuthResponse> {
return getAccessToken(this.requestClient, clientId, clientSecret, authCode) return getAccessTokenForViya(
this.requestClient,
clientId,
clientSecret,
authCode
)
} }
/** /**
* Exchanges the refresh token for an access token for the given client. * Exchanges the refresh token for an access token for the given client.
* @param clientId - the client ID to authenticate with. * @param clientId - the client ID to authenticate with.
* @param clientSecret - the client secret to authenticate with. * @param clientSecret - the client secret to authenticate with.
* @param authCode - the refresh token received from the server. * @param refreshToken - the refresh token received from the server.
*/ */
public async refreshTokens( public async refreshTokens(
clientId: string, clientId: string,
clientSecret: string, clientSecret: string,
refreshToken: string refreshToken: string
) { ) {
return refreshTokens( return refreshTokensForViya(
this.requestClient, this.requestClient,
clientId, clientId,
clientSecret, clientSecret,

View File

@@ -5,27 +5,29 @@ import {
EditContextInput, EditContextInput,
PollOptions, PollOptions,
LoginMechanism, LoginMechanism,
FolderMember, ExecutionQuery,
ServiceMember, FileTree
ExecutionQuery
} from './types' } from './types'
import { SASViyaApiClient } from './SASViyaApiClient' import { SASViyaApiClient } from './SASViyaApiClient'
import { SAS9ApiClient } from './SAS9ApiClient' import { SAS9ApiClient } from './SAS9ApiClient'
import { SASjsApiClient } from './SASjsApiClient' import { SASjsApiClient, SASjsAuthResponse } from './SASjsApiClient'
import { AuthManager } from './auth' import { AuthManager } from './auth'
import { import {
ServerType, ServerType,
MacroVar, MacroVar,
AuthConfig, AuthConfig,
ExtraResponseAttributes ExtraResponseAttributes,
SasAuthResponse
} from '@sasjs/utils/types' } from '@sasjs/utils/types'
import { RequestClient } from './request/RequestClient' import { RequestClient } from './request/RequestClient'
import { SasjsRequestClient } from './request/SasjsRequestClient'
import { import {
JobExecutor, JobExecutor,
WebJobExecutor, WebJobExecutor,
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'
@@ -53,7 +55,7 @@ export default class SASjs {
private jobsPath: string = '' private jobsPath: string = ''
private sasViyaApiClient: SASViyaApiClient | null = null private sasViyaApiClient: SASViyaApiClient | null = null
private sas9ApiClient: SAS9ApiClient | null = null private sas9ApiClient: SAS9ApiClient | null = null
private SASjsApiClient: SASjsApiClient | null = null private sasJSApiClient: SASjsApiClient | null = null
private fileUploader: FileUploader | null = null private fileUploader: FileUploader | null = null
private authManager: AuthManager | null = null private authManager: AuthManager | null = null
private requestClient: RequestClient | null = null private requestClient: RequestClient | null = null
@@ -61,6 +63,7 @@ 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 = {
@@ -75,12 +78,18 @@ 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,
password: string password: string
) { ) {
this.isMethodSupported('executeScriptSAS9', ServerType.Sas9) this.isMethodSupported('executeScriptSAS9', [ServerType.Sas9])
return await this.sas9ApiClient?.executeScript( return await this.sas9ApiClient?.executeScript(
linesOfCode, linesOfCode,
@@ -89,12 +98,44 @@ 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.
*/ */
public async getComputeContexts(accessToken: string) { public async getComputeContexts(accessToken: string) {
this.isMethodSupported('getComputeContexts', ServerType.SasViya) this.isMethodSupported('getComputeContexts', [ServerType.SasViya])
return await this.sasViyaApiClient!.getComputeContexts(accessToken) return await this.sasViyaApiClient!.getComputeContexts(accessToken)
} }
@@ -104,7 +145,7 @@ export default class SASjs {
* @param accessToken - an access token for an authorized user. * @param accessToken - an access token for an authorized user.
*/ */
public async getLauncherContexts(accessToken: string) { public async getLauncherContexts(accessToken: string) {
this.isMethodSupported('getLauncherContexts', ServerType.SasViya) this.isMethodSupported('getLauncherContexts', [ServerType.SasViya])
return await this.sasViyaApiClient!.getLauncherContexts(accessToken) return await this.sasViyaApiClient!.getLauncherContexts(accessToken)
} }
@@ -113,7 +154,7 @@ export default class SASjs {
* Gets default(system) launcher contexts. * Gets default(system) launcher contexts.
*/ */
public getDefaultComputeContexts() { public getDefaultComputeContexts() {
this.isMethodSupported('getDefaultComputeContexts', ServerType.SasViya) this.isMethodSupported('getDefaultComputeContexts', [ServerType.SasViya])
return this.sasViyaApiClient!.getDefaultComputeContexts() return this.sasViyaApiClient!.getDefaultComputeContexts()
} }
@@ -123,7 +164,7 @@ export default class SASjs {
* @param authConfig - an access token, refresh token, client and secret for an authorized user. * @param authConfig - an access token, refresh token, client and secret for an authorized user.
*/ */
public async getExecutableContexts(authConfig: AuthConfig) { public async getExecutableContexts(authConfig: AuthConfig) {
this.isMethodSupported('getExecutableContexts', ServerType.SasViya) this.isMethodSupported('getExecutableContexts', [ServerType.SasViya])
return await this.sasViyaApiClient!.getExecutableContexts(authConfig) return await this.sasViyaApiClient!.getExecutableContexts(authConfig)
} }
@@ -145,7 +186,7 @@ export default class SASjs {
accessToken: string, accessToken: string,
authorizedUsers?: string[] authorizedUsers?: string[]
) { ) {
this.isMethodSupported('createComputeContext', ServerType.SasViya) this.isMethodSupported('createComputeContext', [ServerType.SasViya])
return await this.sasViyaApiClient!.createComputeContext( return await this.sasViyaApiClient!.createComputeContext(
contextName, contextName,
@@ -170,7 +211,7 @@ export default class SASjs {
launchType: string, launchType: string,
accessToken: string accessToken: string
) { ) {
this.isMethodSupported('createLauncherContext', ServerType.SasViya) this.isMethodSupported('createLauncherContext', [ServerType.SasViya])
return await this.sasViyaApiClient!.createLauncherContext( return await this.sasViyaApiClient!.createLauncherContext(
contextName, contextName,
@@ -191,7 +232,7 @@ export default class SASjs {
editedContext: EditContextInput, editedContext: EditContextInput,
accessToken?: string accessToken?: string
) { ) {
this.isMethodSupported('editComputeContext', ServerType.SasViya) this.isMethodSupported('editComputeContext', [ServerType.SasViya])
return await this.sasViyaApiClient!.editComputeContext( return await this.sasViyaApiClient!.editComputeContext(
contextName, contextName,
@@ -206,7 +247,7 @@ export default class SASjs {
* @param accessToken - an access token for an authorized user. * @param accessToken - an access token for an authorized user.
*/ */
public async deleteComputeContext(contextName: string, accessToken?: string) { public async deleteComputeContext(contextName: string, accessToken?: string) {
this.isMethodSupported('deleteComputeContext', ServerType.SasViya) this.isMethodSupported('deleteComputeContext', [ServerType.SasViya])
return await this.sasViyaApiClient!.deleteComputeContext( return await this.sasViyaApiClient!.deleteComputeContext(
contextName, contextName,
@@ -224,7 +265,7 @@ export default class SASjs {
contextName: string, contextName: string,
accessToken?: string accessToken?: string
) { ) {
this.isMethodSupported('getComputeContextByName', ServerType.SasViya) this.isMethodSupported('getComputeContextByName', [ServerType.SasViya])
return await this.sasViyaApiClient!.getComputeContextByName( return await this.sasViyaApiClient!.getComputeContextByName(
contextName, contextName,
@@ -238,7 +279,7 @@ export default class SASjs {
* @param accessToken - an access token for an authorized user. * @param accessToken - an access token for an authorized user.
*/ */
public async getComputeContextById(contextId: string, accessToken?: string) { public async getComputeContextById(contextId: string, accessToken?: string) {
this.isMethodSupported('getComputeContextById', ServerType.SasViya) this.isMethodSupported('getComputeContextById', [ServerType.SasViya])
return await this.sasViyaApiClient!.getComputeContextById( return await this.sasViyaApiClient!.getComputeContextById(
contextId, contextId,
@@ -247,43 +288,11 @@ export default class SASjs {
} }
public async createSession(contextName: string, accessToken: string) { public async createSession(contextName: string, accessToken: string) {
this.isMethodSupported('createSession', ServerType.SasViya) this.isMethodSupported('createSession', [ServerType.SasViya])
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.
@@ -357,7 +366,7 @@ export default class SASjs {
* @param accessToken - the access token to authorize the request. * @param accessToken - the access token to authorize the request.
*/ */
public async getFolder(folderPath: string, accessToken?: string) { public async getFolder(folderPath: string, accessToken?: string) {
this.isMethodSupported('getFolder', ServerType.SasViya) this.isMethodSupported('getFolder', [ServerType.SasViya])
return await this.sasViyaApiClient!.getFolder(folderPath, accessToken) return await this.sasViyaApiClient!.getFolder(folderPath, accessToken)
} }
@@ -367,7 +376,7 @@ export default class SASjs {
* @param accessToken - an access token for authorizing the request. * @param accessToken - an access token for authorizing the request.
*/ */
public async deleteFolder(folderPath: string, accessToken: string) { public async deleteFolder(folderPath: string, accessToken: string) {
this.isMethodSupported('deleteFolder', ServerType.SasViya) this.isMethodSupported('deleteFolder', [ServerType.SasViya])
return await this.sasViyaApiClient?.deleteFolder(folderPath, accessToken) return await this.sasViyaApiClient?.deleteFolder(folderPath, accessToken)
} }
@@ -382,7 +391,7 @@ export default class SASjs {
accessToken?: string, accessToken?: string,
limit?: number limit?: number
) { ) {
this.isMethodSupported('listFolder', ServerType.SasViya) this.isMethodSupported('listFolder', [ServerType.SasViya])
return await this.sasViyaApiClient?.listFolder( return await this.sasViyaApiClient?.listFolder(
sourceFolder, sourceFolder,
@@ -404,7 +413,7 @@ export default class SASjs {
targetFolderName: string, targetFolderName: string,
accessToken: string accessToken: string
) { ) {
this.isMethodSupported('moveFolder', ServerType.SasViya) this.isMethodSupported('moveFolder', [ServerType.SasViya])
return await this.sasViyaApiClient?.moveFolder( return await this.sasViyaApiClient?.moveFolder(
sourceFolder, sourceFolder,
@@ -422,7 +431,7 @@ export default class SASjs {
accessToken?: string, accessToken?: string,
sasApiClient?: SASViyaApiClient sasApiClient?: SASViyaApiClient
) { ) {
this.isMethodSupported('createJobDefinition', ServerType.SasViya) this.isMethodSupported('createJobDefinition', [ServerType.SasViya])
if (sasApiClient) if (sasApiClient)
return await sasApiClient!.createJobDefinition( return await sasApiClient!.createJobDefinition(
@@ -442,7 +451,7 @@ export default class SASjs {
} }
public async getAuthCode(clientId: string) { public async getAuthCode(clientId: string) {
this.isMethodSupported('getAuthCode', ServerType.SasViya) this.isMethodSupported('getAuthCode', [ServerType.SasViya])
return await this.sasViyaApiClient!.getAuthCode(clientId) return await this.sasViyaApiClient!.getAuthCode(clientId)
} }
@@ -457,8 +466,14 @@ export default class SASjs {
clientId: string, clientId: string,
clientSecret: string, clientSecret: string,
authCode: string authCode: string
) { ): Promise<SasAuthResponse | SASjsAuthResponse> {
this.isMethodSupported('getAccessToken', ServerType.SasViya) this.isMethodSupported('getAccessToken', [
ServerType.SasViya,
ServerType.Sasjs
])
if (this.sasjsConfig.serverType === ServerType.Sasjs)
return await this.sasJSApiClient!.getAccessToken(clientId, authCode)
return await this.sasViyaApiClient!.getAccessToken( return await this.sasViyaApiClient!.getAccessToken(
clientId, clientId,
@@ -467,12 +482,24 @@ export default class SASjs {
) )
} }
/**
* Exchanges the refresh token for an access token for the given client.
* @param clientId - the client ID to authenticate with.
* @param clientSecret - the client secret to authenticate with.
* @param refreshToken - the refresh token received from the server.
*/
public async refreshTokens( public async refreshTokens(
clientId: string, clientId: string,
clientSecret: string, clientSecret: string,
refreshToken: string refreshToken: string
) { ): Promise<SasAuthResponse | SASjsAuthResponse> {
this.isMethodSupported('refreshTokens', ServerType.SasViya) this.isMethodSupported('refreshTokens', [
ServerType.SasViya,
ServerType.Sasjs
])
if (this.sasjsConfig.serverType === ServerType.Sasjs)
return await this.sasJSApiClient!.refreshTokens(refreshToken)
return await this.sasViyaApiClient!.refreshTokens( return await this.sasViyaApiClient!.refreshTokens(
clientId, clientId,
@@ -482,7 +509,7 @@ export default class SASjs {
} }
public async deleteClient(clientId: string, accessToken: string) { public async deleteClient(clientId: string, accessToken: string) {
this.isMethodSupported('deleteClient', ServerType.SasViya) this.isMethodSupported('deleteClient', [ServerType.SasViya])
return await this.sasViyaApiClient!.deleteClient(clientId, accessToken) return await this.sasViyaApiClient!.deleteClient(clientId, accessToken)
} }
@@ -528,29 +555,39 @@ export default class SASjs {
/** /**
* Checks whether a session is active, or login is required. * Checks whether a session is active, or login is required.
* @param accessToken - an optional access token is required for SASjs server type.
* @returns - a promise which resolves with an object containing two values - a boolean `isLoggedIn`, and a string `userName`. * @returns - a promise which resolves with an object containing two values - a boolean `isLoggedIn`, and a string `userName`.
*/ */
public async checkSession(accessToken?: string) { public async checkSession() {
return this.authManager!.checkSession(accessToken) return this.authManager!.checkSession()
} }
/** /**
* Logs into the SAS server with the supplied credentials. * Logs into the SAS server with the supplied credentials.
* @param username - a string representing the username. * @param username - a string representing the username.
* @param password - a string representing the password. * @param password - a string representing the password.
* @param clientId - a string representing the client ID.
*/ */
public async logIn( public async logIn(
username?: string, username?: string,
password?: string, password?: string,
clientId?: string,
options: LoginOptions = {} options: LoginOptions = {}
): Promise<LoginResult> { ): Promise<LoginResult> {
if (this.sasjsConfig.loginMechanism === LoginMechanism.Default) { if (this.sasjsConfig.loginMechanism === LoginMechanism.Default) {
if (!username || !password) { if (!username || !password)
throw new Error( throw new Error(
'A username and password are required when using the default login mechanism.' 'A username and password are required when using the default login mechanism.'
) )
if (this.sasjsConfig.serverType === ServerType.Sasjs) {
if (!clientId)
throw new Error(
'A username, password and clientId are required when using the default login mechanism with server type SASJS.'
)
return this.authManager!.logInSasjs(username, password, clientId)
} }
return this.authManager!.logIn(username, password) return this.authManager!.logIn(username, password)
} }
@@ -565,10 +602,9 @@ export default class SASjs {
/** /**
* Logs out of the configured SAS server. * Logs out of the configured SAS server.
* @param accessToken - an optional access token is required for SASjs server type.
*/ */
public logOut(accessToken?: string) { public logOut() {
return this.authManager!.logOut(accessToken) return this.authManager!.logOut()
} }
/** /**
@@ -647,7 +683,16 @@ export default class SASjs {
const validationResult = this.validateInput(data) const validationResult = this.validateInput(data)
if (validationResult.status) { if (validationResult.status) {
if ( if (config.serverType === ServerType.Sasjs) {
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
@@ -707,15 +752,19 @@ 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_]/)) { if (!key.match(/^[a-zA-Z_]/) && !isSasFormatsTable(key)) {
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_]*$/)) { if (!key.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/) && !isSasFormatsTable(key)) {
return { status: false, msg: 'Table name should be alphanumeric.' } return { status: false, msg: 'Table name should be alphanumeric.' }
} }
@@ -726,7 +775,7 @@ export default class SASjs {
} }
} }
if (this.getType(data[key]) !== 'Array') { if (this.getType(data[key]) !== 'Array' && !isSasFormatsTable(key)) {
return { return {
status: false, status: false,
msg: 'Parameter data contains invalid table structure.' msg: 'Parameter data contains invalid table structure.'
@@ -742,6 +791,7 @@ export default class SASjs {
} }
} }
} }
return { status: true, msg: '' } return { status: true, msg: '' }
} }
@@ -777,7 +827,7 @@ export default class SASjs {
accessToken?: string, accessToken?: string,
isForced = false isForced = false
) { ) {
this.isMethodSupported('deployServicePack', ServerType.SasViya) this.isMethodSupported('deployServicePack', [ServerType.SasViya])
let sasApiClient: any = null let sasApiClient: any = null
if (serverUrl || appLoc) { if (serverUrl || appLoc) {
@@ -831,12 +881,26 @@ export default class SASjs {
) )
} }
public async deployToSASjs(members: [FolderMember, ServiceMember]) { /**
return await this.SASjsApiClient?.deploy(members, this.sasjsConfig.appLoc) * Creates the folders and services at the given location `appLoc` on the given server `serverUrl`.
* @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) {
return await this.SASjsApiClient?.executeJob(query) return await this.sasJSApiClient?.executeJob(query)
} }
/** /**
@@ -872,7 +936,7 @@ export default class SASjs {
...config ...config
} }
this.isMethodSupported('startComputeJob', ServerType.SasViya) this.isMethodSupported('startComputeJob', [ServerType.SasViya])
if (!config.contextName) { if (!config.contextName) {
throw new Error( throw new Error(
'Context name is undefined. Please set a `contextName` in your SASjs or override config.' 'Context name is undefined. Please set a `contextName` in your SASjs or override config.'
@@ -964,7 +1028,11 @@ export default class SASjs {
} }
if (!this.requestClient) { if (!this.requestClient) {
this.requestClient = new RequestClient( const RequestClientClass =
this.sasjsConfig.serverType === ServerType.Sasjs
? SasjsRequestClient
: RequestClient
this.requestClient = new RequestClientClass(
this.sasjsConfig.serverUrl, this.sasjsConfig.serverUrl,
this.sasjsConfig.httpsAgentOptions this.sasjsConfig.httpsAgentOptions
) )
@@ -980,7 +1048,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,
@@ -1020,10 +1088,10 @@ export default class SASjs {
} }
if (this.sasjsConfig.serverType === ServerType.Sasjs) { if (this.sasjsConfig.serverType === ServerType.Sasjs) {
if (this.SASjsApiClient) { if (this.sasJSApiClient) {
this.SASjsApiClient.setConfig(this.sasjsConfig.serverUrl) this.sasJSApiClient.setConfig(this.sasjsConfig.serverUrl)
} else { } else {
this.SASjsApiClient = new SASjsApiClient( this.sasJSApiClient = new SASjsApiClient(
this.sasjsConfig.serverUrl, this.sasjsConfig.serverUrl,
this.requestClient this.requestClient
) )
@@ -1045,6 +1113,13 @@ 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!,
@@ -1117,12 +1192,15 @@ export default class SASjs {
}) })
} }
private isMethodSupported(method: string, serverType: string) { private isMethodSupported(method: string, serverTypes: ServerType[]) {
if (this.sasjsConfig.serverType !== serverType) { if (
!this.sasjsConfig.serverType ||
!serverTypes.includes(this.sasjsConfig.serverType)
) {
throw new Error( throw new Error(
`Method '${method}' is only supported on ${ `Method '${method}' is only supported on ${serverTypes.join(
serverType === ServerType.Sas9 ? 'SAS9' : 'SAS Viya' ', '
} servers.` )} servers.`
) )
} }
} }

View File

@@ -1,5 +1,11 @@
import { FolderMember, ServiceMember, ExecutionQuery } from './types' import { AuthConfig, ServerType } from '@sasjs/utils/types'
import { FileTree, ExecutionQuery } from './types'
import { RequestClient } from './request/RequestClient' import { RequestClient } from './request/RequestClient'
import { getAccessTokenForSasjs } from './auth/getAccessTokenForSasjs'
import { refreshTokensForSasjs } from './auth/refreshTokensForSasjs'
import { getAuthCodeForSasjs } from './auth/getAuthCodeForSasjs'
import { parseWeboutResponse } from './utils'
import { getTokens } from './auth/getTokens'
export class SASjsApiClient { export class SASjsApiClient {
constructor( constructor(
@@ -11,7 +17,19 @@ export class SASjsApiClient {
if (serverUrl) this.serverUrl = serverUrl if (serverUrl) this.serverUrl = serverUrl
} }
public async deploy(members: [FolderMember, ServiceMember], appLoc: string) { public async deploy(
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
@@ -19,7 +37,7 @@ export class SASjsApiClient {
}>( }>(
'SASjsApi/drive/deploy', 'SASjsApi/drive/deploy',
{ fileTree: members, appLoc: appLoc }, { fileTree: members, appLoc: appLoc },
undefined access_token
) )
return Promise.resolve(result) return Promise.resolve(result)
@@ -32,8 +50,53 @@ 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)
} }
/**
* Exchanges the auth code for an access token for the given client.
* @param clientId - the client ID to authenticate with.
* @param authCode - the auth code received from the server.
*/
public async getAccessToken(
clientId: string,
authCode: string
): Promise<SASjsAuthResponse> {
return getAccessTokenForSasjs(this.requestClient, clientId, authCode)
}
/**
* Exchanges the refresh token for an access token.
* @param refreshToken - the refresh token received from the server.
*/
public async refreshTokens(refreshToken: string): Promise<SASjsAuthResponse> {
return refreshTokensForSasjs(this.requestClient, refreshToken)
}
/**
* Performs a login authenticate and returns an auth code for the given client.
* @param username - a string representing the username.
* @param password - a string representing the password.
* @param clientId - the client ID to authenticate with.
*/
public async getAuthCode(
username: string,
password: string,
clientId: string
) {
return getAuthCodeForSasjs(this.requestClient, username, password, clientId)
}
}
// todo move to sasjs/utils
export interface SASjsAuthResponse {
access_token: string
refresh_token: string
} }

View File

@@ -168,7 +168,7 @@ export class SessionManager {
) { ) {
if (stateLink) { if (stateLink) {
if (this.debug && !this.printedSessionState.printed) { if (this.debug && !this.printedSessionState.printed) {
logger.info('Polling session status...') logger.info(`Polling: ${this.serverUrl + stateLink.href}`)
this.printedSessionState.printed = true this.printedSessionState.printed = true
} }

View File

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

View File

@@ -206,10 +206,11 @@ const doPoll = async (
pollCount++ pollCount++
const jobHref = postedJob.links.find((l: Link) => l.rel === 'self')!.href
if (pollOptions?.streamLog) { if (pollOptions?.streamLog) {
const jobUrl = postedJob.links.find((l: Link) => l.rel === 'self')
const { result: job } = await requestClient.get<Job>( const { result: job } = await requestClient.get<Job>(
jobUrl!.href, jobHref,
authConfig?.access_token authConfig?.access_token
) )
@@ -231,7 +232,7 @@ const doPoll = async (
} }
if (debug && printedState !== state) { if (debug && printedState !== state) {
logger.info('Polling job status...') logger.info(`Polling: ${requestClient.getBaseUrl() + jobHref}/state`)
logger.info(`Current job state: ${state}`) logger.info(`Current job state: ${state}`)
printedState = state printedState = state

View File

@@ -9,7 +9,10 @@ import * as isNodeModule from '../../../utils/isNode'
import { PollOptions } from '../../../types' import { PollOptions } from '../../../types'
import { WriteStream } from 'fs' import { WriteStream } from 'fs'
const baseUrl = 'http://localhost'
const requestClient = new (<jest.Mock<RequestClient>>RequestClient)() const requestClient = new (<jest.Mock<RequestClient>>RequestClient)()
requestClient['httpClient'].defaults.baseURL = baseUrl
const defaultPollOptions: PollOptions = { const defaultPollOptions: PollOptions = {
maxPollCount: 100, maxPollCount: 100,
pollInterval: 500, pollInterval: 500,
@@ -195,7 +198,7 @@ describe('pollJobState', () => {
expect((process as any).logger.info).toHaveBeenCalledTimes(4) expect((process as any).logger.info).toHaveBeenCalledTimes(4)
expect((process as any).logger.info).toHaveBeenNthCalledWith( expect((process as any).logger.info).toHaveBeenNthCalledWith(
1, 1,
'Polling job status...' `Polling: ${baseUrl}/job/state`
) )
expect((process as any).logger.info).toHaveBeenNthCalledWith( expect((process as any).logger.info).toHaveBeenNthCalledWith(
2, 2,
@@ -203,7 +206,7 @@ describe('pollJobState', () => {
) )
expect((process as any).logger.info).toHaveBeenNthCalledWith( expect((process as any).logger.info).toHaveBeenNthCalledWith(
3, 3,
'Polling job status...' `Polling: ${baseUrl}/job/state`
) )
expect((process as any).logger.info).toHaveBeenNthCalledWith( expect((process as any).logger.info).toHaveBeenNthCalledWith(
4, 4,

View File

@@ -1,24 +1,34 @@
import { WriteStream } from '../../../types' import { WriteStream } from '../../../types'
import { writeStream } from '../writeStream' import { writeStream } from '../writeStream'
import 'jest-extended' import {
createWriteStream,
fileExists,
readFile,
deleteFile
} from '@sasjs/utils'
describe('writeStream', () => { describe('writeStream', () => {
const stream: WriteStream = { const filename = 'test.txt'
write: jest.fn(), const content = 'test'
path: 'test' let stream: WriteStream
}
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 () => {
expect(writeStream(stream, 'test')).toResolve() await expect(writeStream(stream, content)).toResolve()
await expect(fileExists(filename)).resolves.toEqual(true)
await expect(readFile(filename)).resolves.toEqual(content + '\n')
expect(stream.write).toHaveBeenCalledWith('test\n', expect.anything()) await deleteFile(filename)
}) })
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, 'test').catch((e) => e) const error = await writeStream(stream, content).catch((e) => e)
expect(error.message).toEqual('Test Error') expect(error.message).toEqual('Test Error')
}) })

View File

@@ -3,13 +3,9 @@ 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

@@ -2,6 +2,8 @@ import { ServerType } from '@sasjs/utils/types'
import { RequestClient } from '../request/RequestClient' import { RequestClient } from '../request/RequestClient'
import { LoginOptions, LoginResult } from '../types/Login' import { LoginOptions, LoginResult } from '../types/Login'
import { serialize } from '../utils' import { serialize } from '../utils'
import { getAccessTokenForSasjs } from './getAccessTokenForSasjs'
import { getAuthCodeForSasjs } from './getAuthCodeForSasjs'
import { openWebPage } from './openWebPage' import { openWebPage } from './openWebPage'
import { verifySas9Login } from './verifySas9Login' import { verifySas9Login } from './verifySas9Login'
import { verifySasViyaLogin } from './verifySasViyaLogin' import { verifySasViyaLogin } from './verifySasViyaLogin'
@@ -81,6 +83,39 @@ export class AuthManager {
return { isLoggedIn: false, userName: '' } return { isLoggedIn: false, userName: '' }
} }
/**
* Logs into the SAS server with the supplied credentials.
* @param userName - a string representing the username.
* @param password - a string representing the password.
* @param clientId - a string representing the client ID.
* @returns - a boolean `isLoggedin` and a string `username`
*/
public async logInSasjs(
username: string,
password: string,
clientId: string
): Promise<LoginResult> {
const isLoggedIn = await this.sendLoginRequestSasjs(
username,
password,
clientId
)
.then((res) => {
this.userName = username
this.requestClient.saveLocalStorageToken(
res.access_token,
res.refresh_token
)
return true
})
.catch(() => false)
return {
isLoggedIn,
userName: this.userName
}
}
/** /**
* Logs into the SAS server with the supplied credentials. * Logs into the SAS server with the supplied credentials.
* @param username - a string representing the username. * @param username - a string representing the username.
@@ -180,28 +215,41 @@ export class AuthManager {
return loginResponse return loginResponse
} }
private async sendLoginRequestSasjs(
username: string,
password: string,
clientId: string
) {
const authCode = await getAuthCodeForSasjs(
this.requestClient,
username,
password,
clientId
)
return getAccessTokenForSasjs(this.requestClient, clientId, authCode)
}
/** /**
* Checks whether a session is active, or login is required. * Checks whether a session is active, or login is required.
* @param accessToken - an optional access token is required for SASjs server type.
* @returns - a promise which resolves with an object containing three values * @returns - a promise which resolves with an object containing three values
* - a boolean `isLoggedIn` * - a boolean `isLoggedIn`
* - a string `userName` and * - a string `userName` and
* - a form `loginForm` if not loggedin. * - a form `loginForm` if not loggedin.
*/ */
public async checkSession(accessToken?: string): Promise<{ public async checkSession(): Promise<{
isLoggedIn: boolean isLoggedIn: boolean
userName: string userName: string
loginForm?: any loginForm?: any
}> { }> {
const { isLoggedIn, userName } = await this.fetchUserName(accessToken) const { isLoggedIn, userName } = await this.fetchUserName()
let loginForm = null let loginForm = null
if (!isLoggedIn && this.serverType !== ServerType.Sasjs) { if (!isLoggedIn) {
//We will logout to make sure cookies are removed and login form is presented //We will logout to make sure cookies are removed and login form is presented
//Residue can happen in case of session expiration //Residue can happen in case of session expiration
await this.logOut() await this.logOut()
loginForm = await this.getNewLoginForm() if (this.serverType !== ServerType.Sasjs)
loginForm = await this.getNewLoginForm()
} }
return Promise.resolve({ return Promise.resolve({
@@ -221,7 +269,7 @@ export class AuthManager {
return await this.getLoginForm(formResponse) return await this.getLoginForm(formResponse)
} }
private async fetchUserName(accessToken?: string): Promise<{ private async fetchUserName(): Promise<{
isLoggedIn: boolean isLoggedIn: boolean
userName: string userName: string
}> { }> {
@@ -232,9 +280,8 @@ export class AuthManager {
? `${this.serverUrl}/SASStoredProcess` ? `${this.serverUrl}/SASStoredProcess`
: `${this.serverUrl}/SASjsApi/session` : `${this.serverUrl}/SASjsApi/session`
// Access token is required for server type `SASjs`
const { result: loginResponse } = await this.requestClient const { result: loginResponse } = await this.requestClient
.get<string>(url, accessToken, 'text/plain') .get<string>(url, undefined, 'text/plain')
.catch((err: any) => { .catch((err: any) => {
return { result: 'authErr' } return { result: 'authErr' }
}) })
@@ -315,11 +362,19 @@ export class AuthManager {
* Logs out of the configured SAS server. * Logs out of the configured SAS server.
* @param accessToken - an optional access token is required for SASjs server type. * @param accessToken - an optional access token is required for SASjs server type.
*/ */
public logOut(accessToken?: string) { public async logOut() {
if (this.serverType === ServerType.Sasjs) { if (this.serverType === ServerType.Sasjs) {
return this.requestClient.post(this.logoutUrl, undefined, accessToken) return this.requestClient
.delete(this.logoutUrl)
.catch(() => true)
.finally(() => {
this.requestClient.clearLocalStorageTokens()
return true
})
} }
this.requestClient.clearCsrfTokens() this.requestClient.clearCsrfTokens()
return this.requestClient.get(this.logoutUrl, undefined).then(() => true) return this.requestClient.get(this.logoutUrl, undefined).then(() => true)
} }
} }

Some files were not shown because too many files have changed in this diff Show More