mirror of
https://github.com/sasjs/adapter.git
synced 2025-12-11 09:24:35 +00:00
Compare commits
110 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78945d9f45 | ||
|
|
1e0365de1c | ||
|
|
c8d71b0267 | ||
|
|
4026b03005 | ||
|
|
eab19a0e6e | ||
|
|
1d7b7d654d | ||
| 2c4152a593 | |||
|
|
de51946850 | ||
|
|
ebd55c5b02 | ||
|
|
f48089cb8c | ||
|
|
30a99f9cc5 | ||
|
|
b1979f63ef | ||
|
|
97c3cfd574 | ||
|
|
56df578ab2 | ||
|
|
556ab608c5 | ||
|
|
0633a6de84 | ||
|
|
a39f9bb7e8 | ||
|
|
a0172337ea | ||
|
|
56683bcee2 | ||
|
|
8d99612695 | ||
|
|
f232fe158c | ||
|
|
11b218702b | ||
|
|
a957e1d359 | ||
|
|
c6b25d2d49 | ||
|
|
ca5eb1dbbe | ||
|
|
c3bc3c051b | ||
|
|
dbb95b7763 | ||
|
|
6ca887ba56 | ||
|
|
70e26c57d9 | ||
|
|
1a5c84cd0f | ||
|
|
fbce35b272 | ||
|
|
84ed3e7d03 | ||
|
|
0f7f3e0a11 | ||
|
|
437bbe114b | ||
|
|
9f7870b804 | ||
|
|
9f00cd646e | ||
|
|
4e125ce38f | ||
|
|
4a963ffbf5 | ||
|
|
f25d9ec09d | ||
|
|
4197ad5aa8 | ||
|
|
2ebd6e11ba | ||
|
|
098e7f8590 | ||
|
|
42aec96410 | ||
|
|
f2905ee169 | ||
|
|
1ba9291746 | ||
|
|
c44766ea14 | ||
|
|
2c10b9c65c | ||
|
|
c56874fe00 | ||
|
|
ebe9c2ffeb | ||
|
|
b645d1495b | ||
| 3a4a4c3460 | |||
|
|
182de51f9b | ||
|
|
712d1549c7 | ||
|
|
5a478c8936 | ||
| c9ecc1dde4 | |||
| bdf9e2fd5b | |||
|
|
0b795b26c0 | ||
| 96aac0cfa2 | |||
|
|
a82e1f33e3 | ||
|
|
058c887cd3 | ||
|
|
81d959c7c1 | ||
| 7c5adeabb5 | |||
|
|
cb88376bda | ||
|
|
4c8ddeca25 | ||
|
|
d264a3f239 | ||
|
|
840b1aa1bf | ||
|
|
e26fd307c8 | ||
|
|
62deaf9f03 | ||
|
|
865bf71f7d | ||
|
|
734c5bccaa | ||
|
|
bc82cb5f5e | ||
|
|
f25c76fdfd | ||
|
|
e649b41e9e | ||
|
|
a962979765 | ||
|
|
01d76fa66f | ||
|
|
49cfde9f7d | ||
|
|
ce04ffea05 | ||
| 9dc0499f66 | |||
|
|
bc1a7dc54f | ||
|
|
93c267fd4e | ||
|
|
0457eb6663 | ||
|
|
519494718b | ||
|
|
de5c38f0fb | ||
|
|
208470e7d9 | ||
|
|
0321f77451 | ||
|
|
6c5fdc01eb | ||
|
|
2aa0cd8d7a | ||
|
|
397bc4524f | ||
|
|
8dce9f3e48 | ||
| 8e9f1df1ce | |||
|
|
ff4915f7f3 | ||
|
|
6ff8eece7b | ||
|
|
2849e6ed07 | ||
|
|
90b11fe3fa | ||
|
|
147609842d | ||
|
|
dd6f9cd617 | ||
|
|
7f590c35da | ||
|
|
a38de108e3 | ||
|
|
e975e7de97 | ||
|
|
d418a7e971 | ||
|
|
a5b5052a5f | ||
|
|
7638595523 | ||
|
|
70d64f6eec | ||
|
|
f0ecfa57e5 | ||
|
|
5f3416ecd7 | ||
|
|
d8b1a72da2 | ||
|
|
7e64819eb2 | ||
|
|
2f1d403af4 | ||
|
|
075d410f7d | ||
| 085a3f84e9 |
3
.github/reviewer-lottery.yml
vendored
3
.github/reviewer-lottery.yml
vendored
@@ -2,11 +2,8 @@ groups:
|
||||
- name: SASjs Devs # name of the group
|
||||
reviewers: 1 # how many reviewers do you want to assign?
|
||||
usernames: # github usernames of the reviewers
|
||||
- krishna-acondy
|
||||
- YuryShkoda
|
||||
- saadjutt01
|
||||
- medjedovicm
|
||||
- allanbowe
|
||||
- sabhas
|
||||
- name: SASjs QA
|
||||
reviewers: 1
|
||||
|
||||
7
.github/workflows/build.yml
vendored
7
.github/workflows/build.yml
vendored
@@ -13,14 +13,17 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [15.x]
|
||||
node-version: [lts/fermium]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: npm
|
||||
- name: Check npm audit
|
||||
run: npm audit --production --audit-level=low
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
- name: Check code style
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -5,4 +5,4 @@ build
|
||||
|
||||
/coverage
|
||||
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
|
||||
2
.gitpod.yml
Normal file
2
.gitpod.yml
Normal file
@@ -0,0 +1,2 @@
|
||||
tasks:
|
||||
- init: npm install && npm run build
|
||||
@@ -3,3 +3,4 @@ docs/
|
||||
.github/
|
||||
*.md
|
||||
*.spec.ts
|
||||
.all-contributorsrc
|
||||
|
||||
94
README.md
94
README.md
@@ -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.
|
||||
|
||||
### 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
|
||||
|
||||
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)
|
||||
|
||||
/* some sas code */
|
||||
data some sas tables;
|
||||
data a b c;
|
||||
set from js;
|
||||
run;
|
||||
|
||||
%webout(OPEN) /* open the JSON to be returned */
|
||||
%webout(OBJ,some) /* `some` table is sent in object format */
|
||||
%webout(ARR,sas) /* `sas` table is sent in array format, smaller filesize */
|
||||
%webout(OBJ,tables,fmt=N) /* unformatted (raw) data */
|
||||
%webout(OBJ,tables,label=newtable) /* rename tables on export */
|
||||
%webout(CLOSE) /* close the JSON and send some extra useful variables too */
|
||||
%webout(OPEN) /* Open the JSON to be returned */
|
||||
%webout(OBJ,a) /* Rows in table `a` are objects (easy to use) */
|
||||
%webout(ARR,b) /* Rows in table `b` are arrays (compact) */
|
||||
%webout(OBJ,c,fmt=N) /* Table `c` is sent unformatted (raw) */
|
||||
%webout(OBJ,c,label=d) /* Rename as `d` on JS side */
|
||||
%webout(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
|
||||
@@ -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:
|
||||
|
||||
* `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.
|
||||
* `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.
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
225
docs/classes/api_viya_spec.mockstream.html
Normal file
225
docs/classes/api_viya_spec.mockstream.html
Normal file
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
358
docs/classes/job_execution.fileuploader.html
Normal file
358
docs/classes/job_execution.fileuploader.html
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
366
docs/classes/job_execution.sas9jobexecutor.html
Normal file
366
docs/classes/job_execution.sas9jobexecutor.html
Normal file
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
1168
docs/classes/request.sas9requestclient.html
Normal file
1168
docs/classes/request.sas9requestclient.html
Normal file
File diff suppressed because one or more lines are too long
1131
docs/classes/request.sasjsrequestclient.html
Normal file
1131
docs/classes/request.sasjsrequestclient.html
Normal file
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
420
docs/classes/root.sasjsapiclient.html
Normal file
420
docs/classes/root.sasjsapiclient.html
Normal file
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
281
docs/classes/types_errors.invalidjsonerror.html
Normal file
281
docs/classes/types_errors.invalidjsonerror.html
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
308
docs/classes/types_errors.jobstatepollerror.html
Normal file
308
docs/classes/types_errors.jobstatepollerror.html
Normal file
File diff suppressed because one or more lines are too long
281
docs/classes/types_errors.jsonparsearrayerror.html
Normal file
281
docs/classes/types_errors.jsonparsearrayerror.html
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
347
docs/classes/types_errors.nosessionstateerror.html
Normal file
347
docs/classes/types_errors.nosessionstateerror.html
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
293
docs/classes/types_errors.rootfoldernotfounderror.html
Normal file
293
docs/classes/types_errors.rootfoldernotfounderror.html
Normal file
File diff suppressed because one or more lines are too long
281
docs/classes/types_errors.sas9autherror.html
Normal file
281
docs/classes/types_errors.sas9autherror.html
Normal file
File diff suppressed because one or more lines are too long
305
docs/classes/types_errors.weboutresponseerror.html
Normal file
305
docs/classes/types_errors.weboutresponseerror.html
Normal file
File diff suppressed because one or more lines are too long
235
docs/enums/types.loginmechanism.html
Normal file
235
docs/enums/types.loginmechanism.html
Normal file
File diff suppressed because one or more lines are too long
235
docs/enums/types.membertype.html
Normal file
235
docs/enums/types.membertype.html
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
139
docs/index.html
139
docs/index.html
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
183
docs/interfaces/root.sasjsauthresponse.html
Normal file
183
docs/interfaces/root.sasjsauthresponse.html
Normal file
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
243
docs/interfaces/types.executionquery.html
Normal file
243
docs/interfaces/types.executionquery.html
Normal file
File diff suppressed because one or more lines are too long
279
docs/interfaces/types.file.html
Normal file
279
docs/interfaces/types.file.html
Normal file
File diff suppressed because one or more lines are too long
225
docs/interfaces/types.filetree.html
Normal file
225
docs/interfaces/types.filetree.html
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
261
docs/interfaces/types.foldermember.html
Normal file
261
docs/interfaces/types.foldermember.html
Normal file
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
225
docs/interfaces/types.loginoptions.html
Normal file
225
docs/interfaces/types.loginoptions.html
Normal file
File diff suppressed because one or more lines are too long
243
docs/interfaces/types.loginresult.html
Normal file
243
docs/interfaces/types.loginresult.html
Normal file
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
147
docs/interfaces/types.process.html
Normal file
147
docs/interfaces/types.process.html
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
261
docs/interfaces/types.servicemember.html
Normal file
261
docs/interfaces/types.servicemember.html
Normal file
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
285
docs/interfaces/types.writestream.html
Normal file
285
docs/interfaces/types.writestream.html
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
455
docs/modules/api_viya.html
Normal file
455
docs/modules/api_viya.html
Normal file
File diff suppressed because one or more lines are too long
452
docs/modules/api_viya_spec.html
Normal file
452
docs/modules/api_viya_spec.html
Normal file
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
251
docs/modules/test.html
Normal file
251
docs/modules/test.html
Normal file
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
@@ -127,7 +127,7 @@ module.exports = {
|
||||
setupFiles: [],
|
||||
|
||||
// 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
|
||||
// snapshotSerializers: [],
|
||||
|
||||
18639
package-lock.json
generated
18639
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
68
package.json
68
package.json
@@ -40,40 +40,44 @@
|
||||
},
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/axios": "^0.14.0",
|
||||
"@types/form-data": "^2.5.0",
|
||||
"@types/jest": "^27.0.1",
|
||||
"@types/mime": "^2.0.3",
|
||||
"@types/tough-cookie": "^4.0.1",
|
||||
"copyfiles": "^2.4.1",
|
||||
"cp": "^0.2.0",
|
||||
"dotenv": "^10.0.0",
|
||||
"jest": "^27.2.0",
|
||||
"jest-extended": "^0.11.5",
|
||||
"node-polyfill-webpack-plugin": "^1.1.4",
|
||||
"path": "^0.12.7",
|
||||
"process": "^0.11.10",
|
||||
"rimraf": "^3.0.2",
|
||||
"semantic-release": "^17.4.7",
|
||||
"terser-webpack-plugin": "^5.2.4",
|
||||
"ts-jest": "^27.0.3",
|
||||
"ts-loader": "^9.2.2",
|
||||
"tslint": "^6.1.3",
|
||||
"tslint-config-prettier": "^1.18.0",
|
||||
"typedoc": "^0.22.3",
|
||||
"typedoc-neo-theme": "^1.1.1",
|
||||
"typedoc-plugin-external-module-name": "^4.0.6",
|
||||
"typescript": "4.3.5",
|
||||
"webpack": "^5.52.1",
|
||||
"webpack-cli": "^4.7.2"
|
||||
"@types/axios": "0.14.0",
|
||||
"@types/express": "4.17.13",
|
||||
"@types/form-data": "2.5.0",
|
||||
"@types/jest": "27.0.2",
|
||||
"@types/mime": "2.0.3",
|
||||
"@types/pem": "1.9.6",
|
||||
"@types/tough-cookie": "4.0.1",
|
||||
"copyfiles": "2.4.1",
|
||||
"cp": "0.2.0",
|
||||
"dotenv": "10.0.0",
|
||||
"express": "4.17.1",
|
||||
"jest": "27.4.7",
|
||||
"jest-extended": "2.0.0",
|
||||
"node-polyfill-webpack-plugin": "1.1.4",
|
||||
"path": "0.12.7",
|
||||
"pem": "1.14.4",
|
||||
"process": "0.11.10",
|
||||
"rimraf": "3.0.2",
|
||||
"semantic-release": "18.0.0",
|
||||
"terser-webpack-plugin": "5.3.0",
|
||||
"ts-jest": "27.1.3",
|
||||
"ts-loader": "9.2.6",
|
||||
"tslint": "6.1.3",
|
||||
"tslint-config-prettier": "1.18.0",
|
||||
"typedoc": "0.22.11",
|
||||
"typedoc-neo-theme": "1.1.1",
|
||||
"typedoc-plugin-external-module-name": "4.0.6",
|
||||
"typescript": "4.5.4",
|
||||
"webpack": "5.66.0",
|
||||
"webpack-cli": "4.7.2"
|
||||
},
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@sasjs/utils": "^2.30.0",
|
||||
"axios": "^0.21.4",
|
||||
"axios-cookiejar-support": "^1.0.1",
|
||||
"form-data": "^4.0.0",
|
||||
"https": "^1.0.0",
|
||||
"tough-cookie": "^4.0.0"
|
||||
"@sasjs/utils": "2.35.0",
|
||||
"axios": "0.26.0",
|
||||
"axios-cookiejar-support": "1.0.1",
|
||||
"form-data": "4.0.0",
|
||||
"https": "1.0.0",
|
||||
"tough-cookie": "4.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ npm i -g copyfiles
|
||||
```
|
||||
and then run to build:
|
||||
```bash
|
||||
npm run update:adapter && npm run build
|
||||
npm run update:adapter && npm run build
|
||||
```
|
||||
when it finishes run to deploy:
|
||||
```bash
|
||||
@@ -70,7 +70,7 @@ parmcards4;
|
||||
%webout(FETCH)
|
||||
%webout(OPEN)
|
||||
%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()
|
||||
%webout(CLOSE)
|
||||
;;;;
|
||||
@@ -79,7 +79,7 @@ parmcards4;
|
||||
%webout(FETCH)
|
||||
%webout(OPEN)
|
||||
%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()
|
||||
%webout(CLOSE)
|
||||
;;;;
|
||||
@@ -111,7 +111,7 @@ parmcards4;
|
||||
%macro x();
|
||||
%do i=1 %to %sysfunc(countw(&sasjs_tables));
|
||||
%let table=%scan(&sasjs_tables,&i);
|
||||
%webout(OBJ,&table)
|
||||
%webout(OBJ,&table,missing=STRING)
|
||||
%end;
|
||||
%mend;
|
||||
%x()
|
||||
@@ -125,7 +125,7 @@ parmcards4;
|
||||
%macro x();
|
||||
%do i=1 %to %sysfunc(countw(&sasjs_tables));
|
||||
%let table=%scan(&sasjs_tables,&i);
|
||||
%webout(ARR,&table)
|
||||
%webout(ARR,&table,missing=STRING)
|
||||
%end;
|
||||
%mend;
|
||||
%x()
|
||||
|
||||
30648
sasjs-tests/package-lock.json
generated
30648
sasjs-tests/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@sasjs/adapter": "file:../build/sasjs-adapter-5.0.0.tgz",
|
||||
"@sasjs/test-framework": "^1.4.0",
|
||||
"@sasjs/test-framework": "^1.4.3",
|
||||
"@types/jest": "^26.0.20",
|
||||
"@types/node": "^14.14.41",
|
||||
"@types/react": "^17.0.1",
|
||||
@@ -22,7 +22,7 @@
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"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-win": "scp %DEPLOY_PATH% ./build/*",
|
||||
"deploy": "npm run update:adapter && npm run build && npm run deploy:tests"
|
||||
@@ -43,6 +43,6 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"node-sass": "^5.0.0"
|
||||
"node-sass": "^6.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ const stringData: any = { table1: [{ col1: 'first col value' }] }
|
||||
|
||||
const defaultConfig: SASjsConfig = {
|
||||
serverUrl: window.location.origin,
|
||||
pathSASJS: '/SASjsApi/stp/execute',
|
||||
pathSAS9: '/SASStoredProcess/do',
|
||||
pathSASViya: '/SASJobExecution',
|
||||
appLoc: '/Public/seedapp',
|
||||
@@ -13,7 +14,6 @@ const defaultConfig: SASjsConfig = {
|
||||
debug: false,
|
||||
contextName: 'SAS Job Execution compute context',
|
||||
useComputeApi: false,
|
||||
allowInsecureRequests: false,
|
||||
loginMechanism: LoginMechanism.Default
|
||||
}
|
||||
|
||||
|
||||
@@ -79,6 +79,19 @@ const errorAndCsrfData: any = {
|
||||
_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 => ({
|
||||
name: 'Special Cases',
|
||||
tests: [
|
||||
@@ -247,6 +260,39 @@ export const specialCaseTests = (adapter: SASjs): TestSuite => ({
|
||||
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
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import * as https from 'https'
|
||||
import { generateTimestamp } from '@sasjs/utils/time'
|
||||
import * as NodeFormData from 'form-data'
|
||||
import { Sas9RequestClient } from './request/Sas9RequestClient'
|
||||
@@ -13,10 +14,10 @@ export class SAS9ApiClient {
|
||||
constructor(
|
||||
private serverUrl: string,
|
||||
private jobsPath: string,
|
||||
allowInsecureRequests: boolean
|
||||
httpsAgentOptions?: https.AgentOptions
|
||||
) {
|
||||
if (serverUrl) isUrl(serverUrl)
|
||||
this.requestClient = new Sas9RequestClient(serverUrl, allowInsecureRequests)
|
||||
this.requestClient = new Sas9RequestClient(serverUrl, httpsAgentOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,8 +22,8 @@ import { pollJobState } from './api/viya/pollJobState'
|
||||
import { getTokens } from './auth/getTokens'
|
||||
import { uploadTables } from './api/viya/uploadTables'
|
||||
import { executeScript } from './api/viya/executeScript'
|
||||
import { getAccessToken } from './auth/getAccessToken'
|
||||
import { refreshTokens } from './auth/refreshTokens'
|
||||
import { getAccessTokenForViya } from './auth/getAccessTokenForViya'
|
||||
import { refreshTokensForViya } from './auth/refreshTokensForViya'
|
||||
|
||||
/**
|
||||
* A client for interfacing with the SAS Viya REST API.
|
||||
@@ -534,21 +534,26 @@ export class SASViyaApiClient {
|
||||
clientSecret: string,
|
||||
authCode: string
|
||||
): 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.
|
||||
* @param clientId - the client ID 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(
|
||||
clientId: string,
|
||||
clientSecret: string,
|
||||
refreshToken: string
|
||||
) {
|
||||
return refreshTokens(
|
||||
return refreshTokensForViya(
|
||||
this.requestClient,
|
||||
clientId,
|
||||
clientSecret,
|
||||
|
||||
186
src/SASjs.ts
186
src/SASjs.ts
@@ -4,18 +4,23 @@ import {
|
||||
UploadFile,
|
||||
EditContextInput,
|
||||
PollOptions,
|
||||
LoginMechanism
|
||||
LoginMechanism,
|
||||
ExecutionQuery,
|
||||
FileTree
|
||||
} from './types'
|
||||
import { SASViyaApiClient } from './SASViyaApiClient'
|
||||
import { SAS9ApiClient } from './SAS9ApiClient'
|
||||
import { SASjsApiClient, SASjsAuthResponse } from './SASjsApiClient'
|
||||
import { AuthManager } from './auth'
|
||||
import {
|
||||
ServerType,
|
||||
MacroVar,
|
||||
AuthConfig,
|
||||
ExtraResponseAttributes
|
||||
ExtraResponseAttributes,
|
||||
SasAuthResponse
|
||||
} from '@sasjs/utils/types'
|
||||
import { RequestClient } from './request/RequestClient'
|
||||
import { SasjsRequestClient } from './request/SasjsRequestClient'
|
||||
import {
|
||||
JobExecutor,
|
||||
WebJobExecutor,
|
||||
@@ -29,6 +34,7 @@ import { LoginOptions, LoginResult } from './types/Login'
|
||||
|
||||
const defaultConfig: SASjsConfig = {
|
||||
serverUrl: '',
|
||||
pathSASJS: '/SASjsApi/stp/execute',
|
||||
pathSAS9: '/SASStoredProcess/do',
|
||||
pathSASViya: '/SASJobExecution',
|
||||
appLoc: '/Public/seedapp',
|
||||
@@ -36,7 +42,6 @@ const defaultConfig: SASjsConfig = {
|
||||
debug: false,
|
||||
contextName: 'SAS Job Execution compute context',
|
||||
useComputeApi: null,
|
||||
allowInsecureRequests: false,
|
||||
loginMechanism: LoginMechanism.Default
|
||||
}
|
||||
|
||||
@@ -49,6 +54,7 @@ export default class SASjs {
|
||||
private jobsPath: string = ''
|
||||
private sasViyaApiClient: SASViyaApiClient | null = null
|
||||
private sas9ApiClient: SAS9ApiClient | null = null
|
||||
private sasJSApiClient: SASjsApiClient | null = null
|
||||
private fileUploader: FileUploader | null = null
|
||||
private authManager: AuthManager | null = null
|
||||
private requestClient: RequestClient | null = null
|
||||
@@ -57,7 +63,7 @@ export default class SASjs {
|
||||
private jesJobExecutor: JobExecutor | null = null
|
||||
private sas9JobExecutor: JobExecutor | null = null
|
||||
|
||||
constructor(config?: any) {
|
||||
constructor(config?: Partial<SASjsConfig>) {
|
||||
this.sasjsConfig = {
|
||||
...defaultConfig,
|
||||
...config
|
||||
@@ -75,7 +81,7 @@ export default class SASjs {
|
||||
userName: string,
|
||||
password: string
|
||||
) {
|
||||
this.isMethodSupported('executeScriptSAS9', ServerType.Sas9)
|
||||
this.isMethodSupported('executeScriptSAS9', [ServerType.Sas9])
|
||||
|
||||
return await this.sas9ApiClient?.executeScript(
|
||||
linesOfCode,
|
||||
@@ -89,7 +95,7 @@ export default class SASjs {
|
||||
* @param accessToken - an access token for an authorized user.
|
||||
*/
|
||||
public async getComputeContexts(accessToken: string) {
|
||||
this.isMethodSupported('getComputeContexts', ServerType.SasViya)
|
||||
this.isMethodSupported('getComputeContexts', [ServerType.SasViya])
|
||||
|
||||
return await this.sasViyaApiClient!.getComputeContexts(accessToken)
|
||||
}
|
||||
@@ -99,7 +105,7 @@ export default class SASjs {
|
||||
* @param accessToken - an access token for an authorized user.
|
||||
*/
|
||||
public async getLauncherContexts(accessToken: string) {
|
||||
this.isMethodSupported('getLauncherContexts', ServerType.SasViya)
|
||||
this.isMethodSupported('getLauncherContexts', [ServerType.SasViya])
|
||||
|
||||
return await this.sasViyaApiClient!.getLauncherContexts(accessToken)
|
||||
}
|
||||
@@ -108,7 +114,7 @@ export default class SASjs {
|
||||
* Gets default(system) launcher contexts.
|
||||
*/
|
||||
public getDefaultComputeContexts() {
|
||||
this.isMethodSupported('getDefaultComputeContexts', ServerType.SasViya)
|
||||
this.isMethodSupported('getDefaultComputeContexts', [ServerType.SasViya])
|
||||
|
||||
return this.sasViyaApiClient!.getDefaultComputeContexts()
|
||||
}
|
||||
@@ -118,7 +124,7 @@ export default class SASjs {
|
||||
* @param authConfig - an access token, refresh token, client and secret for an authorized user.
|
||||
*/
|
||||
public async getExecutableContexts(authConfig: AuthConfig) {
|
||||
this.isMethodSupported('getExecutableContexts', ServerType.SasViya)
|
||||
this.isMethodSupported('getExecutableContexts', [ServerType.SasViya])
|
||||
|
||||
return await this.sasViyaApiClient!.getExecutableContexts(authConfig)
|
||||
}
|
||||
@@ -140,7 +146,7 @@ export default class SASjs {
|
||||
accessToken: string,
|
||||
authorizedUsers?: string[]
|
||||
) {
|
||||
this.isMethodSupported('createComputeContext', ServerType.SasViya)
|
||||
this.isMethodSupported('createComputeContext', [ServerType.SasViya])
|
||||
|
||||
return await this.sasViyaApiClient!.createComputeContext(
|
||||
contextName,
|
||||
@@ -165,7 +171,7 @@ export default class SASjs {
|
||||
launchType: string,
|
||||
accessToken: string
|
||||
) {
|
||||
this.isMethodSupported('createLauncherContext', ServerType.SasViya)
|
||||
this.isMethodSupported('createLauncherContext', [ServerType.SasViya])
|
||||
|
||||
return await this.sasViyaApiClient!.createLauncherContext(
|
||||
contextName,
|
||||
@@ -186,7 +192,7 @@ export default class SASjs {
|
||||
editedContext: EditContextInput,
|
||||
accessToken?: string
|
||||
) {
|
||||
this.isMethodSupported('editComputeContext', ServerType.SasViya)
|
||||
this.isMethodSupported('editComputeContext', [ServerType.SasViya])
|
||||
|
||||
return await this.sasViyaApiClient!.editComputeContext(
|
||||
contextName,
|
||||
@@ -201,7 +207,7 @@ export default class SASjs {
|
||||
* @param accessToken - an access token for an authorized user.
|
||||
*/
|
||||
public async deleteComputeContext(contextName: string, accessToken?: string) {
|
||||
this.isMethodSupported('deleteComputeContext', ServerType.SasViya)
|
||||
this.isMethodSupported('deleteComputeContext', [ServerType.SasViya])
|
||||
|
||||
return await this.sasViyaApiClient!.deleteComputeContext(
|
||||
contextName,
|
||||
@@ -219,7 +225,7 @@ export default class SASjs {
|
||||
contextName: string,
|
||||
accessToken?: string
|
||||
) {
|
||||
this.isMethodSupported('getComputeContextByName', ServerType.SasViya)
|
||||
this.isMethodSupported('getComputeContextByName', [ServerType.SasViya])
|
||||
|
||||
return await this.sasViyaApiClient!.getComputeContextByName(
|
||||
contextName,
|
||||
@@ -233,7 +239,7 @@ export default class SASjs {
|
||||
* @param accessToken - an access token for an authorized user.
|
||||
*/
|
||||
public async getComputeContextById(contextId: string, accessToken?: string) {
|
||||
this.isMethodSupported('getComputeContextById', ServerType.SasViya)
|
||||
this.isMethodSupported('getComputeContextById', [ServerType.SasViya])
|
||||
|
||||
return await this.sasViyaApiClient!.getComputeContextById(
|
||||
contextId,
|
||||
@@ -242,7 +248,7 @@ export default class SASjs {
|
||||
}
|
||||
|
||||
public async createSession(contextName: string, accessToken: string) {
|
||||
this.isMethodSupported('createSession', ServerType.SasViya)
|
||||
this.isMethodSupported('createSession', [ServerType.SasViya])
|
||||
|
||||
return await this.sasViyaApiClient!.createSession(contextName, accessToken)
|
||||
}
|
||||
@@ -262,7 +268,7 @@ export default class SASjs {
|
||||
authConfig?: AuthConfig,
|
||||
debug?: boolean
|
||||
) {
|
||||
this.isMethodSupported('executeScriptSASViya', ServerType.SasViya)
|
||||
this.isMethodSupported('executeScriptSASViya', [ServerType.SasViya])
|
||||
if (!contextName) {
|
||||
throw new Error(
|
||||
'Context name is undefined. Please set a `contextName` in your SASjs or override config.'
|
||||
@@ -352,7 +358,7 @@ export default class SASjs {
|
||||
* @param accessToken - the access token to authorize the request.
|
||||
*/
|
||||
public async getFolder(folderPath: string, accessToken?: string) {
|
||||
this.isMethodSupported('getFolder', ServerType.SasViya)
|
||||
this.isMethodSupported('getFolder', [ServerType.SasViya])
|
||||
return await this.sasViyaApiClient!.getFolder(folderPath, accessToken)
|
||||
}
|
||||
|
||||
@@ -362,7 +368,7 @@ export default class SASjs {
|
||||
* @param accessToken - an access token for authorizing the request.
|
||||
*/
|
||||
public async deleteFolder(folderPath: string, accessToken: string) {
|
||||
this.isMethodSupported('deleteFolder', ServerType.SasViya)
|
||||
this.isMethodSupported('deleteFolder', [ServerType.SasViya])
|
||||
|
||||
return await this.sasViyaApiClient?.deleteFolder(folderPath, accessToken)
|
||||
}
|
||||
@@ -377,7 +383,7 @@ export default class SASjs {
|
||||
accessToken?: string,
|
||||
limit?: number
|
||||
) {
|
||||
this.isMethodSupported('listFolder', ServerType.SasViya)
|
||||
this.isMethodSupported('listFolder', [ServerType.SasViya])
|
||||
|
||||
return await this.sasViyaApiClient?.listFolder(
|
||||
sourceFolder,
|
||||
@@ -399,7 +405,7 @@ export default class SASjs {
|
||||
targetFolderName: string,
|
||||
accessToken: string
|
||||
) {
|
||||
this.isMethodSupported('moveFolder', ServerType.SasViya)
|
||||
this.isMethodSupported('moveFolder', [ServerType.SasViya])
|
||||
|
||||
return await this.sasViyaApiClient?.moveFolder(
|
||||
sourceFolder,
|
||||
@@ -417,7 +423,7 @@ export default class SASjs {
|
||||
accessToken?: string,
|
||||
sasApiClient?: SASViyaApiClient
|
||||
) {
|
||||
this.isMethodSupported('createJobDefinition', ServerType.SasViya)
|
||||
this.isMethodSupported('createJobDefinition', [ServerType.SasViya])
|
||||
|
||||
if (sasApiClient)
|
||||
return await sasApiClient!.createJobDefinition(
|
||||
@@ -437,7 +443,7 @@ export default class SASjs {
|
||||
}
|
||||
|
||||
public async getAuthCode(clientId: string) {
|
||||
this.isMethodSupported('getAuthCode', ServerType.SasViya)
|
||||
this.isMethodSupported('getAuthCode', [ServerType.SasViya])
|
||||
|
||||
return await this.sasViyaApiClient!.getAuthCode(clientId)
|
||||
}
|
||||
@@ -452,8 +458,14 @@ export default class SASjs {
|
||||
clientId: string,
|
||||
clientSecret: string,
|
||||
authCode: string
|
||||
) {
|
||||
this.isMethodSupported('getAccessToken', ServerType.SasViya)
|
||||
): Promise<SasAuthResponse | SASjsAuthResponse> {
|
||||
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(
|
||||
clientId,
|
||||
@@ -462,12 +474,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(
|
||||
clientId: string,
|
||||
clientSecret: string,
|
||||
refreshToken: string
|
||||
) {
|
||||
this.isMethodSupported('refreshTokens', ServerType.SasViya)
|
||||
): Promise<SasAuthResponse | SASjsAuthResponse> {
|
||||
this.isMethodSupported('refreshTokens', [
|
||||
ServerType.SasViya,
|
||||
ServerType.Sasjs
|
||||
])
|
||||
|
||||
if (this.sasjsConfig.serverType === ServerType.Sasjs)
|
||||
return await this.sasJSApiClient!.refreshTokens(refreshToken)
|
||||
|
||||
return await this.sasViyaApiClient!.refreshTokens(
|
||||
clientId,
|
||||
@@ -477,7 +501,7 @@ export default class SASjs {
|
||||
}
|
||||
|
||||
public async deleteClient(clientId: string, accessToken: string) {
|
||||
this.isMethodSupported('deleteClient', ServerType.SasViya)
|
||||
this.isMethodSupported('deleteClient', [ServerType.SasViya])
|
||||
|
||||
return await this.sasViyaApiClient!.deleteClient(clientId, accessToken)
|
||||
}
|
||||
@@ -533,18 +557,29 @@ export default class SASjs {
|
||||
* 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.
|
||||
*/
|
||||
public async logIn(
|
||||
username?: string,
|
||||
password?: string,
|
||||
clientId?: string,
|
||||
options: LoginOptions = {}
|
||||
): Promise<LoginResult> {
|
||||
if (this.sasjsConfig.loginMechanism === LoginMechanism.Default) {
|
||||
if (!username || !password) {
|
||||
if (!username || !password)
|
||||
throw new Error(
|
||||
'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)
|
||||
}
|
||||
|
||||
@@ -700,15 +735,19 @@ export default class SASjs {
|
||||
msg: string
|
||||
} {
|
||||
if (data === null) return { status: true, msg: '' }
|
||||
|
||||
const isSasFormatsTable = (key: string) =>
|
||||
key.match(/^\$.*/) && Object.keys(data).includes(key.replace(/^\$/, ''))
|
||||
|
||||
for (const key in data) {
|
||||
if (!key.match(/^[a-zA-Z_]/)) {
|
||||
if (!key.match(/^[a-zA-Z_]/) && !isSasFormatsTable(key)) {
|
||||
return {
|
||||
status: false,
|
||||
msg: 'First letter of table should be alphabet or underscore.'
|
||||
}
|
||||
}
|
||||
|
||||
if (!key.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/)) {
|
||||
if (!key.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/) && !isSasFormatsTable(key)) {
|
||||
return { status: false, msg: 'Table name should be alphanumeric.' }
|
||||
}
|
||||
|
||||
@@ -719,7 +758,7 @@ export default class SASjs {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.getType(data[key]) !== 'Array') {
|
||||
if (this.getType(data[key]) !== 'Array' && !isSasFormatsTable(key)) {
|
||||
return {
|
||||
status: false,
|
||||
msg: 'Parameter data contains invalid table structure.'
|
||||
@@ -735,6 +774,7 @@ export default class SASjs {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { status: true, msg: '' }
|
||||
}
|
||||
|
||||
@@ -770,7 +810,7 @@ export default class SASjs {
|
||||
accessToken?: string,
|
||||
isForced = false
|
||||
) {
|
||||
this.isMethodSupported('deployServicePack', ServerType.SasViya)
|
||||
this.isMethodSupported('deployServicePack', [ServerType.SasViya])
|
||||
|
||||
let sasApiClient: any = null
|
||||
if (serverUrl || appLoc) {
|
||||
@@ -792,7 +832,7 @@ export default class SASjs {
|
||||
sasApiClient = new SAS9ApiClient(
|
||||
serverUrl,
|
||||
this.jobsPath,
|
||||
this.sasjsConfig.allowInsecureRequests
|
||||
this.sasjsConfig.httpsAgentOptions
|
||||
)
|
||||
}
|
||||
} else {
|
||||
@@ -824,6 +864,28 @@ export default class SASjs {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
return await this.sasJSApiClient?.executeJob(query)
|
||||
}
|
||||
|
||||
/**
|
||||
* Kicks off execution of the given job via the compute API.
|
||||
* @returns an object representing the compute session created for the given job.
|
||||
@@ -857,7 +919,7 @@ export default class SASjs {
|
||||
...config
|
||||
}
|
||||
|
||||
this.isMethodSupported('startComputeJob', ServerType.SasViya)
|
||||
this.isMethodSupported('startComputeJob', [ServerType.SasViya])
|
||||
if (!config.contextName) {
|
||||
throw new Error(
|
||||
'Context name is undefined. Please set a `contextName` in your SASjs or override config.'
|
||||
@@ -949,21 +1011,27 @@ export default class SASjs {
|
||||
}
|
||||
|
||||
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.allowInsecureRequests
|
||||
this.sasjsConfig.httpsAgentOptions
|
||||
)
|
||||
} else {
|
||||
this.requestClient.setConfig(
|
||||
this.sasjsConfig.serverUrl,
|
||||
this.sasjsConfig.allowInsecureRequests
|
||||
this.sasjsConfig.httpsAgentOptions
|
||||
)
|
||||
}
|
||||
|
||||
this.jobsPath =
|
||||
this.sasjsConfig.serverType === ServerType.SasViya
|
||||
? this.sasjsConfig.pathSASViya
|
||||
: this.sasjsConfig.pathSAS9
|
||||
: this.sasjsConfig.serverType === ServerType.Sas9
|
||||
? this.sasjsConfig.pathSAS9
|
||||
: this.sasjsConfig.pathSASJS
|
||||
|
||||
this.authManager = new AuthManager(
|
||||
this.sasjsConfig.serverUrl,
|
||||
@@ -973,30 +1041,44 @@ export default class SASjs {
|
||||
)
|
||||
|
||||
if (this.sasjsConfig.serverType === ServerType.SasViya) {
|
||||
if (this.sasViyaApiClient)
|
||||
if (this.sasViyaApiClient) {
|
||||
this.sasViyaApiClient!.setConfig(
|
||||
this.sasjsConfig.serverUrl,
|
||||
this.sasjsConfig.appLoc
|
||||
)
|
||||
else
|
||||
} else {
|
||||
this.sasViyaApiClient = new SASViyaApiClient(
|
||||
this.sasjsConfig.serverUrl,
|
||||
this.sasjsConfig.appLoc,
|
||||
this.sasjsConfig.contextName,
|
||||
this.requestClient
|
||||
)
|
||||
}
|
||||
|
||||
this.sasViyaApiClient.debug = this.sasjsConfig.debug
|
||||
}
|
||||
|
||||
if (this.sasjsConfig.serverType === ServerType.Sas9) {
|
||||
if (this.sas9ApiClient)
|
||||
if (this.sas9ApiClient) {
|
||||
this.sas9ApiClient!.setConfig(this.sasjsConfig.serverUrl)
|
||||
else
|
||||
} else {
|
||||
this.sas9ApiClient = new SAS9ApiClient(
|
||||
this.sasjsConfig.serverUrl,
|
||||
this.jobsPath,
|
||||
this.sasjsConfig.allowInsecureRequests
|
||||
this.sasjsConfig.httpsAgentOptions
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.sasjsConfig.serverType === ServerType.Sasjs) {
|
||||
if (this.sasJSApiClient) {
|
||||
this.sasJSApiClient.setConfig(this.sasjsConfig.serverUrl)
|
||||
} else {
|
||||
this.sasJSApiClient = new SASjsApiClient(
|
||||
this.sasjsConfig.serverUrl,
|
||||
this.requestClient
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
this.fileUploader = new FileUploader(
|
||||
@@ -1018,7 +1100,8 @@ export default class SASjs {
|
||||
this.sasjsConfig.serverUrl,
|
||||
this.sasjsConfig.serverType!,
|
||||
this.jobsPath,
|
||||
this.sasjsConfig.allowInsecureRequests
|
||||
this.requestClient,
|
||||
this.sasjsConfig.httpsAgentOptions
|
||||
)
|
||||
|
||||
this.computeJobExecutor = new ComputeJobExecutor(
|
||||
@@ -1085,12 +1168,15 @@ export default class SASjs {
|
||||
})
|
||||
}
|
||||
|
||||
private isMethodSupported(method: string, serverType: string) {
|
||||
if (this.sasjsConfig.serverType !== serverType) {
|
||||
private isMethodSupported(method: string, serverTypes: ServerType[]) {
|
||||
if (
|
||||
!this.sasjsConfig.serverType ||
|
||||
!serverTypes.includes(this.sasjsConfig.serverType)
|
||||
) {
|
||||
throw new Error(
|
||||
`Method '${method}' is only supported on ${
|
||||
serverType === ServerType.Sas9 ? 'SAS9' : 'SAS Viya'
|
||||
} servers.`
|
||||
`Method '${method}' is only supported on ${serverTypes.join(
|
||||
', '
|
||||
)} servers.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
102
src/SASjsApiClient.ts
Normal file
102
src/SASjsApiClient.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { AuthConfig, ServerType } from '@sasjs/utils/types'
|
||||
import { FileTree, ExecutionQuery } from './types'
|
||||
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 {
|
||||
constructor(
|
||||
private serverUrl: string,
|
||||
private requestClient: RequestClient
|
||||
) {}
|
||||
|
||||
public setConfig(serverUrl: string) {
|
||||
if (serverUrl) this.serverUrl = serverUrl
|
||||
}
|
||||
|
||||
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<{
|
||||
status: string
|
||||
message: string
|
||||
example?: {}
|
||||
}>(
|
||||
'SASjsApi/drive/deploy',
|
||||
{ fileTree: members, appLoc: appLoc },
|
||||
access_token
|
||||
)
|
||||
|
||||
return Promise.resolve(result)
|
||||
}
|
||||
|
||||
public async executeJob(query: ExecutionQuery) {
|
||||
const { result } = await this.requestClient.post<{
|
||||
status: string
|
||||
message: string
|
||||
log?: string
|
||||
logPath?: string
|
||||
error?: {}
|
||||
_webout?: string
|
||||
}>('SASjsApi/stp/execute', query, undefined)
|
||||
|
||||
if (Object.keys(result).includes('_webout')) {
|
||||
result._webout = parseWeboutResponse(result._webout!)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -168,7 +168,7 @@ export class SessionManager {
|
||||
) {
|
||||
if (stateLink) {
|
||||
if (this.debug && !this.printedSessionState.printed) {
|
||||
logger.info('Polling session status...')
|
||||
logger.info(`Polling: ${this.serverUrl + stateLink.href}`)
|
||||
|
||||
this.printedSessionState.printed = true
|
||||
}
|
||||
|
||||
@@ -272,7 +272,13 @@ export async function executeScript(
|
||||
|
||||
return { result: jobResult?.result, log }
|
||||
} catch (e) {
|
||||
if (e && e.status === 404) {
|
||||
interface HttpError {
|
||||
status: number
|
||||
}
|
||||
|
||||
const error = e as HttpError
|
||||
|
||||
if (error.status === 404) {
|
||||
return executeScript(
|
||||
requestClient,
|
||||
sessionManager,
|
||||
@@ -287,7 +293,7 @@ export async function executeScript(
|
||||
true
|
||||
)
|
||||
} else {
|
||||
throw prefixMessage(e, 'Error while executing script. ')
|
||||
throw prefixMessage(e as Error, 'Error while executing script. ')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,10 +206,11 @@ const doPoll = async (
|
||||
|
||||
pollCount++
|
||||
|
||||
const jobHref = postedJob.links.find((l: Link) => l.rel === 'self')!.href
|
||||
|
||||
if (pollOptions?.streamLog) {
|
||||
const jobUrl = postedJob.links.find((l: Link) => l.rel === 'self')
|
||||
const { result: job } = await requestClient.get<Job>(
|
||||
jobUrl!.href,
|
||||
jobHref,
|
||||
authConfig?.access_token
|
||||
)
|
||||
|
||||
@@ -231,7 +232,7 @@ const doPoll = async (
|
||||
}
|
||||
|
||||
if (debug && printedState !== state) {
|
||||
logger.info('Polling job status...')
|
||||
logger.info(`Polling: ${requestClient.getBaseUrl() + jobHref}/state`)
|
||||
logger.info(`Current job state: ${state}`)
|
||||
|
||||
printedState = state
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user